ClojureScript
与 Clojure 的区别

与 Clojure 的区别

基本原理

ClojureScript 的基本原理与 Clojure 非常相似,只是将 JavaScript 作为平台,并额外强调了 JS 的覆盖范围,因为它显然不像 Clojure 那样是一个丰富的平台。

可以在本网站的其他地方找到关于 ClojureScript 基本原理的更深入讨论

状态和标识

与 Clojure 相同。即使在单线程环境中,Clojure 的标识模型也比可变状态更简单、更健壮。

动态开发

与 Clojure 一样,ClojureScript 支持 REPL 驱动的开发,为各种 JavaScript 环境提供易于启动的 REPL。有关详细信息,请参见 快速入门

此外,ClojureScript 的自托管功能支持将动态特性扩展到纯 JavaScript 环境,在这些环境中可以创建第三方 REPL 和其他动态设施。

函数式编程

ClojureScript 与 JVM 上的 Clojure 具有相同的不可变持久集合。

Lisp

与 Clojure 不同,ClojureScript 宏定义及其使用不能在同一个编译阶段混合使用。请参见下面的宏部分。

运行时多态

  • ClojureScript 协议与 Clojure 协议具有相同的语义。

并发编程

即使在单线程环境中,Clojure 的值、状态、标识和时间模型也很有价值。

  • 原子与 Clojure 中的原子一样

  • 没有引用或 STM

  • binding 的用户体验类似于 Clojure 中的体验

    • 变量

      • 在运行时不进行具体化

      • 通过分析器访问 Clojure 数据结构,可以避免在开发时对具体化的许多使用

    • def 生成普通的 JS 变量

  • 目前未实现代理

托管在 JVM 上

  • ClojureScript 托管在 JavaScript VM 上

  • 可以选择使用 Google 的 Closure 编译器进行优化

  • 旨在利用 Google 的 Closure 库,并参与其依赖项/要求/提供机制

入门

请参见 快速入门

读取器

  • 数字

    • ClojureScript 目前仅支持映射到 JavaScript 原语的整数和浮点字面量

      • 目前不支持 Ratio、BigDecimal 和 BigInteger 字面量

      • 数字的相等性与 JavaScript 相似,而不是 Clojure:(= 0.0 0) ⇒ true

  • 字符

    • ClojureScript 没有字符字面量。相反,字符与 JavaScript 中的字符相同(即单字符字符串)

  • 列表、向量、映射和集合字面量与 Clojure 中的相同

  • 宏字符

    • 由于 ClojureScript 中没有字符类型,因此 \ 生成一个单字符字符串。

  • read

    • readread-string 函数位于 cljs.reader 命名空间中

  • 标记字面量

    • Clojure 标记字面量 相同,只是在编译阶段使用的读取器函数类似于 Clojurescript 宏,因为它们应该返回 Clojurescript 代码形式(或字符串或数字之类的字面量)。

    • Clojure 编译器不会自动要求 data_readers.clj/c 中引用的读取器函数,但 Clojurescript 编译器会。

    • 有关更多详细信息,请参见 读取器

REPL 和 main

  • 请参见 快速入门,了解 ClojureScript REPL 的使用方法。

  • 标准 ClojureScript REPL 支持 Clojure 的 main 模式。

评估

  • ClojureScript 与 Clojure 具有相同的评估规则

  • load 存在,但仅作为 REPL 特殊函数

  • load-file 存在,但仅作为 REPL 特殊函数

  • 虽然 Clojure 会清除局部变量,但 ClojureScript 不会

特殊形式

