August 10, 2020
By: Kevin

clojurescript和javascript交互 2020-08-10更新

  1. 命名空间
  2. 属性访问
  3. 函数调用
  4. 赋值
  5. 几个陷阱
  6. 语法糖
  7. #js指示符
  8. 数据结构转化
  9. 高级编译选项
  10. js object的读写
  11. js->clj 的性能
  12. Promise await/async
  13. new

ClojureScript很棒,但也免不了和JavaScript打交道,比如引入npm的package,或者直接使用浏览器的原生API。

本文力求成为一个完善的JS和CLJS的交互手册,覆盖各种可能的情形,会不断保持更新.

此外,CLJS和JS的交互和CLJ和Java的交互大同小异,特别是属性访问,函数调用部分。

命名空间

JavaScript本身是没有namespace的,所以很容易造成冲突,JS本身为了避免冲突,一般会把个JS对象当成Module。其他对象再塞到这个Module里面。

而ClojureScript是有namespace的。为了和JS互动,在CLJS中,虚拟了一个叫js的namespace,来代表js中的所有的全局scope。

[js/Date js/window js/alert ]

属性访问

.是clj/cljs的一个Special Form,用来表示函数调用/属性访问.

(. js/document -title)

为了避免(. (. (. js/document -location) -href) -length), 深度嵌套的可以用宏..

(.. js/document -location -href -length)

因为他等价于:

document.location.href.length

看看这个宏展开

