两年多前知道 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}
在任何 Lisp 方言中 Symbol 作为标识符 (Identity),凡是标识符均会被限制可使用的字符集范围。那么合法的 symbol 需遵守以下规则:
- [0-9:]
- [a-zA-Z0-9*+-_!?|:=<>$&]
- :
以
为首字符则解释为 Keyword
- :
cljs 中每个 symbol 无论是函数还是绑定,都隶属于某个具体的命名空间之下,因此在每个
的首行一般为命名空间的声明。
- .cljs
- (ns hello-world.core)
文件与命名空间的关系是一一对应的,上述命名空间对应文件路径为
、
- hello_word/core.cljs
或
- hello_word/core.clj
。
- hello_word/core.cljc
文件用于存放 ClojureScript 代码
- .cljs
文件用于存放 Clojure 代码或供 JVM 编译器编译的 ClojureScript 的 Macro 代码
- .clj
文件用于存放供 CljureScript 自举编译器编译的 ClojureScript 的 Macro 代码
- .cljc
要调用其他命名空间的成员,必须要先将其引入
- ;;; 命名空间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)
- ; 比较,若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
的行为是和 JS 下的
- and
和
- ||
一致,
- &&
返回值为入参中首个不为
- or
或
- nil
的参数;而
- false
则是最后一个不为
- and
或
- nil
的参数。
- false
类型。
- 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)
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]
简单来说就是声明式萃取集合元素
- ;数组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
来源: http://www.cnblogs.com/fsjohnhuang/p/7040661.html