13

I am trying to convert a Javascript object to a Clojure. However, I get the following error :

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

Yes, this object comes from the Geolocation API. I suppose that I have to extend IEncodeClojure and IWriter, but I have no clue how.

For instance adding the following :

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

Yields an error when loading my code : Uncaught TypeError: Cannot read property 'prototype' of undefined

nha
  • 17,623
  • 13
  • 87
  • 133
  • Are you sure you have an object there and not `undefined`? What does `(js/console.log (undefined? e))` yield? – Tim Pote Sep 08 '15 at 23:42
  • @TimPote It is not undefined : using Clojure timbre, I get the name of the object. Using js/console.log I get the same js object when doing `(js/console.log e)` and `(js/console.log (js->clj e))`. – nha Sep 09 '15 at 07:27

4 Answers4

13

The accepted answer wasn't working for me with the javascript object window.performance.timing. This is because Object.keys() doesn't actually return the props for the PerformanceTiming object.

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

This is despite the fact that the props of PerformanceTiming are indeed iterable with a vanilla JavaScript loop:

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

The following is what I came up with to convert an arbitrary JavaScript object to a ClojureScript map. Note the use of two simple Google Closure functions.

  • goog.typeOf wraps typeof, which isn't normally accessible to us in ClojureScript. I use this to filter out props which are functions.
  • goog.object.getKeys wraps for (prop in obj) {...}, building up an array result which we can reduce into a map.

Solution (flat)

(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))))

Solution (recursive)

Update: This solution will work for nested maps.

(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))
Community
  • 1
  • 1
Aaron Blenkush
  • 3,034
  • 2
  • 28
  • 54
11

js->clj only works for Object, anything with custom constructor (see type) will be returned as is.

see: https://github.com/clojure/clojurescript/blob/master/src/main/cljs/cljs/core.cljs#L9319

I suggest doing this instead:

(defn jsx->clj
  [x]
  (into {} (for [k (.keys js/Object x)] [k (aget x k)])))

UPDATE for correct solution see Aaron's answer, gotta use goog.object

skrat
  • 5,518
  • 3
  • 32
  • 48
  • 1
    `aget` should not be used for obtaining values of JS objects: https://clojurescript.org/news/2017-07-14-checked-array-access – kamituel Jul 20 '18 at 09:40
2

Two approaches that do not require writing custom conversion functions - they both employ standard JavaScript functions to loose the custom prototype and thus enable clj->js to work correctly.

Using JSON serialization

This approach just serializes to JSON and immediately parses it:

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

Advantages:

  • does not require any helper function
  • works for nested objects, with/without prototype
  • supported in every browser

Disadvantages:

  • performance might be a problem in critical pieces of codebase
  • will strip any non-serializable values, like functions.

Using Object.assign()

This approach is based on Object.assign() and it works by copying all the properties from e onto a fresh, plain (no custom prototype) #js {}.

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

Advantages:

  • does not require any helper function

Disadvantages:

  • works on flat objects, if there is another nested object withing e, it won't be converted by clj->js.
  • Object.assign() is not supported by old browsers, most notably - IE.
kamituel
  • 34,606
  • 6
  • 81
  • 98
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))))

Small problem with the original above is that JS treats #inst and #uuid as objects. Seems like those are the only tagged literals in clojure

I also added the option to keywordize keys by looking at js->clj source