当前位置

网站首页> 程序设计 > 开源项目 > 程序开发 > 浏览文章

[译] ClojureScript 中的 JavaScript 互操作 - 题叶, JiyinYiyong

作者:小梦 来源: 网络 时间: 2024-02-15 阅读:

机翻为主, 原文: ClojureScript: JavaScript Interop
http://www.spacjer.com/blog/2014/09/12/clojurescript-javascript-interop/

(原文更新于 15th of March 2015)

正如我在这个博客上提到过,我在持续不断学习的 Clojure(和 ClojureScript)。为了更好地理解语言,我已经写了小型 Web 应用程序。为了好玩,我决定,我所有的前端代码将被写入 ClojureScript。因为我需要使用外部JavaScript API(Bing 地图 AJAX 控件),我写了相当多的 JavaScript 的互操作码 -- 对我来说语法并不明显,我找不到有所有这些信息的地方,所以我写了这篇文章。请注意,这是一个相当长的帖子!

JavaScript 例子

为了更容易理解所有的例子可以定义简单的 JavaScript 代码:

//global variableglobalName = "JavaScript Interop";globalArray = globalArray = [1, 2, false, ["a", "b", "c"]];globalObject = {  a: 1,  b: 2,  c: [10, 11, 12],  d: "some text"};//global functionwindow.hello = function() {  alert("hello!");}//global functionwindow.helloAgain = function(name) {  alert(name);}//a JS typeMyType = 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 对象)。

(def text js/globalName)

JS 输出:

namespace.text = globalName;

创建对象

ClojureScript 中可以通过在构造函数的结尾添加 . 创建 JavaScript 对象:

(def t1 (js/MyType.))

JS 输出:

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 要做到这一点,可以使用 asetset! 函数:

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";

Array

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.invokeanalyzer.clj:1444 cljs.analyzer/analyze-seqanalyzer.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.invokeanalyzer.clj:1444 cljs.analyzer/analyze-seqanalyzer.clj:1532 cljs.analyzer/analyze[fn]analyzer.clj:1525 cljs.analyzer/analyzeanalyzer.clj:1520 cljs.analyzer/analyze compiler.clj:908 cljs.compiler/compile-file*compiler.clj:1022 cljs.compiler/compile-file

创建 JavaScript 对象

有许多情况下,我们需要从 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.PersistentHashSetcljs.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 对象

有些时候,我们需要转换的 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 函数

值得注意的是,在从 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}}]}

正如你可以看到上面定义了两个构建:devrelease。请注意 :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);

请注意,所有的破折号,取而代之的是下划线。

使用外部 JavaScript 库

: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 的优秀文章。

像往常一样,我赞赏任何评论。

网友最爱