ClojureScript - 将任意的 JavaScript 对象转换为 Clojure Script 的映射

13

我正在尝试将一个JavaScript对象转换为Clojure。但是,我遇到了以下错误:

 (js/console.log (js->clj e)) ;; has no effect
 (pprint (js->clj e)) ;; No protocol method IWriter.-write defined for type object: [object Geoposition]

是的,这个对象来自于Geolocation API。我想我需要扩展IEncodeClojureIWriter,但我不知道该怎么做。

例如,添加以下内容:

(extend-protocol IEncodeClojure
  Coordinates
  (-js->clj [x options]
    (println "HERE " x options)))

加载我的代码时会产生错误:Uncaught TypeError: Cannot read property 'prototype' of undefined


你确定那里有一个对象而不是undefined吗?(js/console.log (undefined? e))会产生什么结果? - Tim Pote
@TimPote 不是未定义的:使用Clojure timbre,我可以获取对象的名称。当执行(js/console.log e)(js/console.log (js->clj e))时,我得到相同的js对象。 - nha
4个回答

13

对于JavaScript对象window.performance.timing,被接受的答案在我这里并不起作用。这是因为Object.keys()实际上并未返回PerformanceTiming对象的属性。

(.keys js/Object (.-timing (.-performance js/window))
; => #js[]

尽管使用原生 JavaScript 循环可以迭代 PerformanceTiming 的属性,但事实并非如此:

for (a in window.performance.timing) {
  console.log(a);
}
// navigationStart
// unloadEventStart
// unloadEventEnd
// ...

以下是我想出的将任意JavaScript对象转换为ClojureScript map的解决方案。请注意使用了两个简单的Google Closure函数。

  • goog.typeOf包装了typeof,这在ClojureScript中通常是不可访问的。我使用它来过滤掉是函数的props。
  • goog.object.getKeys包装了for (prop in obj) {...},构建一个数组结果,我们可以将其缩减到一个map中。

解决方案(平面)

(defn obj->clj
  [obj]
  (-> (fn [result key]
        (let [v (goog.object/get obj key)]
          (if (= "function" (goog/typeOf v))
            result
            (assoc result key v))))
      (reduce {} (.getKeys goog/object obj))))

解决方案(递归)

更新:此解决方案适用于嵌套映射。

(defn obj->clj
  [obj]
  (if (goog.isObject obj)
    (-> (fn [result key]
          (let [v (goog.object/get obj key)]
            (if (= "function" (goog/typeOf v))
              result
              (assoc result key (obj->clj v)))))
        (reduce {} (.getKeys goog/object obj)))
    obj))

1
aget 不应用于获取 JS 对象的值:https://clojurescript.org/news/2017-07-14-checked-array-access - kamituel
1
@kamituel,感谢您指出这一点。我已经更新了我的答案,使用goog.object/get - Aaron Blenkush
1
关键字化似乎很容易,只需用(clojure.walk/keywordize-keys (if ...))将整个(if ...)包装起来即可。 - Robert J Berger
2
@RobertJBerger,优化的方法是将第8行的 key 更改为 (keyword key) - Aaron Blenkush
1
@NikoNyrh 如果你取消 isObject 检查并信任调用者,你可以在函数签名中使用类型提示 obj,例如 [^js/Object obj] - Felipe Cortez
显示剩余2条评论

11

1
aget 不应该用于获取 JS 对象的值:https://clojurescript.org/news/2017-07-14-checked-array-access - kamituel

2
两种方法不需要编写自定义转换函数,它们都使用标准的JavaScript函数来解开自定义原型,从而使clj->js正常工作。

使用JSON序列化

这种方法只是将其序列化为JSON并立即解析:

(js->clj (-> e js/JSON.stringify js/JSON.parse))

优点:
  • 不需要任何辅助函数
  • 适用于嵌套对象,有/无原型
  • 在每个浏览器中都得到支持
缺点:
  • 在关键部分的代码库中可能会影响性能
  • 将剥离所有非可序列化值,如函数。

使用 Object.assign()

这种方法基于 Object.assign(),它通过将所有属性从 e 复制到一个新的、普通的(没有自定义原型)#js {} 上来实现。

(js->clj (js/Object.assign #js {} e))

优点:
- 不需要任何辅助函数。
缺点:
- 仅适用于扁平对象,如果 e 内还有其他嵌套对象,则 clj->js 无法将其转换。 - Object.assign() 不受旧版浏览器支持,尤其是 IE。

有趣。与其他解决方案相比有什么优势吗? - nha
1
@nha 我最终使用了不同的方法 (JSON 序列化)。更新了我的回答,解释了两种方法以及各自的利弊。 - kamituel
Object.assign() 对于 window.performance.timing 不起作用。 - Aaron Blenkush

0
(defn obj->clj
  ([obj]
   (obj->clj obj :keywordize-keys false))
  ([obj & opts]
   (let [{:keys [keywordize-keys]} opts
         keyfn (if keywordize-keys keyword str)]
     (if (and (not-any? #(% obj) [inst? uuid?])
              (goog.isObject obj))
       (-> (fn [result k]
             (let [v (goog.object/get obj k)]
               (if (= "function" (goog/typeOf v))
                 result
                 (assoc result (keyfn k) (apply obj->clj v opts)))))
           (reduce {} (.getKeys goog/object obj)))
       obj))))

原文存在一个小问题,即JS将#inst和#uuid视为对象。这似乎是Clojure中唯一的标记字面量。
我还通过查看js->clj源代码添加了将关键字化的键作为选项。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接