ClojureScript

常見問題 (給 JavaScript 開發者)

本頁旨在解答 JavaScript 開發人員在使用 ClojureScript 時可能會有的一些疑慮。

語言特性與語義

實際上,你可以變更不可變資料結構,只是「變更」的意義不同。在這裡,變更意味著建立一個新的資料結構,它與你開始時的資料結構有一些差異。

在 JavaScript 中,當你處理字串、布林值和數字時,你已經在這樣做了:遞增一個數字是建立一個新的數字;附加到一個字串是建立一個新的字串。

所以你不應該將不可變資料結構視為受限的容器,而應將它們視為複合值

你應該因為持久性資料結構的實作效率很難而感到安心。幕後有一些複雜的演算法在為你執行繁重的工作,所以你可以魚與熊掌兼得。這類似於垃圾收集:一種演算法創新,產生對電腦來說不那麼自然,但對人類來說更自然的語義。

這一切都很好,但使用值進行程式設計如何讓我的生活更好呢?

最直接的好處是你永遠不必使用諸如防禦性複製、複製等脆弱的技術,因為你永遠不必擔心你的程式碼的其他部分會在你不注意的情況下修改你的資料。

讓我們來看一個具體的例子。假設你的網路應用程式中有一個 Person 模型,以及一個讓你編輯並提交對 Person 欄位值變更的元件。當使用值進行程式設計時,以下功能很容易實現

  1. 從你的模型在某個時間點的目前版本開始編輯

  2. 編輯時,你的修改僅限於你的編輯元件,而不會影響你的應用程式的其他部分

  3. 反之,讓你的應用程式看到來自外部來源(例如從你的 API 推送的更新)對你的模型的變更,而你的本地編輯過程仍然處理它開始時的版本

  4. 在編輯時支援復原和重做

  5. 當提交你的變更時,樂觀地變更你的網路應用程式中你的模型的值,以便你的變更立即可見,如果伺服器回覆你錯誤,則還原它們。

更深入地說,有充分的理由相信,對於建立資訊系統而言,值比可變資料結構更自然。在網路密集型應用程式(如網路應用程式)中尤其如此(你從網路上取得/傳送的是值;你的 AJAX 呼叫不可能給你一個可以讓你修改資料庫的資料結構)。有一個演講為值提供了絕佳的案例。

最後,即使不考慮基於值進行程式設計的好處,你也會發現 ClojureScript 的集合非常強大且使用愉快,這要歸功於非常完整的標準函式庫。

由於資料結構是不可變的,而且區域變數也不是變數,我要如何建立一個隨著時間演變的程式呢?

別擔心:你無需使用單子 (monads)。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 原子比 JavaScript 變數優越,就像 JavaScript 函式比 Java 方法優越一樣:因為它們是一級物件。它們可以傳遞給函式、被資料結構引用,並且可以在它們之上建立抽象。此外,可以觀察原子。

你會發現你使用的 ClojureScript 原子比你使用的 JavaScript 變數少得多。原子和不可變資料結構的組合可以讓你將狀態管理在幾個關鍵位置,而不是散佈在你的程式中。

很多人將巨集定義為一種自訂編譯第一步的簡單方法。這是一種你只有在使用過巨集後才能理解的定義,所以讓我們用不同的方式來解釋它。

巨集可以讓你指定程式碼的某些轉換;使用一個名為 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]}

如你所見,主要區別是你擺脫了逗號和冒號。

有趣的是,雖然看起來不多,但這對網路程式設計的一個重要部分: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 抽象,由所有預設集合實作。

產生器

惰性序列

集合和映射

標準函式庫的一部分(作為持久性資料結構),允許使用任意鍵。

Proxy

不相關

符號

 根據設計,ClojureScript 反對資訊隱藏,因此沒有私有成員。關鍵字可以命名空間,減少映射鍵中發生衝突的可能性。協議讓你擴展現有資料類型的行為,而無需新的可見成員。

Math + Number + String + Object API

 標準函式庫和 Google Closure 程式庫中都有類似的功能。

數字字面值

任何進制都有字面值,八進制和十六進制(當然還有十進制)有特殊的語法

Promise

可透過函式庫使用

Reflect 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 之外,最受歡迎的 React ClojureScript 包裝器(Om、Reagent 和 Quiescent)都讓您毫不費力地包含 React 元件。

工具

絕對有。許多人使用 Emacs 和 Clojure,也有適用於 IntelliJVim、Eclipse 和 Sublime Text 的優秀外掛程式。

您可能會發現結構化編輯比您習慣的方式更實用,因為它自然地讓您操作程式碼的建構區塊(即表達式,而不是行或字)。

有。大多數 ClojureScript 開發人員使用 Leiningen 來管理 ClojureScript 專案,它負責處理相依性載入、封裝,並具有用於前端開發工作流程的外掛程式(CSS 預處理、資產最小化等)。另一個熱門工具是 Boot

特別是,由於即時程式碼重新載入和 ClojureScript REPL,ClojureScript 與 Figwheel 無疑是互動式前端開發的最新技術。

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 和語言內建的文件在加速學習過程中扮演著關鍵的角色。

很可能,最大的挑戰不是吸收新概念,而是忘記舊概念。正如尤達所說:「你必須忘記你所學的[關於命令式和物件導向範式]」;這使得經驗豐富的程式設計師更難適應。話雖如此,如果你一直以接近函式式風格開發 JavaScript(例如,正如 Douglas Crockford 所倡導的),這就不會是個問題。

我聽說函式式程式設計只適合擁有博士學位的人。

你不需要了解範疇論、Monad 等等才能使用 ClojureScript。對於你 99% 的工作,你需要的程式設計概念都是你在 JavaScript 中已經使用的概念(動態型別、函式、資料結構)。

社群

有,而且它正在快速成長。實際上,社群通常是人們最喜歡 Clojure 的原因。Slack 和各種郵件列表非常活躍,當你需要幫助時,人們都會很積極地回應。

Clojure(Script) 社群的特點是實用主義、創造力、高品質標準和明智的領導力的結合。

(而且,這裡的人們都很友善。)

我想知道 Clojure 的人們是否只是沉迷於一些優雅的想法,而沒有清楚地了解它們在現實世界中的實際附加價值。

Clojure 社群中的大多數人在接觸到其創作者的想法時(特別是透過一些很棒的演講,如簡單造就容易價值的價值),經歷了一種頓悟,並在採用 Clojure 的過程中獲得了豐厚的回報。因此,我們有時可能會過於熱情,以至於看起來不太客觀。

但您應該知道,實用性和務實一直是 Clojure 的核心價值觀,這促成了許多基本決策,例如將目標鎖定在流行的 JVM 和 JavaScript 執行環境,以及犧牲相較於其他語言的某些函式式純粹性的設計決策。您可以從 Clojure 社群在開發實用工具和將 Clojure 的觸角延伸到廣泛的平台所付出的努力中,看出他們一直忠於這種精神。

雜項

Clojure 基於一個根本的信念,即以簡潔為目標可以顯著增強程式設計師的能力。反直覺的含義是,擺脫複雜的工具和技術實際上會讓你更有效率。在你親身體驗之前,你不會相信這一點。

您可以在這裡建議其他常見問題。

同時,請隨時透過 StackOverflow 或郵件列表與社群聯繫,這裡的人們非常友善!

原始作者:David Nolen