(macroexpand '(.. js/document -location -href -length))

函数调用

(. js/document hasFocus)
document.hasFocus()

如果加了-就是访问属性,而不是函数调用

(. js/document -hasFocus)
document.hasFocus

如果调用的对象并非函数,会报错的哦

(. js/document title)
document.title()

带参数的函数:

(. js/document getElementsByTagName "html")
document.getElementsByTagName("html")

嵌套函数:

document.foo = (x, y) => { return { bar: function(a) { return [a, x, y] }}}
document.foo(1, 2).bar(3) 
(. (. js/document foo 1 2) bar 3) ;; => [3 1 2]

赋值

以下代码执行会...比较麻烦,自己可以尝试……_

(set! (.. js/window -location -search) "foo=bar")
window.location.search = "foo=bar"

几个陷阱

属性是函数的时候,直接用cljs的方式去调用是OK的:

document.foo = function(a) { return a;}
((. js/document -foo) 1)

但是,要注意this的绑定(一向以为搞懂了this就搞懂了JS😂)

下面执行因为没有正确绑定this,所以会报错

let f = document.getElementsByTagName
f("html")
let f = document.getElementsByTagName
f.call(document, "html")

或者手工bind一下:

let f = document.getElementsByTagName.bind(document)
f("html")

对应于cljs

call的版本:

(. (. js/document -getElementsByTagName) call js/document "html")

bind的版本:

((. (. js/document -getElementsByTagName) bind js/document) "html")

都很奇怪,但是这个是necessary evil,如果谁有更好的方法请告诉我。

语法糖

(.-title js/document)

扒开糖纸:

(macroexpand '(.-title js/document))
(.hasFocus js/document)

扒开糖纸:

(macroexpand '(.hasFocus js/document))

看个复杂的:

(.. (.item (.call (.-getElementsByTagName js/document) js/document "html") 0) -children -length)

#js指示符

#js 是一个reader(reader是REPL中的R,这另一个话题了)hint,告诉后面的结构是一段原生js。

#js在编译期间执行,效率很高,推荐使用,虽然不是递归转化。

(let [my-obj #js {"a" 1 "b" 2}]
  my-obj)

let my_obj = {"a": 1, "b": 2}
my_obj
(let [my-arr #js ["a" "b" 2]]
  my-arr)

let my_arr = ["a", "b", 2]
my_arr

注意:#js 不支持嵌套

(let [my-obj #js {"a" 1 "b" {"c" 2 "d" 3}}]
    my-obj)

应该定义为:

(let [my-obj #js {"a" 1 "b" #js {"c" 2 "d" 3}}]
  my-obj)

注意:cljs里嵌入js的数据结构不是无缝的,因为

  • js没有keyrwrod
  • js只能拿string来做key

有时候会碰到人:

(->>  #js {:a 1 :b 2}
 (.stringify js/JSON)
 (#(clojure.string/replace % #"\"" "")))

有时候会遇到鬼

(->> #js [:a 'b "c" 3]
  (.stringify js/JSON )
   (#(clojure.string/replace % #"\"" "")))

数据结构转化

  • js->clj: 递归的把js数据结构转化为clj的数据结构
  • clj->js: 和上面相反
(clj->js {:a 1 'b 2 "c" {:d 3}})
(js->clj #js {:a 1, :b 2, :c #js {:d 3}})

clj->js支持参数,可以修改下key

(clj->js {:a 1 'b 2 "c" {:d 3}} :keyword-fn (fn [x] (str "+" (name x))))

clj->js支持参数,可以把js的key转化为keyword

(js->clj #js {:a 1, :b 2, :c #js {:d 3}} :keywordize-keys true)

高级编译选项

高级编译(advanced)是破坏性的。它会混淆名称,函数名、属性都不会放过,closure(google的js编译器)编译优化的过程中会损失这些信息。

window.my_js_fn = function() { return true; }

本blog的js代码没有经过高级编译所以下面的调用是OK的,开高级编译下面的执行一定会报错

(defn -main []
  (. js/window my-js-fn))

(-main)

解决方案

(require '[goog.object :as g])
(g/set js/window "my-js-fn" (fn [] true) )
(defn -main []
  ((g/get js/window "my-js-fn")))

(-main)

属性设置同样使用goog.object的函数:

(g/set js/window "my-js-property" false)
(g/get js/window "my-js-property")

这些库的工作原理很简单:

window.myProperty = true
//可能会被重写 => window.ab = true
window["myProperty"] = true
//不会被重写 => window["myProperty"] = true

js object的读写

js obj不能通过js-clj来做转化

参考: goog.object/getValueByKeys

js->clj 的性能

比较低, 一般不是问题, 但如果在performance critical的路径上, 更多直接使用goog.object/get,goog.object/getValueByKeys, 使用上对应于get,get-in.

有100~200倍的性能差!!!!!!!

(goog.object/get #js {:a "a"} "a")
(goog.object/getValueByKeys #js {:a #js {:b #js {:c #js {:d "dddd"}}}} "a" "b" "c" "d")

同样参考: goog.object/getValueByKeys

Promise await/async

Promise.resolve(42)
  .then(val => console.log(val));

对应于:

(.then (js/Promise.resolve 42)
       #(js/console.log %))
Promise.resolve(42)
  .then(val => console.log(val))
  .catch(err => console.log(err))
  .finally(() => console.log('cleanup'));

对应于:

(.finally
  (.catch
  (.then (js/Promise.resolve 42)
          #(js/console.log %))
  #(js/console.log %))
  #(js/console.log "cleanup"))

; same as above
(-> (js/Promise.resolve 42)
    (.then #(js/console.log %))
    (.catch #(js/console.log %))
    (.finally #(js/console.log "cleanup")))
const puppeteer = require('puppeteer');
(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  try {
    await page.goto('https://example.com');
    await page.screenshot({path: 'example.png'});
  } catch (err) {
    console.log(err);
  }
  await browser.close();
})();

对应于:

(def puppeteer (js/require "puppeteer"))
(-> (.launch puppeteer)
    (.then (fn [browser]
             (-> (.newPage browser)
                 (.then (fn [page]
                          (-> (.goto page "https://clojure.org")
                              (.then #(.screenshot page #js{:path "screenshot.png"}))
                              (.catch #(js/console.log %))
                              (.then #(.close browser)))))))))

这个是有点长, 更好的方式是使用<p!, 很好的对应了async/await

(:require
   [cljs.core.async :refer [go]]
   [cljs.core.async.interop :refer-macros [<p!]])
(def puppeteer (js/require "puppeteer"))
(go
  (let [browser (<p! (.launch puppeteer))
        page (<p! (.newPage browser))]
    (try
      (<p! (.goto page "https://clojure.org"))
      (<p! (.screenshot page #js{:path "screenshot.png"}))
      (catch js/Error err (js/console.log (ex-cause err))))
    (.close browser)))

new

new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。

// JS class类
class User {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
};

方法一:

// js写法
const yang = new User('yang', 18);
;; clojure写法
(let [yang (new User "yang" 18)])

方法二:

// js写法
const yang = new User();
yang.name = 'yang';
yang.age = 18;
;; clojure写法
(let [yang (new User)]
  (.set! (.-name yang) "yang")
  (.set! (.-age yang) 18))

注意:方法一使用的前提是要确保类的构造方法中使用了传入的参数。
某些SDK的提供的API方法类的构造方法中可能没有使用传入的参数,而是指定了默认值,如下:

exports.TRTCVideoEncParam = class TRTCVideoEncParam {
  constructor() {
    this.videoResolution = TRTCVideoResolution.TRTCVideoResolution_640_360;
    this.resMode = TRTCVideoResolutionMode.TRTCVideoResolutionModeLandscape;
    this.videoFps = 15;
    this.videoBitrate = 550;
    this.enableAdjustRes = false;
  }
};
Tags: clojurescript