文章出處

前言

 兩年多前知道cljs的存在時十分興奮,但因為工作中根本用不上,國內也沒有專門的職位于是擱置了對其的探索。而近一兩年來又刮起了函數式編程的風潮,恰逢有幸主理新項目的前端架構,于是引入Ramda.js來療藉心中壓抑已久的渴望,誰知一發不可收拾,于是拋棄所有利益的考慮,遵循內心,好好追逐cljs一番:D
 cljs就是ClojureScript的縮寫,就是讓Clojure代碼transpile為JavaScript代碼然后運行在瀏覽器或其他JSVM上的技術。由于宿主環境的不同,因此只能與宿主環境無關的Clojure代碼可以在JVM和JSVM間共享,并且cljs也未能完全實現clj中的所有語言特性,更何況由于JSVM是單線程因此根本就不需要clj中STM等特性呢……
 transpile為JS的函數式編程那么多(如Elm,PureScript),為什么偏要cljs呢?語法特別吧,有geek的感覺吧,隨心就好:)

 本文將快速介紹cljs的語言基礎,大家可以直接通過clojurescript.net的Web REPL來練練手!

注釋

 首先介紹一下注釋的寫法,后續內容會用到哦!

;    單行注釋
;;   函數單行注釋
;;;  macro或defmulti單行注釋
;;;; 命名空間單行注釋
(comment "
    多行注釋
")

#! shebang相當于;單行注釋
#_ 注釋緊跟其后的表達式, 如: [1 #_2 3] 實際為[1 3],#_(defn test [x] (println x)) 則注釋了成個test函數

數據類型

標量類型

; 空值/空集
nil

; 字符串(String)
"String Data Type"

; 字符(Char)
\a
\newline

; 布爾類型(Boolean),nil隱式類型轉換為false,0和空字符串等均隱式類型轉換為true
true
false

; 長整型(Long)
1

; 浮點型(Float)
1.2

; 整型十六進制
0x0000ff

; 指數表示法
1.2e3

; 鍵(Keyword),以:為首字符,一般用于Map作為key
:i-am-a-key

; Symbol,標識符
i-am-symbol

; Special Form
; 如if, let, do等
(if pred then else?)
(let [a 1] expr1 expr2)
(do expr*)

集合類型

; 映射(Map),鍵值對間的逗號禁用于提高可讀性,實質上可移除掉
{:k1 1, :k2 2}

; 列表(List)
[1 2 3]

; 矢量(Vector)
'(1 2 3)
; 或
(list 1 2 3)

; 集合(Set)
#{1 2 3}

關于命名-Symbol的合法字符集

 在任何Lisp方言中Symbol作為標識符(Identity),凡是標識符均會被限制可使用的字符集范圍。那么合法的symbol需遵守以下規則:

  1. 首字符不能是[0-9:]
  2. 后續字符可為[a-zA-Z0-9*+-_!?|:=<>$&]
  3. 末尾字符不能是:

:為首字符則解釋為Keyword

命名空間

 cljs中每個symbol無論是函數還是綁定,都隸屬于某個具體的命名空間之下,因此在每個.cljs的首行一般為命名空間的聲明。

(ns hello-world.core)

文件與命名空間的關系是一一對應的,上述命名空間對應文件路徑為hello_word/core.cljshello_word/core.cljhello_word/core.cljc
.cljs文件用于存放ClojureScript代碼
.clj文件用于存放Clojure代碼或供JVM編譯器編譯的ClojureScript的Macro代碼
.cljc文件用于存放供CljureScript自舉編譯器編譯的ClojureScript的Macro代碼

引入其他命名空間

 要調用其他命名空間的成員,必須要先將其引入

;;; 命名空間A
(ns a.core)

(defn say1 []
    (println "A1"))
(defn say2 []
    (println "A2"))

;;;; 命名空間B,:require簡單引入
(ns b.core
    (:require a.core))
(a.core/say1) ;-> A1
(a.core/say2) ;-> A2

;;;; 命名空間C,:as別名
(ns b.core
    (:require [a.core :as a]))
(a/say1) ;-> A1
(a/say2) ;-> A2

;;;; 命名空間C,:refer導入symbol
(ns b.core
    (:require [a.core :refer [say1 say2]]))
(say1) ;-> A1
(say2) ;-> A2

綁定和函數

 cljs中默認采用不可變數據結構,因此沒有變量這個概念,取而代之的是"綁定"。

綁定

; 聲明一個全局綁定
(declare x)

; 定義一個沒有初始化值的全局綁定
(def x)

; 定義一個有初始化值的全局綁定
(def x 1)

注意:cljs中的綁定和函數遵循先聲明后使用的規則。

; 編譯時報Use of undeclared Var cljs.user/msg
(defn say []
    (println "say" msg))
(def msg "john")
(say)

; 先聲明則編譯正常
(declare msg)
(defn say []
    (println "say" msg))
(def msg "john")
(say)

函數

函數的一大特點是:一定必然有返回值,并且默認以最后一個表達式的結果作為函數的返回值。

; 定義
(defn 函數名 [參數1 參數2 & 不定數參數列表]
    函數體)

; 示例1
(defn say [a1 a2 & more]
    (println a1)
    (println a2)
    (doseq [a more]
        (print a)))
(say \1 \2 \5 \4 \3) ;輸出 1 2 5 4 3

; 定義帶docstrings的函數
(defn 函數名
    "docstrings"
    [參數1 參數2 & 不定數參數列表]
    函數體)

; 示例2
(defn say
    "輸出一堆參數:D"
    [a1 a2 & more]
    (println a1)
    (println a2)
    (doseq [a more]
        (print a)))

什么是docstrings呢?
docstrings就是Document String,用于描述函數、宏功能。

; 查看綁定或函數的docstrings
(cljs.repl/doc name)

; 示例
(cljs.repl/doc say)
;;輸入如下內容
;; -------------------
;; cljs.user/say
;; ([a1 a2 & more])
;;   輸出一堆參數:D
;;=> nil
; 根據字符串類型的關鍵字,在已加載的命名空間中模糊搜索名稱或docstrings匹配的綁定或函數的docstrings
(cljs.repl/find-doc "keyword")

; 示例
(cljs.repl/find-doc "一堆")
;;輸入如下內容
;; -------------------
;; cljs.user/say
;; ([a1 a2 & more])
;;   輸出一堆參數:D
;;=> nil

題外話!

; 輸出已加載的命名空間下的函數的源碼
; 注意:name必須是classpath下.cljs文件中定義的symbol
(cljs.repl/source name)

; 示例
(cljs.repl/source say)
;;輸入如下內容
;; -------------------
;; (defn say
;;   "輸出一堆參數:D"
;;   [a1 a2 & more]
;;   (println a1)
;;   (println a2)
;;   (doseq [a more]
;;     (print a)))
; 在已加載的ns中通過字符串或正則模糊查找symbols
(cljs.repl/apropos str-or-regex)

; 示例
(cljs.repl/apropos "sa")
(cljs.repl/apropos #"sa.a")
; 查看命名空間下的公開的Var
(cljs.repl/dir ns)

; 示例
(cljs.repl/dir cljs.repl)
; 打印最近或指定的異常對象調用棧信息,最近的異常對象會保存在*e(一個dynamic var)中
(pst)
(pst e)

注意:當我們使用REPL時,會自動引入(require '[cljs.repl :refer [doc find-doc source apropos pst dir]],因此可以直接使用。

關系、邏輯和算數運算函數

 由于cljs采用前綴語法,因此我們熟悉的==!=&&+等均以(= a b)(not= a b)(and 1 2)(+ 1 2)等方式調用。

關系運算函數

; 值等,含值類型轉換,且對于集合、對象而言則會比較所有元素的值
(= a b & more)
; 數字值等
(== a b & more)

; 不等于
(not= a b & more)

; 指針等
(identical? a b)

; 大于、大于等于、小于、小于等于
(> a b)
(>= a b)
(< a b)
(<= a b)
; Surprising!! JS中表示數值范圍只能寫成 1 < x && x < 10,但cljs中可以直接寫成
(< 1 x 10)
; > >= <=都可以這樣哦!

; 比較,若a小于b,則返回-1;等于則返回0;大于則返回1
; 具體實現
; 1. 若a,b實現了IComparable協議,則采用IComparable協議比較
; 2. 若a和b為對象,則采用google.array.defaultCompare
; 3. nil用于小于其他入參
(compare a b)

邏輯運算函數

; 或
(or a & next)
; 與
(and a & next)
; 非
(not a)

 對于orand的行為是和JS下的||&&一致,

  1. 非條件上下文時,or返回值為入參中首個不為nilfalse的參數;而and則是最后一個不為nilfalse的參數。
  2. 條件上下文時,返回會隱式轉換為Boolean類型。

算數運算函數

; 加法,(+)返回0
(+ & more)

; 減法,或取負
(- a & more)

; 乘法, (*)返回1
(*)

; 除法,或取倒數,分母d為0時會返回Infinity
(/ a & more)

; 整除,分母d為0時會返回NaN
(quot n d)

; 自增
(inc n)

; 自減
(dec n)

; 取余,分母d為0時會返回NaN
(rem n d)

; 取模,分母d為0時會返回NaN
(mod n d)

取余和取模的區別是:

/**
 * @description 求模
 * @method mod
 * @public
 * @param {Number} o - 操作數
 * @param {Number} m - 模,取值范圍:除零外的數字(整數、小數、正數和負數)
 * @returns {Number} - 取模結果的符號與模的符號保持一致
 */
var mod = (o/*perand*/, m/*odulus*/) => {
    if (0 == m) throw TypeError('argument modulus must not be zero!')
    return o - m * Math.floor(o/m)
}

/**
 * @description 求余
 * @method rem
 * @public
 * @param {Number} dividend - 除數
 * @param {Number} divisor - 被除數,取值范圍:除零外的數字(整數、小數、正數和負數)
 * @returns {Number} remainder - 余數,符號與除數的符號保持一致
 */
var rem = (dividend, divisor) => {
    if (0 == divisor) throw TypeError('argument divisor must not be zero!')
    return dividend - divisor * Math.trunc(dividend/divisor)
}

 至于次方,開方和對數等則要調用JS中Math所提供的方法了!

; 次方
(js/Math.pow d e)
; 開方
(js/Math.sqrt n)

可以注意到調用JS方法時只需以js/開頭即可,是不是十分方便呢!
根據我的習慣會用**標示次方,于是自定個方法就好

(defn **
    ([d e] (js/Math.pow d e))
    ([d e & more]
        (reduce ** (** d e) more)))

流程控制

; if
(when test
    then)
;示例
(when (= 1 2)
    (println "1 = 2"))

; if...else...
; else?的缺省值為nil
(if test
    then
    else?)
;示例
(if (= 1 2)
    (println "1 = 2")
    (println "1 <> 2"))

; if...elseif..elseif...else
; expr-else的缺省值為nil
(cond
    test1 expr1
    test2 expr2
    :else expr-else)
;示例
(cond
    (= 1 2) (println "1 = 2")
    (= 1 3) (println "1 = 3")
    :else (println "1 <> 2 and 1 <> 3"))

; switch
; e為表達式,而test-constant為字面常量,可以是String、Number、Boolean、Keyword和Symbol甚至是List等集合。e的運算結果若值等test-constant的值(對于集合則深度相等時),那么就以其后對應的result-expr作為case的返回值,若都不匹配則返回default-result-expr的運算值
; 若沒有設置default-result-expr,且匹配失敗時會拋出異常
(case expr
    test-constant1 result-expr
    test-constant2 result-expr
    ......
    default-result-expr)
;示例
(def a 1)
(case a
    1 "result1"
    {:a 2} (println 1))
; -> 返回 result1,且不執行println 1

; for
(loop [i start-value]
    expr
    (when (< i amount)
        (recur (inc i))))
; 示例
(loop [i 0]
    (println i)
    (when (< i 10)
        (recur (inc i))))

; try...catch...finally
(try expr* catch-clause* finally-clause?)
catch-clause => (catch classname name expr*)
finally-clause? => (finally expr*)

; throw,將e-expr運算結果作為異常拋出
(throw e-expr)

進階

與JavaScript互操作(Interop)

cljs最終是運行在JSVM的,所以免不了與JS代碼作互調。

; 調用JS函數,以下兩種形式是等價的。但注意第二種,第一個參數將作為函數的上下文,和python的方法相似。
; 最佳實踐為第一種方式
(js/Math.pow 2 2)
(.pow js/Math 2 2)

; 獲取JS對象屬性值,以下兩種形式是等價的。
; 但注意第一種采用的是字面量指定屬性名,解析時確定
; 第二種采用表達式來指定屬性名,運行時確定
; 兩種方式均可訪問嵌套屬性
(.-body js/document)
(aget js/document "body")
; 示例:訪問嵌套屬性值,若其中某屬性值為nil時直接返回nil,而不是報異常
(.. js/window -document -body -firstChild) ;-> 返回body元素的第一個子元素
(aget js/window "document" "body" "firstChild") ;-> 返回body元素的第一個子元素
(.. js/window -document -body -firstChild1) ;-> 返回nil,而不會報異常
(aget js/window "document" "body" "firstChild1") ;-> 返回nil,而不會報異常
; 有用過Ramda.js的同學看到這個時第一感覺則不就是R.compose(R.view, R.lensPath)的嗎^_^

; 設置JS對象屬性值,以下兩種形式是等價的。注意點和獲取對象屬性是一致的
(set! (.-href js/location) "new href")
(aset! js/location "href" "new href")

; 刪除JS對象屬性值
(js-delete js/location href)

; 創建JS對象,以下兩種形式是等價的
#js {:a 1} ; -> {a: 1}
(js-obj {:a 1}) ; -> {a: 1}

; 創建JS數組,以下兩種形式是等價的
#js [1 2]
(array 1 2)
; 創建指定長度的空數組
(make-array size)
; 淺復制數組
(aclone arr)

; cljs數據類型轉換為JS數據類型
; Map -> Object
(clj->js {:k1 "v1"}) ;-> {k1: "v1"}
; List -> Array
(clj->js '(1 2)) ;-> [1, 2]
; Set -> Array
(clj->js #{1 2}) ;-> [1, 2]
; Vector -> Array
(clj->js [1 2]) ;-> [1, 2]
; Keyword -> String
(clj->js :a) ;-> "a"
; Symbol -> String
(clj-js 'i-am-symbol) ;-> "i-am-symbol"

; JS數據類型轉換為cljs數據類型
; JS的數組轉換為Vector
(js->clj (js/Array. 1 2)) ;-> [1 2]
; JS的對象轉換為Map
(js->clj (clj->js {:a 1})) ;-> {"a" 1}
; JS的對象轉換為Map,將鍵轉換為Keyword類型
(js->clj (clj->js {:a 1}) :keywordize-keys true) ;-> {:a 1}

; 實例化JS實例
; 最佳實踐為第一種方式
(js/Array. 1 2) ;-> [1, 2]
(new js/Array 1 2) ;-> [1, 2]

解構(Destructuring)

 簡單來說就是聲明式萃取集合元素

; 數組1解構
(defn a [[a _ b]]
    (println a b))
(a [1 2 3]) ;-> 1 3

; 數組2解構
(defn b [[a _ b & more]]
    (println a b (first more)))
(a [1 2 3 4 5]) ;-> 1 3 4

; 數組3解構,通過:as獲取完整的數組
(let [[a _ b & more :as orig] [1 2 3 4 5]]
    (println {:a a, :b b, :more more, :orig orig}))
;-> {:a 1, :b 3, :more [4 5], :orig [1 2 3 4 5]}

; 鍵值對1解構
; 通過鍵解構鍵值對,若沒有匹配則返回nil或默認值(通過:or {綁定 默認值}),
(let [{name :name, val :val, prop :prop :or {prop "prop1"}} {:name "name1"}]
    (println name (nil? val) prop)) ;-> "name1 true prop1"

; 鍵值對2解構,通過:as獲取完整的鍵值對
(let [{name :name :as all} {:name "name1", :val "val1"}]
    (println all)) ;-> {:name "name1", :val "val1"}

; 鍵值對3解構,鍵類型為Keyword類型
(let [{:keys [name val]} {:name "name1", :val "val1"}]
    (println name val)) ;-> name1 val1

; 鍵值對4解構,鍵類型為String類型
(let [{:strs [name val]} {"name" "name1", "val" "val1"}]
    (println name val)) ;-> name1 val1

; 鍵值對5解構,鍵類型為Symbol類型
(let [{:syms [name val]} {'name"name1", 'val "val1"}]
    (println name val)) ;-> name1 val1

; 鍵值和數組組合解構
(let [{[a _ b] :name} {:name [1 2 3]}]
    (println a b)) ;-> 1 3

總結

 是不是已經被Clojure的語法深深地吸引呢?是不是對Special Form,Symbol,Namespace等仍有疑問呢?是不是很想知道如何用在項目中呢?先不要急,后面我們會一起好好深入玩耍cljs。不過這之前你會不會發現在clojurescript.net上運行示例代碼居然會報錯呢?問題真心是在clojurescript.net上,下一篇(cljs/run-at (JSVM. :browser) "搭建剛好可用的開發環境!"),我們會先搭建一個剛好可用的開發環境再進一步學習cljs。
尊重原創,轉載請注明來自:http://www.cnblogs.com/fsjohnhuang/p/7040661.html ^_^肥仔John


文章列表




Avast logo

Avast 防毒軟體已檢查此封電子郵件的病毒。
www.avast.com


arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()