又一份 ClojureScript 介绍

2019年12月09日 阅读数:50
这篇文章主要向大家介绍又一份 ClojureScript 介绍,主要内容包括基础应用、实用技巧、原理机制等方面,希望对大家有所帮助。

又一份 ClojureScript 介绍

ClojureScript 是什么样的

ClojureScript 是一门编译到 JavaScript 的 Lisp 方言, 就像 CoffeeScript.
Clojure 是 Lisp 方言, 因此它的语法基于 S-Expression(S 表达式),
"S 表达式"大量使用圆括号好比 (f arg1 arg2) 来控制代码的嵌套结构,
甚至于像是日常的 a + b + c 在 S 表达式当中也编程 (+ a b c).html

这是一种"前缀表达式"的写法, 它很灵活, 能够构造出很是灵活的代码,
好比这样一段代码, 能够完成 10 之内的奇数的平方求和:node

(->> (range 10)
     (filter odd?)
     (map (fn [x] (* x x)))
     (reduce +))

而后你能够按照 Lumo, 保存上面的代码到 app.cljs, 而后运行它:git

npm install -g lumo-cljs
lumo app.cljs

Clojure 为了能更方便, 使用了方括号和花括号做为特殊的语法.
上面的代码当中有个 (fn [x] (* x x)), 其中函数参数就必需要 [x] 写.github

这段代码如何执行

这段代码首先运行生成一个长度为 10 的列表(List):npm

(range 10)
; (0 1 2 3 4 5 6 7 8 9)

而后运行 filter 函数过滤列表, 使用 odd? 来判断是不是奇数:编程

(filter odd? (list 0 1 2 3 4 5 6 7 8 9))
; (1 3 5 7 9)

(fn [x] (* x x)) 是一个匿名函数, 传递给后面的 map 函数运行使用:api

(map (fn [x] (* x x)) (list 1 3 5 7 9))
; (1 9 25 49 81)

最后运行的是 reduce 函数, 经过 + 这个函数将列表里全部的数字相加:浏览器

(reduce + (list 1 9 25 49 81))
; 165

这里能够看到 list 能够表示列表的结构, 而 ->> 会管理后面几段代码的执行顺序.
这里是 ->> 是经过宏(Macro)来完成的, 宏的语法颇有难度, 这里先跳过.bash

ClojureScript 有什么优点

Clojure 自己是一门 Lisp 方言, 突出了不可变数数据和惰性计算等等函数式编程的功能,
ClojureScript 是 Clojure 编译到 JavaScript 的版本, 用来开发网页或者 Node 应用.
跟 JavaScript 相比, Clojure 的设计更加仔细, 并且做为 Lisp 有着强大的表达能力,
同时, 对于不可变数据的思考也让 Clojure 对于并发计算和状态管理有好的改进.
Clojure 做者作过大学老师, 他给人演讲有一种充满智慧的感受, 也是我信任 Clojure 的缘由.数据结构

JavaScript 和 React 当中写网页的时候, 须要 JSX 和 immutable-js,
JSX 表示 Virtual DOM 的代码中间须要特殊处理 if switch 等逻辑,
在 ClojureScript 当中 ifcase 自己就是表达式, 不须要额外处理,
至于不可变数据, ClojureScript 默认的数据已是 immutable data 了, 无需额外引入,
因此 ClojureScript 社区有不少人使用 React, 好比能够用 Reagent 来定义 React 组件:

(defn simple-component []
  [:div
   [:p "I am a component!"]
   [:p.someclass
    "I have " [:strong "bold"]
    [:span {:style {:color "red"}} " and red "] "text."]])

当你熟练 ClojureScript 的时候, 你能够变得比 JavaScript 更加灵活和自如.
经过高阶函数和宏, 能够构造出很是精简的代码来完成一样的任务.

用什么软件执行和编译

ClojureScript 是运行在 JavaScript 环境当中的, 好比浏览器或者 Node.js ,
Lumo 是一个基于 V8 和 Node.js 的 ClojureScript 运行环境, 能够用 npm 安装:

npm install -g lumo-cljs

启动 Lumo 能够获得一个 REPL 环境, 跟 Node.js 的 REPL 很像:

$ lumo
Lumo 1.8.0
ClojureScript 1.9.946
Node.js v9.2.0
 Docs: (doc function-name-here)
       (find-doc "part-of-name-here")
 Source: (source function-name-here)
 Exit: Control+D or :cljs/quit or exit

cljs.user=> (range 10)
(0 1 2 3 4 5 6 7 8 9)
cljs.user=> (filter odd? (list 0 1 2 3 4 5 6 7 8 9))
(1 3 5 7 9)
cljs.user=>

另外一个工具是 shadow-cljs, 更适合编译代码, 像 Webpack. 而后也用 npm 能够安装.
Lumo 适合用来运行 REPL 和代码片断, 而 shadow-cljs 适合作项目开发和编译.
注意对于 shadow-cljs, 你仍是要在安装 Java 给它后台调用的.

