博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
(cljs/run-at (->JSVM :browser) "语言基础")
阅读量:7112 次
发布时间:2019-06-28

本文共 9418 字,大约阅读时间需要 31 分钟。

前言

 两年多前知道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和空字符串等均隐式类型转换为truetruefalse; 长整型(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); 比较,若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。

尊重原创,转载请注明来自: ^_^肥仔John

你可能感兴趣的文章
我的友情链接
查看>>
PHP导出MySQL数据到Excel
查看>>
Javascript的console.log()用法
查看>>
小程序里json字符串转json对象需注意的地方
查看>>
struts过滤器和拦截器的区别
查看>>
runtime 的常用姿势
查看>>
Unix编程艺术阅读笔记
查看>>
创建git库
查看>>
[译] 将第三方动画库集成到项目中 — 第 1 部分
查看>>
JavaScript 小数取整的函数
查看>>
小程序flex-direction
查看>>
编程基本功(一)
查看>>
迭代器随笔
查看>>
flex布局居中无效果注意是否设置了宽度
查看>>
Bootstrap学习笔记系列5------Bootstrap图片显示
查看>>
CentOS服务器下对mysql的优化
查看>>
linux内核模块开发
查看>>
android 小结
查看>>
【转】Android 基于Socket的聊天室
查看>>
小记录
查看>>