(cljs/run-at (->JSVM :browser) "語言基礎")
前言
兩年多前知道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.cljs
、hello_word/core.clj
或hello_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)
對於or
和and
的行為是和JS下的||
和&&
一致,
1. 非條件上下文時,or
返回值為入參中首個不為nil
或false
的引數;而and
則是最後一個不為nil
或false
的引數。
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
相關文章
- (cljs/run-at (JSVM. :browser) "簡單型別可不簡單啊~")JS型別
- (cljs/run-at (JSVM. :browser) "名稱空間就這麼簡單")JS
- (cljs/run-at (JSVM. :browser) "搭建剛好可用的開發環境!")JS開發環境
- (cljs/run-at (JSVM. :all) "細說函式")JS函式
- (cljs/run-at (JSVM. :all) "Metadata就這樣哦")JS
- (cljs/run-at (JSVM. :all) "一起實現柯里化")JS
- (cljs/run-at (JSVM. :all) "一次說白DataType、Record和Protocol")JSProtocol
- dart語言基礎Dart
- C語言基礎C語言
- Swift 語言基礎Swift
- Go語言基礎Go
- SQL語言基礎(資料控制語言)SQL
- Julia語言程式基礎
- Go語言基礎-序言Go
- 【Go語言基礎】sliceGo
- 【01】C語言基礎C語言
- Java語言基礎(一)Java
- c語言的基礎C語言
- e語言基礎01
- D程式語言基礎篇
- 組合語言-基礎功能組合語言
- [06 Go語言基礎-包]Go
- python程式語言基礎Python
- c語言基礎知識C語言
- C語言入門基礎C語言
- SQL語言基礎(函式)SQL函式
- plsql開發語言基礎SQL
- Oracle PL/SQL語言基礎OracleSQL
- java語言基礎學習Java
- c語言基礎學習C語言
- C語言基礎-指標C語言指標
- C語言基礎函式C語言函式
- Go語言基礎語法總結Go
- SQL語言基礎(SELECT語句)SQL
- Dart 語言基礎入門篇Dart
- 組合語言-基礎知識組合語言
- Gradle 之語言基礎 GroovyGradle
- c語言基礎知識3C語言