ClojureScript

常见问题解答 (面向 JavaScript 开发者)

此页面旨在解答 JavaScript 开发者在使用 ClojureScript 时可能遇到的疑问。

语言特性和语义

实际上,你可以更改不可变数据结构,只是“更改”的含义有所不同。在这种情况下,更改意味着创建新的数据结构,该结构与你开始时的结构有所不同。

在 JavaScript 中,你在处理字符串、布尔值和数字时已经做过这样的操作:对数字进行递增是创建新的数字;将内容追加到字符串是创建新的字符串。

因此,你不应该将不可变数据结构视为受限的容器,而应该视为复合值

你应该放心的是,持久化数据结构的高效实现非常困难。在幕后有一些复杂的算法在为你做繁重的工作,这样你就可以鱼与熊掌兼得。这与垃圾回收类似:算法创新带来了对计算机而言不太自然、但对人类而言更自然的语义。

这很好,但使用值编程如何让我的生活更美好呢?

最直接的益处是你永远不需要使用诸如防御性复制、克隆等脆弱的技术,因为你永远不需要担心代码的另一部分会偷偷地修改你的数据。

让我们看一个具体例子。想象一下,你的 Web 应用中有一个 Person 模型,以及一个组件,它允许你编辑和提交 Person 字段值的更改。在使用值编程时,很容易实现以下功能:

  1. 在某个时间点从模型的当前版本开始编辑

  2. 在编辑时,让你的修改只对你的编辑组件生效,而不会影响你的应用程序的其他部分

  3. 相反,让你的应用程序看到来自外部来源的模型更改(例如,从你的 API 推送的更新),而你的本地编辑过程仍然处理它开始时的版本

  4. 在编辑时支持撤销和重做

  5. 提交更改时,在你的 Web 应用中乐观地更改模型的值,以便你的更改立即可见,并在服务器返回错误时将其撤销。

更深入地说,有充分的理由相信,值比可变数据结构更适合创建信息系统。这在网络密集型应用程序(例如 Web 应用)中尤为明显(值是你从网络获取/发送的内容;你的 AJAX 调用不会为你提供一个数据结构,让你能够修改数据库)。有一个演讲为值做出了很好的论证。

最后,即使不考虑基于值的编程的好处,你也会发现 ClojureScript 的集合非常强大且易于使用,这要归功于一个非常完整的标准库。

既然数据结构是不可变的,并且局部变量不是可变的,我该如何编写一个随时间推移而不断发展的程序呢?

别担心:你不必使用 Monad。ClojureScript 承认对可变状态的需要,并提供了一种引用类型,即 atom,来管理它。以下是一个示例:

JavaScript

// declaring the state
var state = {
  count: 0
};

// ...

// updating the state :

state.count = state.count + 1;

ClojureScript

;; declaring the state
(def state (atom {:count 0}))

;; ...

