(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需遵守以下规则:
- 首字符不能是
[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
。
.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下的||
和&&
一致,
- 非条件上下文时,
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)
进阶
与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) "语言基础")的更多相关文章
- (cljs/run-at (JSVM. :browser) "搭建刚好可用的开发环境!")
前言 书接上一回,在了解cljs基本语法后并在clojurescript.net的奇特错误提示后,我们必须痛定思痛地搭建一个本地的开发环境,以便后续深入地学习cljs. 现有的构建工具 由于浏览器 ...
- (cljs/run-at (JSVM. :browser) "命名空间就这么简单")
前言 一个cljs文件定义一个命名空间,通过命名空间可以有效组织代码,这是构建大型系统必备的基础设施.本篇我们就深入理解cljs中的命名空间吧! 好习惯从"头"开始 每个cljs ...
- php面试题之三——PHP语言基础(基础部分)
三.PHP语言基础 1. strlen( )与 mb_strlen( )的作用分别是什么(新浪网技术部) strlen和mb_strlen都是用于获取字符串长度. strlen只针对单字节编码字符,也 ...
- Object Pascal 语言基础
Delphi 是以Object Pascal 语言为基础的可视化开发工具,所以要学好Delphi,首先要掌握的就是Object Pascal 语言.Object Pascal语言是Pascal之父在1 ...
- Golang 入门系列(三)Go语言基础知识汇总
前面已经了 Go 环境的配置和初学Go时,容易遇到的坑,大家可以请查看前面的文章 https://www.cnblogs.com/zhangweizhong/category/1275863.html ...
- Go语言基础(一)
Go语言基础(一) 国庆体验一下大名鼎鼎的Go语言,IDE使用IEDA+Go插件,边敲代码边体会,感觉Go语言好酷 一.Hello World 和Java类似,go文件需要一个package包含,代码 ...
- 20165223 学习基础和C语言基础调查
一.学习基础 1. 我所擅长的技能 从小我就对新鲜事物抱有浓厚的兴趣,因此多年来培养了许多爱好,对感兴趣的诸如绘画方面的国画.油画.素描.漫画等:音乐方面的钢琴.吉他.架子鼓等:运动方面的滑板.溜冰. ...
- D16——C语言基础学PYTHON
C语言基础学习PYTHON——基础学习D16 20180927内容纲要: 1.JavaScript介绍 2.JavaScript功能介绍 3.JavaScript变量 4.Dom操作 a.获取标签 b ...
- D14——C语言基础学PYTHON
C语言基础学习PYTHON——基础学习D14 20180919内容纲要: 1.html认识 2.常用标签 3.京东html 4.小结 5.练习(简易淘宝html) 1.html初识(HyperText ...
随机推荐
- ASP.NET Core开发之HttpContext
ASP.NET Core中的HttpContext开发,在ASP.NET开发中我们总是会经常用到HttpContext. 那么在ASP.NET Core中要如何使用HttpContext呢,下面就来具 ...
- 配置web.xml和glassfish容器实现javaEE表单验证
web.xml配置: <!-- 声明用于安全约束的角色 --> <security-role> <role-name>ReimUser</role-name& ...
- static class - 静态类
通常一个普通类不允许声明为静态的,只有一个内部类才可以.这时这个声明为静态的内部类可以直接作为一个普通类来使用,而不需实例一个外部类. 如下代码所示: 1 public class StaticCls ...
- tpcc-mysql安装
1.因为我的虚拟机是centos 7 min版本,所以先得安装gcc gcc++: http://mirror.centos.org/centos/7/os/x86_64/Packages/ rp ...
- 在Caffe上运行Cifar10示例
准备数据集 在终端上运行以下指令: cd caffe/data/cifar10 ./get_cifar10.sh cd caffe/examples/cifar10 ./create_cifar10. ...
- jeesz分布式架构集成阿里云oss存储
1. 服务接口定义 /** * 文件上传 1:头像 2:显示图片 3:个人封面 :4:基础图片 * @param request * @param response * @param ...
- Item 27: 明白什么时候选择重载,什么时候选择universal引用
本文翻译自<effective modern C++>,由于水平有限,故无法保证翻译完全正确,欢迎指出错误.谢谢! 博客已经迁移到这里啦 Item 26已经解释了,不管是对全局函数还是成员 ...
- 4.从AbstractQueuedSynchronizer(AQS)说起(3)——AQS结语
前两节的内容<2.从AbstractQueuedSynchronizer(AQS)说起(1)——独占模式的锁获取与释放> .<3.从AbstractQueuedSynchronize ...
- java中为什么实体类需要实现序列化
当客户端访问某个能开启会话功能的资源,web服务器就会创建一个HTTPSession对象,每个HTTPSession对象都会占用一定的内存,如果在同一个时间段内访问的用户太多,就会消耗大量的服务器内存 ...
- SmartCoder每日站立会议09
站立会议内容 今天约了在一起编程,详细确定各个页面以及消息的添加.发送等一些小的细节. 1.站立会议照片: 2.任务展板 2.燃尽图