机翻为主, 原文: ClojureScript: JavaScript Interop
http://www.spacjer.com/blog/2014/09/12/clojurescript-javascript-interop/javascript
(原文更新于 15th of March 2015)html
正如我在这个博客上提到过,我在持续不断学习的 Clojure(和 ClojureScript)。为了更好地理解语言,我已经写了小型 Web 应用程序。为了好玩,我决定,我全部的前端代码将被写入 ClojureScript。由于我须要使用外部JavaScript API(Bing 地图 AJAX 控件),我写了至关多的 JavaScript 的互操做码 -- 对我来讲语法并不明显,我找不到有全部这些信息的地方,因此我写了这篇文章。请注意,这是一个至关长的帖子!前端
为了更容易理解全部的例子能够定义简单的 JavaScript 代码:java
//global variable globalName = "JavaScript Interop"; globalArray = globalArray = [1, 2, false, ["a", "b", "c"]]; globalObject = { a: 1, b: 2, c: [10, 11, 12], d: "some text" }; //global function window.hello = function() { alert("hello!"); } //global function window.helloAgain = function(name) { alert(name); } //a JS type MyType = function() { this.name = "MyType"; } MyComplexType = function(name) { this.name = name; } MyComplexType.prototype.hello = function() { alert(this.name); } MyComplexType.prototype.helloFrom = function(userName) { alert("Hello from " + userName); }
ClojureScript 定义了特殊的 js
命名空间容许访问 JavaScript 类型/函数/方法/全局对象(即浏览器 window
对象)。node
(def text js/globalName)
JS 输出:git
namespace.text = globalName;
ClojureScript 中能够经过在构造函数的结尾添加 .
建立 JavaScript 对象:github
(def t1 (js/MyType.))
JS 输出:api
namespace.t1 = new MyType;
(注:起初我觉得,这产生的 JS 代码是由于缺乏括号错了,但它其实是有效的 - 若是构造函数没有参数,那么括号可省略)数组
还有建立对象的不一样的方式,使用 new
函数(JS 构造函数的名称应该是没有点号):浏览器
(def my-type (new js/MyComplexType "Bob"))
JS 输出:
namespace.my_type = new MyComplexType("Bob");
要调用 JavaScript 方法,咱们须要方法名以前加上 .
(点号):
(.hello js/window)
JS 输出:
window.hello();
去掉语法糖就是:
(. js/window (hello))
将参数传递给咱们的函数:
(.helloAgain js/window "John")
JS 输出:
window.helloAgain("John");
或者:
(. js/window (helloAgain "John"))
一样的事情能够经过建立对象来完成:
(def my-type (js/MyComplexType. "Bob")) (.hello my-type)
JS 输出:
namespace.my_type = new MyComplexType("Bob"); namespace.my_type.hello();
ClojureScript 提供一些方法 JavaScript 操做属性。最简单的一种是使用 .-
属性访问语法:
(def my-type (js/MyType.)) (def name (.-name my-type))
JS 输出:
namespace.my_type = new MyType; namespace.name = namespace.my_type.name;
相似的事情能够经过 aget
函数,它接受对象和属性的名称(字符串)做为参数来完成:
(def name (aget my-type "name"))
JS 输出:
namespace.name = namespace.my_type["name"];
aget
也容许访问嵌套的属性:
(aget js/object "prop1" "prop2" "prop3")
JS 输出:
object["prop1"]["prop2"]["prop3"];
一样的事情(生成的代码是不一样的)能够作到经过使用 ..
语法完成:
(.. js/object -prop1 -prop2 -prop3)
JS 输出:
object.prop1.prop2.prop3;
您还能够设置一个属性的值,ClojureScript 要作到这一点,可使用 aset
或 set!
函数:
该 aset
函数将属性做为一个字符串的名字:
(def my-type (js/MyType.)) (aset my-type "name" "Bob")
JS 输出:
namespace.my_type["name"] = "Bob";
而 set!
须要一个属性访问:
(set! (.-name my-type) "Andy")
JS 输出:
namespace.my_type.name = "Andy";
aget
函数也可用于访问 JavaScript 数组元素:
(aget js/globalArray 1)
JS 输出:
globalArray[1];
或者,若是你想得到嵌套的元素,您能够以这种方式使用它:
(aget js/globalArray 3 1)
JS 输出:
globalArray[3][1];
这个主题对我来讲有点混乱。在个人项目,我想翻译这样的代码:
var map = new Microsoft.Maps.Map();
到 ClojureScript。正如你所看到的 Map
函数在嵌套的做用域中。访问嵌套属性的惯用方法是使用 ..
或 aget
函数,可是这不能用于构造函数来完成。在这种状况下,咱们须要用点号(即便它不是地道的 Clojure 的代码):
(def m2 (js/Microsoft.Maps.Themes.BingTheme.))
或使用 new
函数:
(def m1 (new js/Microsoft.Maps.Themes.BingTheme))
若是咱们这样写这个表达式:
(def m3 (new (.. js/Microsoft -Maps -Themes -BingTheme)))
咱们将获得一个异常:
First arg to new must be a symbol at line core.clj:4403 clojure.core/ex-info analyzer.clj:268 cljs.analyzer/error analyzer.clj:265 cljs.analyzer/error analyzer.clj:908 cljs.analyzer/eval1316[fn] MultiFn.java:241 clojure.lang.MultiFn.invoke analyzer.clj:1444 cljs.analyzer/analyze-seq analyzer.clj:1532 cljs.analyzer/analyze[fn] analyzer.clj:1525 cljs.analyzer/analyze analyzer.clj:609 cljs.analyzer/eval1188[fn] analyzer.clj:608 cljs.analyzer/eval1188[fn] MultiFn.java:241 clojure.lang.MultiFn.invoke analyzer.clj:1444 cljs.analyzer/analyze-seq analyzer.clj:1532 cljs.analyzer/analyze[fn] analyzer.clj:1525 cljs.analyzer/analyze analyzer.clj:1520 cljs.analyzer/analyze compiler.clj:908 cljs.compiler/compile-file* compiler.clj:1022 cljs.compiler/compile-file
有许多状况下,咱们须要从 ClojureScript 的方法传递 JavaScript 对象。通常 ClojureScript 能处理本身的数据结构(不可变的,持久的 vector,Map,set 等)转化为纯的 JS 对象。有这样作的几种方法。
若是咱们要键值对列表中建立一个简单的 JavaScript 对象, 咱们能够用 js-obj
这个宏:
(def my-object (js-obj "a" 1 "b" true "c" nil))
JS 输出:
namespace.my_object_4 = (function (){var obj6284 = {"a":(1),"b":true,"c":null};return obj6284;
须要注意的是 js-obj
强迫你使用字符串做为键和基础数据的字面量(字符串,数字,布尔值)的值。ClojureScript 数据结构不会改变,因此这样的:
(def js-object (js-obj :a 1 :b [1 2 3] :c #{"d" true :e nil}))
会建立这样的 JavaScript 对象:
{ ":c" cljs.core.PersistentHashSet, ":b" cljs.core.PersistentVector, ":a" 1 }
你能够看到有使用的内部类型,如:
cljs.core.PersistentHashSet cljs.core.PersistentVector
ClojureScript 关键字改成字符串前面加上冒号。
为了解决这个问题,咱们可使用 clj-> js
函数:“递归转换 ClojureScript 值到 JavaScript。Set / Vector / List 成为 Array,Keyword 和 Symbol 成为字符串,Map 成为 Object“。
{ "a": 1, "b": [1, 2, 3], "c": [null, "d", "e", true] }
也有生产的 JavaScript 对象的另外一种方式 -- 咱们可使用 #js
reader 语法:
(def js-object #js {:a 1 :b 2})
生成的代码:
namespace.core.js_object = {"b": (2), "a": (1)};
使用 #js
时,你须要谨慎,由于这个语法也不会改变内部结构(这是浅层的):
(def js-object #js {:a 1 :b [1 2 3] :c {"d" true :e nil}})
会建立这样的对象:
{ "c": cljs.core.PersistentArrayMap, "b": cljs.core.PersistentVector, "a": 1 }
要解决这个问题,你须要在每一个 ClojureScript 结构前添加 #js
:
(def js-object #js {:a 1 :b #js [1 2 3] :c #js ["d" true :e nil]})
JavaScript 对象:
{ "c": { "e": null, "d": true }, "b": [1, 2, 3 ], "a": 1 }
有些时候,咱们须要转换的 JavaScript 对象或数组到 ClojureScript的数据结构的状况。咱们能够经过使用 js->clj
函数作到这一点:
“递归转变 JavaScript 数组到 ClojureScript Vector,和 JavaScript 对象到ClojureScript Map。经过选项 :keywordize-key true
将对象字段从转换
字符串的 Keyword。
(def my-array (js->clj (.-globalArray js/window))) (def first-item (get my-array 0)) ;; 1 (def my-obj (js->clj (.-globalObject js/window))) (def a (get my-obj "a")) ;; 1
做为函数的文档说明的,可使用 :keywordize-keys true
转换建立好的 Map 的关键字字符串到 keyword:
(def my-obj-2 (js->clj (.-globalObject js/window) :keywordize-keys true)) (def a-2 (:a my-obj-2)) ;; 1
若是使用 JavaScript 的全部其余方法都失败,有一个 js*
接收一个字符串做为参数,并原样返回做为 JavaScript 代码:
(js* "alert('my special JS code')") ;; JS output: alert('my special JS code');
值得注意的是,在从 ClojureScript 生成 JavaScript 代码的确切形式取决于编译器设置。这些设置能够在 Leiningen project.clj
文件中定义:
project.clj
文件的相关部分:
:cljsbuild { :builds [{:id "dev" :source-paths ["src"] :compiler { :main your-namespace.core :output-to "out/your-namespace.js" :output-dir "out" :optimizations :none :cache-analysis true :source-map true}} {:id "release" :source-paths ["src"] :compiler { :main blog-sc-testing.core :output-to "out-adv/your-namespace.min.js" :output-dir "out-adv" :optimizations :advanced :pretty-print false}}]}
正如你能够看到上面定义了两个构建:dev
和 release
。请注意 :optimizations
参数 -- 使用 :advanced
的代码将被压缩(未使用的代码被删除),并改名(使用较短的名称)。
例如,该 ClojureScript 代码:
(defn add-numbers [a b] (+ a b))
在:advanced
模式将被编译到这样的 JavaScript 代码 :
function yg(a,b){return a+b}
函数名称是彻底“随机”,因此你不能从 JavaScript 文件中使用它。为了可以使用ClojureScript 函数定义(其原始名称),你应该加上标志 :export
做为 metadata:
(defn ^:export add-numbers [a b] (+ a b))
这个 :export
关键字告诉编译器给定函数名导出到外部。(这是经过 Google Closure Compiler 的 exportSymbol
函数来完成 - 但我不会详谈细节)。而后在你的外部 JavaScript 代码,你能够调用这个函数:
your_namespace.core.add_numbers(1,2);
请注意,全部的破折号,取而代之的是下划线。
:advanced
模式也影响到外部库的调用,由于全部的函数/方法的名称更改成最小的形式。让咱们来 ClojureScript 代码,从 Chart
对象调用PolarArea
函数:
(defn ^:export creat-chart [] (let [ch (js/Chart.)] (. ch (PolarArea []))))
编译完成后,该代码将相似于这样:
function(){return(new Chart).Bc(zc)}
正如你所看到的,PolarArea
方法改成 Bc
,这固然会致使运行错误。为了防止这种状况,咱们须要告诉编译器哪些名字不该该被改变。这些名称应在外部 JavaScript 文件中定义(即 externs.js
)并提供给编译器。在咱们的例子中 externs.js
文件看起来应该像这样的:
var Chart = {}; Chart.PolarArea = function() {};
关于这个文件, 编译器应该经过project.clj
中的 :externs
设置被告知 :
{:id "release" :source-paths ["src"] :compiler { :main blog-sc-testing.core :output-to "out-adv/your-namespace.min.js" :output-dir "out-adv" :optimizations :advanced :externs ["externs.js"] :pretty-print false}}
若是咱们作全部这些事情,建立 JavaScript 代码将包含 PolarArea
函数的正确调用:
function(){return(new Chart).PolarArea(Ec)}
要得到有关 ClojureScript 使用外部 JavaScript 库的更多详细信息,关于这一点我建议你阅读 Luke VanderHart 的优秀文章。