機翻爲主, 原文: 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 的優秀文章。