;; updating the state
(swap! state #(update % :count inc))

ClojureScript 的 atom 比 JavaScript 变量更强大,就像 JavaScript 函数比 Java 方法更强大一样:因为它们是头等公民。它们可以传递给函数,由数据结构引用,并且可以基于它们构建抽象。此外,atom 可以被观察。

你会发现你使用 ClojureScript 的 atom 的次数远远少于使用 JavaScript 变量的次数。atom 和不可变数据结构的结合使你能够在几个关键位置管理你的状态,而不是让它们散布在你的整个程序中。

许多人将宏定义为一种轻松定制编译初始步骤的方式。只有在使用过宏之后,你才会理解这种定义,所以让我们从不同的角度来解释它。

宏允许你指定代码的某些转换;使用名为 my-macro 的宏就像对 ClojureScript 说:“当我写 (my-macro <这段漂亮的代码>) 时,我的意思是 (<这段更繁琐的代码>) ”。正如函数将程序执行的某些部分分解一样,宏将代码编写过程的某些部分分解。

因此,当你有了宏后,你总是可以尽可能地让调用代码变得舒适,因为语法永远不会阻碍你的前进。

例如,doto 宏允许你编写:

(def my-date (doto (new Date)
               (.setDate 7) (.setMonth 7) (.setFullYear 1991)))

这就像你写了:

(def my-date
  (let [d (new Date)]
    (.setDate d 7)
    (.setMonth d 7)
    (.setFullYear d 1991)
    d))

你本来会在 JavaScript 中(痛苦地)写:

var myDate = (function(){
  var d = new Date();
  d.setDate(7);
  d.setMonth(7);
  d.setFullYear(1991);
  return d;
}());

更深刻地讲,宏可以轻松地将两个重要的关注点分离:创建结构良好的程序,并从语法的角度使其变得实用。

但宏不仅仅是消除样板代码的一种方式。通过允许你扩展和操作 ClojureScript 的语法,它们使你能够将新的范式引入你的程序中。

ClojureScript 中的宏使你能够按需作为库将这些“功能”添加到 ClojureScript 中:

还需要注意的是,宏没有运行时开销,因为它们所做的一切都发生在生成 JavaScript 时。

实际上,即使在 ClojureScript 社区,也被认为使用宏时不要过度使用是良好的风格。ClojureScript 应用程序开发者很少编写宏,因为大多数情况下函数也能完成任务,并且更容易理解。

但有时,你需要进行只有宏才能实现的概念性飞跃,因为语法中的繁琐无法用函数缓解,或者因为它需要代码分析。

宏就像飞机。你不想每天都乘飞机去上班。但有时,飞机允许你在几小时内到达另一个大陆,所以我们很高兴拥有它们。

的确,从 JavaScript 语法过渡到 Clojure 语法是很可怕的

JavaScript

myFun(x, y, z);

ClojureScript

(myFun x y z)

你需要将一个括号从运算符的一侧移到另一侧,并删除逗号和分号。

Clojure 的语法(即 EDN - 可扩展数据表示法)是使 Clojure 中编写宏变得实用的原因。

它很可能对你来说很陌生,但并不违反自然规律。一旦你习惯了它,你就会发现它比 JavaScript 更规律、更简洁。

这很有趣,让我们再为数据结构字面量做一次这样的转换:

JavaScript

{a : "b",
 c : [d, e]}

ClojureScript

{:a "b"
 :c [d e]}

如你所见,主要区别在于你摆脱了逗号和冒号。

有趣的是,虽然看起来变化不大,但这对 Web 编程的一个重要部分有很大影响:HTML 模板。

Clojure 的数据表示法非常轻量级,以至于一些 Clojure 库使用它将 HTML 模板嵌入语言中:

[:div.text-right
  [:span "Click here: "]
  [:button {:class "btn" :on-click #(do something)} "Click me!"]]

有些人被 Clojurescript 代码所需的括号数量困扰。你会发现,一旦你习惯了Clojure 缩进约定,它们就不再是问题。不用说,你应该使用一个能够帮助你匹配括号、花括号和方括号的编辑器。如果你还使用一个能够帮助你格式化代码的编辑器,你就能立即发现自己是否在括号使用方面犯了错误。

ClojureScript 与 JavaScript 的互操作性非常好。ClojureScript 函数就是普通的 JavaScript 函数。该语言提供了访问原生浏览器对象的基元,访问和设置对象的属性,创建和操作 JavaScript 对象和数组,以及调用任何 JavaScript 函数或方法。

你也可以在 ClojureScript 中编写函数并从 JavaScript 中调用它们。

是的,例如许多 ClojureScript 开发者使用 React 或 d3 等库。

ES2015、ES7 等为 JavaScript 带来了很多表达能力(箭头函数、生成器、解构等),同时解决了它的几个缺点和不足(模块、块级作用域等)。这是否意味着 ClojureScript 等语言变得毫无意义了呢?

通过列出它们的特性来比较编程语言是一项危险的练习,但我们还是试一下吧。更新的 ECMAScript 版本带来的大多数语法增强,Clojure 都提供。下表列出了最新的 ECMAScript 特性及其 ClojureScript 等效项。

ECMAScript ClojureScript

箭头函数

Clojure 函数表达式本身就很简洁,并且是基于表达式的(比较 (fn [x y] (+ x y))function (x, y){return x + y;})。此外,还有一种更轻量级的函数语法 (#(+ %1 %2))。

ClojureScript 不是基于类的设计;使用数据类型和协议来获得面向对象的优点。有关详细信息,请参阅下一部分

模板字符串

ClojureScript 字符串是多行的,并且没有逗号的存在使得使用 str 函数进行连接非常自然;你也可以使用来自 Google Closure 库的 C 风格格式。

解构

 已提供,还支持嵌套和函数参数。

默认参数和剩余参数

完全支持

Let 和 const

 它们与 ClojureScript 的 let 的语义完全相同。

迭代器和 For..Of

 Seq 抽象,它由所有默认集合实现。

生成器

惰性 seq

集合和映射

标准库的一部分(作为持久数据结构),允许使用任意键。

代理

不相关

符号

 按照设计,ClojureScript 反对信息隐藏,所以没有私有成员。关键字可以命名空间,减少了映射键冲突的可能性。协议允许你扩展现有数据类型的行为,而无需添加新的可见成员。

数学 + 数字 + 字符串 + 对象 API

 标准库和 Google Closure 库中具有类似的功能。

数字字面量

对于任何基数都有字面量,八进制和十六进制(当然还有十进制)有特殊的语法。

承诺

通过库提供。

反射 API

不相关

尾调用

通过显式的 recur 结构部分支持。

在很大程度上,Clojure 是为了 解决 类语言(如 Java 和 Ruby)中面向对象所带来的局限性。

从 Clojure 的角度来看,类将数据表示、程序逻辑、代码组织、状态管理和多态性混为一谈;Clojure 通过数据结构、函数、命名空间、管理引用和“按需多态性”结构(协议和多方法)分别解决了所有这些问题。

但是如果我没有继承,我该如何重用代码?

即使在面向对象的领域,专家也会告诉你为了代码重用,要优先考虑组合而不是继承。由于函数非常细粒度,所以它们很容易组合。

数据结构也比类更容易重用,因为它们隐含的特殊性更少。

(注意,这两项优势并不特定于 Clojure;你可能已经在使用 JavaScript 的函数式、面向数据的方式时体验过它们)。

如果你有很强的面向对象背景,你可能需要一段时间才能适应没有类的生活。不要担心。这条学习曲线会越来越平缓,你的努力将会得到回报。

生态系统

是 ClojureScript 本身!ClojureScript 带有一个出色的集合库。你所熟悉和喜爱的所有集合函数都在那里(map、reduce、filter、remove 等),它们适用于抽象,这意味着它们不受限于 Javascript 对象和类数组。

你不会在 ClojureScript 中找到 AngularJs 或 Backbone 的等价物。这实际上是一个好兆头。在很大程度上,这些客户端框架的动力来自于 JavaScript 缺乏模块化、原语和一个像样的标准库。

ClojureScript 作为平台解决了这些问题,并依赖于 Google Closure 库来解决浏览器不一致性问题。构建应用程序的其他问题(例如模板、服务器通信、路由等)通过组合专门用途的库来解决。

要开始 ClojureScript 项目,ClojureScript Wiki 提供了几个 项目模板,以及 目录。

是的,确实

ClojureScript 作为一种编程语言非常成熟。Clojure 在 2008 年发布之前经过了多年的精心设计。当 ClojureScript 在 2011 年发布时,Clojure 已经在 JVM 上经过了多年的测试和验证。这与 Clojure 对简洁的强调以及宏消除了许多困难的语言设计决策相结合,使得 Clojure 作为一种语言在短短几年内就达到了稳定状态。

如果 JavaScript 所经历的转型对你来说是一个问题(新的语言特性、社区中的范式转变、编程商店中的约定更改以及它们在工具和库中引起的所有更改),ClojureScript 可能是一个不错的选择。

当然,语言并不是一切,ClojureScript 库生态系统将在未来几年经历重要的转型。但需要注意的是,这也发生在 JavaScript 生态系统中,正如 React 和 Falcor 等改变范式的库的大量采用以及应用程序平台和需求所经历的变化所表明的那样。

React 和 Flux

是的,几乎 ClojureScript 社区的每个人都使用 React,因为它与 ClojureScript 体现的函数式编程具有很深的协同作用。事实上,很多 ClojureScript 程序员认为 ClojureScript 是利用 React 的最佳方式。

丰富的集合库、高级控制流运算符(如 condcasewhenletif-let、模式匹配等)以及“一切都是表达式”的事实,使你能够以非常直接和声明式的方式编写渲染函数(你不需要布置一堆中间变量)。

由于持久数据结构和管理引用(原子)的组合,Flux 在 ClojureScript 中很容易实现。这是像 Om 这样的有影响力的库存在的理由的一部分。

在很多方面,ClojureScript 正引领着更广泛的 React/Flux 社区,正如不可变数据结构的逐步采用、“所有状态都在一个地方”原则以及其他函数式技术所表明的那样。

是的!除了你可以在 ClojureScript 中直接使用 React 的事实之外,最流行的 ClojureScript 到 React 的包装器(Om、Reagent 和 Quiescent)都允许你毫不费力地包含 React 组件。

工具

当然。很多人使用 带 Clojure 的 Emacs,还有用于 IntelliJVim、Eclipse 和 Sublime Text 的出色插件。

你可能会发现结构编辑比你习惯的更实用,因为它自然地让你操作代码的构建块(即表达式,而不是行或词)。

是的。大多数 ClojureScript 开发人员使用 Leiningen 来管理 ClojureScript 项目,它负责依赖项加载、打包,并具有用于前端开发工作流程的插件(CSS 预处理、资产缩小等)。另一个流行的工具是 Boot

特别是,ClojureScript 与 Figwheel 结合使用,可以说是交互式前端开发的最新技术,这得益于实时代码重载和 ClojureScript REPL。

Google Closure 为前端 JavaScript 开发人员提供了引人注目的优势。

  • 一个非常全面、经过实战检验的库。

  • 非常高效的缩小。

  • 死代码消除,即未使用代码将被删除。(这就是库能够全面的原因:你不必担心添加功能时带来的额外字节。)

大多数 JavaScript 开发人员拒绝了 Google Closure,因为它有一个主要的缺点:为了使死代码消除有效,他们必须严格遵守关于他们编写的 JavaScript 的纪律(特别是,它最终看起来非常像 Java)。

当你从 ClojureScript 使用 Google Closure 时,你不会遇到这种障碍,因为 ClojureScript 编译器会生成针对 Closure 优化的 JavaScript。这意味着你可以获得上面列出的所有好处,而无需对你的语言语义进行任何妥协。

死代码消除对于应用程序开发人员来说是一个很大的便利,但它对生态系统的开发具有更深层的意义。库作者不再需要在功能与重量之间进行权衡,因为用户只会获得他们使用的字节。

平台

首先,ClojureScript 针对所有主要的 JavaScript 引擎。因此,你可以在 NodeJS 上运行 ClojureScript。

但是,你不会找到很多用于编写 NodeJS 服务器的 ClojureScript 示例。出于各种原因,人们倾向于在服务器上使用原始的 JVM Clojure(库生态系统更成熟,而且它不会强迫你编写异步代码)。

自 Clojure 1.7 以来,编写针对 Java 和 JavaScript 运行时的 Clojure 代码变得非常容易。

但是,当这种方法失效时(在你需要依赖服务器和客户端的 JavaScript 特定功能的情况下),人们倾向于转向 Nashorn,它是 Java 8 中嵌入的 JavaScript 运行时。

这将我们引向了

各种 概念证明 已经发布,但目前还没有像 Fluxible 这样的现成库解决方案来解决这个问题。

ClojureScript 编译成兼容 ES3 的代码。在 ClojureScript 中编写可移植代码比在普通 JS 中需要更少的纪律。

实际应用

调试情况如何?

ClojureScript 对传统的 JavaScript 调试技术提供了极好的支持:你可以在源代码中设置断点,获取 ClojureScript 堆栈跟踪等。这得益于设计精良的源映射,即使在具有高级缩小的生产代码中也能正常工作。

除了这一点之外,REPL 驱动的开发和热代码重载为调试带来了全新的维度,使你能够测试和修改有状态程序,而不会消除错误的条件。

锦上添花:宏在这里也能帮到你。一种典型的 JavaScript 调试技术是在代码中间插入 console.log 调用,但这很快就会变得乏味和侵入性。

例如,假设你有这段代码,并且怀疑 myFun 函数有错误

var result = x + myFun(y);

你需要做的是

var z = myFun(y);
console.log("myFun(y) : ", z);
var result = x + z;

在 ClojureScript 中,你可以定义一个 spy 宏,它以非常轻量级的方式执行相同操作。因此等效的 ClojureScript 代码

(let [result (+ x (myFun y))])

将变成

(let [result (+ x (spy (myFun y)))])

你将在控制台中看到相同的信息。很酷吧?

在许多人看来,比 JavaScript 还要好:)。语言语义不那么滑溜,工具现在已经足够成熟,可以放心地使用。

显然,由于 ClojureScript 是 JavaScript 之上的抽象,因此它必然更慢,尤其是因为持久数据结构的原因。

实际上,ClojureScript 的创建者惊喜地发现 JavaScript 的功能对于 ClojureScript 的语义来说是一个非常自然和直接的基础(例如,ClojureScript 函数是普通的 JavaScript 函数;ClojureScript 协议以非常直接的方式映射到 JavaScript 原型)。因此,ClojureScript 速度很快。

ClojureScript 的持久数据结构显然比 JavaScript 数组和对象慢,但没有你想象的那么慢,因为它们不是以天真的方式实现的(不是克隆或写时复制)。这里有一个 基准测试,可以让你了解一下。

此外,持久数据结构允许进行可变数据结构无法实现的全局优化。特别是,ClojureScript 已经通过使用持久数据结构进行缓存,显著提高了 React 应用的性能(http://swannodette.github.io/2013/12/17/the-future-of-javascript-mvcs)。

学习 ClojureScript

这在很大程度上取决于个人的编程经验和能力。

如果你是一名 JavaScript 开发人员,你学习 Clojure(Script) 的起点会比学习经典语言(如 Ruby 或 Java)的开发者更好,因为 JavaScript 已经让你了解了函数一等公民、动态类型、使用数据结构(而不是类)传递信息以及在命名空间(而不是类层次结构)中组织代码。

从概念上讲,你在 ClojureScript 中需要学习的东西比 JavaScript 少得多,因为你不必学习如何避免 JavaScript 的所有陷阱。REPL 和语言中包含的文档在加快学习进程方面发挥着至关重要的作用。

最有可能的是,最大的挑战不是要吸收新的概念,而是要忘记旧的概念。正如 Yoda 所说:“你必须忘掉你所学到的东西 [关于命令式和面向对象范式的]”;这对经验丰富的程序员来说更难。话虽如此,如果你以接近函数式的方式使用 JavaScript 进行开发(例如,Douglas Crockford 建议 的方式),这不会成为问题。

我听说函数式编程只有拥有博士学位的人才能理解。

你不需要知道范畴论、单子等来使用 ClojureScript。在 99% 的工作中,你需要的编程概念是你已经在 JavaScript 中使用的那些(动态类型、函数、数据结构)。

社区

有,而且发展迅速。实际上,社区通常是人们最喜欢 Clojure 的地方。Slack 和各种邮件列表非常活跃,无论何时你需要帮助,人们都会积极响应。

Clojure(Script) 社区的一个特点是将实用性、创造力、高标准和明智的领导力结合在一起。

(而且,人们都很友善。)

我想知道 Clojure 人们是否只是爱上了一些优雅的想法,却没有清醒地认识到它们在现实世界中的实际附加价值。

Clojure 社区中的大多数人在接触到其创建者(特别是通过一些优秀的演讲,如 Simple Made EasyThe Value of Values)的想法时,都经历了一种顿悟,并在采用 Clojure 的体现时获得了极大的回报。因此,我们有时可能会热情到显得不那么客观。

但你应该知道,实用性和务实性一直是 Clojure 的核心价值观,它推动了诸如针对流行平台(即 JVM 和 JavaScript 运行时)以及与其他语言相比牺牲一些函数式纯度等基本决策。你可以通过 Clojure 社区为开发实用工具以及将 Clojure 的影响范围扩展到广泛的平台所付出的所有努力,来判断他们一直忠实于这种精神。

其他

Clojure 基于这样一种基本理念:追求简单可以极大地增强程序员的能力。与直觉相反的是,去除复杂工具和技术实际上会让你更有效。除非你体验过,否则你不会相信它。

你可以在这里为本常见问题解答建议更多问题。

同时,不要犹豫,在 StackOverflow 或 邮件列表 上联系社区,人们非常友好!

原文作者:David Nolen