zoukankan      html  css  js  c++  java
  • (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
    
    ewline
    
    ; 布尔类型(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

  • 相关阅读:
    jquery实现选项卡(两句即可实现)
    常用特效积累
    jquery学习笔记
    idong常用js总结
    织梦添加幻灯片的方法
    LeetCode "Copy List with Random Pointer"
    LeetCode "Remove Nth Node From End of List"
    LeetCode "Sqrt(x)"
    LeetCode "Construct Binary Tree from Inorder and Postorder Traversal"
    LeetCode "Construct Binary Tree from Preorder and Inorder Traversal"
  • 原文地址:https://www.cnblogs.com/fsjohnhuang/p/7040661.html
Copyright © 2011-2022 走看看