这篇文章里默认操做系统是 macOS 或 Linux. 在 Windows 可能要注意其余问题.

用 ClojureScript 写脚本

ClojureScript 当中基础数据类型的跟 JavaScript 类似, 有字符串, 数字, 布尔值,
另外有个 Keyword(关键字)类型, 是一种简化的字符串, 经常使用在"键值对"的"键"使用.
作元编程时候还会遇到 Symbol(符号)类型, 不过如今还用不到, 不用管它.

对于长一点的, 建议把代码写在一个 app.cljs 文件里:

(println "Hello ClojureScript!")

而后经过 Lumo 执行这个文件:

$ lumo app.cljs
Hello ClojureScript!

Lumo 是基于 Node.js 实现的, 因此你能够再里面使用 Node.js API.
不过要在 ClojureScript 里调用, 须要用一些特殊的语法,
好比 JavaScript 对象都须要用 js/console 这种加 js/ 前缀的代码来写, 而后写成这样 :

(.log js/console "a message!")
; console.log("a message!")

(.log js/console (js/require "path"))
; console.log(require"("path))

上面的代码会打印出数据. 要使用构造器或者调用方法须要一些其余的语法,

(println (new js/Date))
; #inst "2018-04-15T08:58:44.338-00:00"

(println (.now js/Date))
; 1523782724340

引用 npm 模块能够借助 require 函数, 在 ClojureScript 里写成 js/require:

(def fs (js/require "fs"))

(println (.readdirSync fs "./"))
; #js ["app.cljs" "build.cljs" "out" "src"]

另外一种写法是将引用的模块写在 ns 的定义当中, 而后经过 fs/readdirSync 这个写法调用:

(ns app
  ; 使用 :as 关键字时, "fs" 模块会被引入, 生成 `fs` 这个命名空间
  (:require ["fs" :as fs]))

; 由于 `fs` 是命空间, 因此这个地方用 `fs/` 的写法了
(println (fs/readdirSync "./"))
; #js [app.cljs build.cljs out src]

对照上面调用 Node.js API 的方法, 读取文件也是很是容易的:

(ns app (:require ["fs" :as fs]))

(println (fs/readFileSync "app.cljs"))

Clojure 当中提供了一些操做字符串的函数, 可是更多函数写在 clojure.string 这个命名空间之下:

(ns app (:require [clojure.string :as string]))

(println (pr-str (str "12" "34")))
; "1234"

(println (pr-str (subs "123455" 2 3)))
; "3"

(println (pr-str (string/split "12345" "3")))
; ["12" "45"]

数据结构和抽象

Clojure 是一门函数式语言, 对于循环的设计有些特别, 须要写成尾递归的形式.
Clojure 须要借助 recur 这个关键字来控制尾递归, 好比这个函数打印 0 到 9 的数字:

(defn f1 [x]
      (if (< x 10)
          (do (println x)
              (recur (+ x 1))))) ; `recur` 会再调用 `f1`, 参数就是 `x+1` 了

(f1 0)

上面的尾递归能够用 loop 简写, 在 [x 0] 指定 x 的初始值是 0:

(loop [x 0]
  (if (< x 10)
    (do (println x)
        (recur (+ x 1)))))

这里的 loop 会先设置 n 是 0, 到了 (recur (inc n)) 的地方这个 n 会加上 1.
这样就模拟了一个 while 循环的语法. Clojure 里要把变化的数据经过参数传递.
这是由于函数式编程当中比较排斥可变的数据, 因此用这种方式更严格地限制了数据的修改.

你也能够用 when 做为只执行一个分支的 if 的简写, 那样就不用 do 包裹多个表达式了:

(loop [x 0]
      (when (< x 10)
            (println x)
            (recur (+ x 1))))

Clojure 里经常使用的数据结构有:

  • List(列表), 或者说成链表, 好比 '(1 2 3 4), 从头部操做, 可是随机后面的节点会很慢
  • Vector(向量), 好比 [1 2 3 4], 这个就能很快得进行随机读写了, 不过适合从尾部读写
  • HashMap(哈希表), 好比 {:a 1, :b 2}

跟 JavaScript 之类的语言不同是, Clojure 里的数据是不可变的,
好比 conj 是个往向量的尾部添加数据的函数, 在 a 的基础上增长数据,
从这个例子你能够看到 a 在操做以后是不变的, 要从 b 才能拿到改变的数据:

cljs.user=> (def a [1 2 3 4])
#'cljs.user/a
cljs.user=> (def b (conj a 6))
#'cljs.user/b
cljs.user=> a
[1 2 3 4]
cljs.user=> b
[1 2 3 4 6]

这个就是不可变数据不同的地方了, 这个是函数式编程很须要的一个功能.
更多的操做数据的函数你能够在 http://cljs.info/cheatsheet/ 找到.

其余

Clojure 还有不少有意思的功能. 后面的文章会再讲, 感兴趣能够找咱们问:


另外感谢一些帮我 review 过草稿的同窗, 我后续还会找人添麻烦, 名字我匿了.