以下 ClojureScript 特殊形式与其 Clojure 对应形式相同:ifdoletletfnquotelooprecurthrowtry

  • var 注释

    • 变量在运行时不进行具体化。当编译器遇到 var 特殊形式时,它会发出一个 Var 实例,该实例反映了 **编译时** 元数据。(这满足了许多常见的 **静态** 使用场景。)

  • def 注释

    • def 生成普通的 JS 变量

    • 编译器不会强制执行 :private 元数据

      • 私有变量访问会触发分析警告

    • :const 元数据

      • 会导致对编译时静态 EDN 值进行内联

      • 导致 case 测试常量(这些常量是解析为 ^:const 变量的符号)与其值一起进行内联

    • def 形式会评估为 init 形式的值(而不是变量),除非设置了 :def-emits-var 编译器选项(默认为 REPL 的 true

  • if 注释

    • 有关 Java 布尔值的包装类的部分与 ClojureScript 无关

  • fn 注释

    • 目前在调用 fn 时没有运行时强制执行的元数

  • monitor-entermonitor-exitlocking 未实现

ClojureScript 的宏必须在与使用它们的阶段不同的 **编译阶段** 中定义。实现这一点的一种方法是在一个命名空间中定义它们,然后在另一个命名空间中使用它们。

宏通过命名空间声明中的 :require-macros 关键字引用

(ns my.namespace
  (:require-macros [my.macros :as my]))

可以采用带糖和其他 ns 变体来代替使用 :require-macros 原语;有关详细信息,请参见下面的命名空间部分。

宏在 *.clj*.cljc 文件中编写,并且在使用常规 ClojureScript 时作为 Clojure 编译,或者在使用自举/自托管 ClojureScript 时作为 ClojureScript 编译。需要注意的是,基于 Clojure 的 ClojureScript 宏生成的代码必须针对 ClojureScript 中的功能。

ClojureScript 命名空间 *可以* 从同一个命名空间中要求宏,只要它们在不同的编译阶段。因此,例如,foo.cljsfoo.cljc 文件可以使用 foo.cljcfoo.clj 文件中的宏。

与 Clojure 不同,在 ClojureScript 中,宏和函数可以具有相同的名称(例如,cljs.core/+ 宏和 cljs.core/+ 函数可以共存)。

你可能想知道:“如果是这样的话,我该怎么得到它呢?” ClojureScript(与 Clojure 不同)有两个不同的阶段,使用两个独立的、不交互的命名空间。宏扩展首先发生,因此像 (+ 1 1) 这样的形式最初会涉及 cljs.core/+ 宏。另一方面,在像 (reduce + [1 1]) 这样的形式中,+ 符号不在运算符位置,并且在不进行宏扩展的情况下传递给分析/编译,在那里它被解析为 cljs.core/+ 函数。

其他函数

  • 打印

    • *out**err* 目前未实现

  • 正则表达式支持

  • 断言

    • 在 JVM ClojureScript 中,无法在运行时将 *assert* 动态设置为 false。相反,必须使用 :elide-asserts 编译器选项来实现省略。(另一方面,在自托管 ClojureScript 中,*assert* 的行为与 Clojure 相同。)

数据结构

  • nil

    • 虽然在 Clojure 中,nil 与 Java 的 null 相同,但在 ClojureScript 中,nil 等效于 JavaScript 的 nullundefined

  • 数字

    • 目前,ClojureScript 数字只是 JavaScript 数字

  • 未实现强制转换,因为目前没有要强制转换的类型

  • 字符

    • JavaScript 没有字符类型。Clojure 字符在内部表示为单字符字符串

  • 关键字

    • ClojureScript 关键字不保证是 identical?,要进行快速相等性测试,请使用 keyword-identical?

  • 集合

    • 可用的持久集合

      • Clojure 实现的移植

    • 持久向量、哈希映射和哈希集的瞬态支持

    • 大多数但不是全部集合函数都已实现

  • 结构化映射

    • ClojureScript 没有实现 defstructcreate-structstruct-mapstructaccessor

序列

  • 序列的语义与 Clojure 中的相同,并且几乎所有序列库函数都可以在 ClojureScript 中使用。

协议

  • defprotocoldeftypeextend-typeextend-protocol 的工作原理与 Clojure 中的相同

  • 协议不像 Clojure 那样进行具体化,没有运行时协议对象

  • 一些反射功能(satisfies?)与 Clojure 中的相同

    • satisfies? 是一个宏,必须传递协议名称

  • extend 目前未实现

  • specify,将不可变值扩展到协议 - 实例级别的 extend-type,无需包装器

元数据

与 Clojure 中的相同。

命名空间

ClojureScript 中的命名空间被编译为 Google Closure 命名空间,这些命名空间表示为嵌套的 JavaScript 对象。重要的是,这意味着命名空间和变量有发生冲突的可能性 - 但是编译器可以检测到这些问题,并且在发生这种情况时会发出警告。

  • 目前,你必须仅使用 ns 形式,并注意以下事项

    • 你必须使用 :use:only 形式

    • :require 支持 :as:refer:rename

      • 不支持 :refer :all

      • 所有选项都可以跳过

      • 在这种情况下,可以将符号直接用作库规范

        • 也就是说,(:require lib.foo)(:require [lib.foo]) 都受支持,并且含义相同

      • :rename 指定了一个映射,用于将引用的变量名称映射到不同的符号(可用于避免冲突)

      • 前缀列表 不受支持

    • :refer-clojure 的唯一选项是 :exclude:rename

    • :import 仅可用于导入 Google Closure 类

      • ClojureScript 类型和记录应使用 :use:require :refer 引入,而不是使用 :import

  • 宏必须在与使用它们的编译阶段不同的 编译阶段 中定义。一种实现方法是在一个命名空间中定义它们,并在另一个命名空间中使用它们。它们通过 ns:require-macros / :use-macros 选项引用

    • :require-macros:use-macros 支持与 :require:use 相同的形式

隐式宏加载:如果需要或使用一个命名空间,并且该命名空间本身需要或使用来自其自身命名空间的宏,那么宏将使用相同的规范隐式地需要或使用。此外,在这种情况下,宏变量可以包含在 :refer:only 规范中。这通常会导致简化的库使用,这样使用命名空间就不必担心显式区分某些变量是函数还是宏。例如

(ns testme.core (:require [cljs.test :as test :refer [test-var deftest]]))

将导致 test/is 正确解析,以及 test-var 函数和 deftest 宏在没有限定的情况下可用。

内联宏规范:为了方便起见,:require 可以提供 :include-macros true:refer-macros [syms…​]。两者都反糖成显式加载包含宏的匹配 Clojure 文件的形式。(这独立于所需要的命名空间是否内部需要或使用自己的宏。)例如

(ns testme.core
  (:require [foo.core :as foo :refer [foo-fn] :include-macros true]
            [woz.core :as woz :refer [woz-fn] :refer-macros [apple jax]]))

(ns testme.core
  (:require [foo.core :as foo :refer [foo-fn]]
            [woz.core :as woz :refer [woz-fn]])
  (:require-macros [foo.core :as foo]
                   [woz.core :as woz :refer [apple jax]]))

的糖

(ns testme.core (:require [clojure.test]))

自动别名 clojure 命名空间:如果需要或使用不存在的 clojure.* 命名空间,并且存在匹配的 cljs.* 命名空间,那么将加载 cljs.* 命名空间,并且将自动从 clojure.* 命名空间到 cljs.* 命名空间建立别名。例如

(ns testme.core (:require [cljs.test :as clojure.test]))

将自动转换为

现有的 Clojure 库必须符合 ClojureScript 子集才能在 ClojureScript 中工作。

此外,Clojure 库中的宏必须能够作为 ClojureScript 编译,才能通过其 cljs.js/*load-fn* 功能在自托管/引导的 ClojureScript 中使用。

  • 变量和全局环境

    • defbinding 的工作方式与 Clojure 相同

    • 但在普通 js 变量上

    • Clojure 可以表示未绑定的变量。在 ClojureScript 中,(def x) 会导致 (nil? x)true。(在这种情况下,(identical? nil x)false,但 (identical? js/undefined x)true。)

  • 原子与 Clojure 中的原子一样

  • 在 Clojure 中,def 会产生 变量本身。在 ClojureScript 中,def 会产生 ,除非设置了 REPL 选项 :def-emits-var(对于 REPL,默认值为 true)。

  • 目前未实现 Ref 和 Agent

  • 验证器的工作方式与 Clojure 相同

intern 未实现 - 没有具象化变量

Ref 和事务

目前不支持 Ref 和事务。

Agent

目前不支持 Agent。

Atom

Atom 的工作方式与 Clojure 相同。

宿主互操作

goog/LOCALE
=> "en"

(let [sb (goog.string.StringBuffer. "hello, ")]
 (.append sb "world")
 (.toString sb))
    => "hello, world"

宿主语言互操作功能(new/. 等)的工作方式与 Clojure 相同(如果可能),例如

在 ClojureScript 中,Foo/bar 始终表示 Foo 是一个命名空间。它不能用于 Clojure 中常见的 Java 静态字段访问模式,因为 JavaScript 中没有反射信息来确定这一点。

js/Infinity
=> Infinity

特殊命名空间 js 提供对全局属性的访问

(.-NEGATIVE_INFINITY js/Number)
=> -Infinity

要访问对象属性(包括您想要作为值的函数,而不是执行的函数),请使用前导连字符

提示

虽然 ^long^double(在函数参数上使用时)在 Clojure 中是类型 声明,但在 ClojureScript 中是类型 提示

类型提示主要用于避免 Clojure 中的反射。在 ClojureScript 中,唯一有意义的类型提示是 ^boolean 类型提示:它用于避免检查 if 评估(它处理了这样一个事实,即例如,0"" 在 JavaScript 中为假,在 ClojureScript 中为真)。

编译和类生成

  • 编译与 Clojure 不同

  • 所有 ClojureScript 程序都被编译成(可选地优化过的)JavaScript。

  • 单个文件可以编译成单个 JS 文件,以便分析输出

  • 生产编译是通过 Google Closure 编译器进行的整个程序编译

gen-classgen-interface 等在 ClojureScript 中不必要且未实现

其他库

  • ClojureScript 目前包含以下从 Clojure 移植的非核心命名空间

  • clojure.set

  • clojure.string

  • clojure.walk

  • clojure.zip

  • clojure.data

    • clojure.core.reducers

  • fold 目前是 reduce 的别名

  • cljs.pprintclojure.pprint 的移植版)

  • cljs.specclojure.spec 的移植版)

cljs.testclojure.test 的移植版)

贡献

Clojure 和 ClojureScript 共享相同的 贡献者协议和开发流程