ClojureScript
與 Clojure 的差異

與 Clojure 的差異

基本原理

ClojureScript 的基本原理與 Clojure 大致相同,只是將 JavaScript 作為平台,並額外強調 JS 的普及性,因為它顯然不是一個那麼豐富的平台。

關於 ClojureScript 的基本原理的更深入討論可以在本網站的其他地方找到。

狀態與身分

與 Clojure 相同。Clojure 的身分模型比可變狀態更簡單且更健壯,即使在單執行緒環境中也是如此。

動態開發

與 Clojure 一樣,ClojureScript 支援 REPL 驅動的開發,為各種 JavaScript 環境提供易於啟動的 REPL。有關詳細資訊,請參閱快速入門

此外,ClojureScript 的自我託管能力支援將動態特性擴展到純 JavaScript 環境,可以在其中建立第三方 REPL 和其他動態設施。

函數式編程

ClojureScript 具有與 JVM 上 Clojure 相同的不可變持續性集合。

Lisp

與 Clojure 不同,ClojureScript 巨集的定義和使用不能在同一個編譯階段中混合。請參閱下面的「巨集」部分。

運行時多型

  • ClojureScript 協定具有與 Clojure 協定相同的語意。

並行編程

Clojure 的數值、狀態、身分和時間模型即使在單執行緒環境中也很有價值。

  • Atoms 的運作方式與 Clojure 中相同

  • 沒有 Refs 或 STM

  • binding 的使用者體驗與 Clojure 中類似

    • Vars

      • 未在運行時重新化

      • 通過分析器存取 Clojure 資料結構可以避免許多開發時重新化的使用

    • def 會產生普通的 JS 變數

  • 目前尚未實作 Agents

託管於 JVM

入門

請參閱快速入門

讀取器

  • 數字

    • 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 註釋

    • Vars 不會在運行時重新化。當編譯器遇到 var 特殊形式時,它會發出一個 Var 實例,反映編譯時元數據。(這滿足許多常見的靜態用例。)

  • def 註釋

    • def 會產生普通的 JS 變數

    • 編譯器不會強制執行 :private 元數據

      • 私有變數存取會觸發分析警告

    • :const 元數據

      • 將導致內嵌編譯時靜態 EDN 值

      • 導致符號解析為 ^:const Vars 的 case 測試常數內嵌其值

    • 除非設定 :def-emits-var 編譯器選項(REPL 的預設值為 true),否則 def 表單會求值為 init 表單的值(而不是變數)

  • if 註釋

    • 有關 Java 的布林包裝盒的部分在 ClojureScript 中不相關

  • fn 註釋

    • 目前在呼叫 fn 時,沒有運行時實作對數量的強制執行

  • monitor-entermonitor-exitlocking 未實作

巨集

ClojureScript 的巨集必須在與使用它們的編譯階段不同的階段中定義。實現此目的的一種方法是在一個命名空間中定義它們,然後從另一個命名空間中使用它們。

巨集通過命名空間宣告中的 :require-macros 關鍵字引用

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

可以使用 Sugar 和其他 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 實作的移植版本

    • 持續性向量、雜湊對應和雜湊集合中已實作暫時性支援

    • 已實作大多數但非全部的集合函數

  • StructMaps

    • ClojureScript 不會實作 defstructcreate-structstruct-mapstructaccessor

Seqs

  • Seqs 具有與 Clojure 中相同的語意,並且幾乎所有 Seq 函式庫函數都可在 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

      • 所有選項都可以省略

      • 在這種情況下,符號可以直接用作 libspec

        • 也就是說,(:require lib.foo)(:require [lib.foo]) 都受支援,並且意思相同

      • :rename 指定從參照的變數名稱到不同符號的映射(可用於防止衝突)

      • 前綴列表 不受支援

    • :refer-clojure 的唯一選項是 :exclude:rename

    • :import 僅適用於匯入 Google Closure 類別

      • ClojureScript 類型和記錄應該使用 :use:require :refer 導入,而不是 :import

  • 巨集必須在與它們被使用的不同的編譯階段中定義。實現此目的的一種方法是在一個命名空間中定義它們,然後在另一個命名空間中使用它們。它們透過 :require-macros / :use-macros 選項引用到 ns

    • :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]]))

自動別名 clojure 命名空間:如果需要或使用不存在的 clojure.* 命名空間,並且存在匹配的 cljs.* 命名空間,則會載入 cljs.* 命名空間,並且會自動建立從 clojure.* 命名空間到 cljs.* 命名空間的別名。例如

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

將會自動轉換為

(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 中,def 會產生變數本身。在 ClojureScript 中,def 會產生,除非設定了 REPL 選項 :def-emits-var(對於 REPL,這預設為 true)。

  • Atoms 的運作方式與 Clojure 中相同

  • 目前尚未實作 Refs 和 Agents

  • 驗證器的運作方式與 Clojure 中相同

  • 未實作 intern - 沒有具體化的變數

Refs 和交易

目前不支援 Refs 和交易。

Agents

目前不支援 Agents。

Atoms

Atoms 的運作方式與 Clojure 中相同。

主機互通

主機語言互通功能(new/. 等)在可能的情況下,運作方式與 Clojure 中相同,例如

goog/LOCALE
=> "en"

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

在 ClojureScript 中,Foo/bar 始終表示 Foo 是一個命名空間。它不能用於 Clojure 中常見的 Java 靜態欄位存取模式,因為 JavaScript 中沒有反射資訊來判斷這一點。

特殊的命名空間 js 提供對全域屬性的存取

js/Infinity
=> Infinity

若要存取物件屬性(包括您想要作為值的函數,而不是執行),請使用前導連字號

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

類型提示

雖然 ^long^double(當在函數參數上使用時)在 Clojure 中是類型宣告,但在 ClojureScript 中它們是類型提示

類型提示主要用於避免 Clojure 中的反射。在 ClojureScript 中,唯一重要的類型提示是 ^boolean 類型提示:它用於避免檢查過的 if 評估(它會處理以下事實,例如,在 JavaScript 中 0"" 為 false,但在 ClojureScript 中為 true)。

編譯和類別產生

編譯與 Clojure 不同

  • 所有 ClojureScript 程式都會編譯成(可選擇最佳化的)JavaScript。

  • 可以將個別檔案編譯成個別的 JS 檔案以分析輸出

  • 生產編譯是透過 Google Closure 編譯器的全程式編譯

  • 在 ClojureScript 中,gen-classgen-interface 等是不必要且未實作的

其他程式庫

ClojureScript 目前包含以下從 Clojure 移植過來的非核心命名空間

  • clojure.set

  • clojure.string

  • clojure.walk

  • clojure.zip

  • clojure.data

  • clojure.core.reducers

    • fold 目前是 reduce 的別名

  • cljs.pprint (clojure.pprint 的移植版本)

  • cljs.spec (clojure.spec 的移植版本)

  • cljs.test (clojure.test 的移植版本)

貢獻

Clojure 和 ClojureScript 共享相同的 貢獻者協議和開發流程