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

2
  • Are you sure you have an object there and not undefined? What does (js/console.log (undefined? e)) yield? Commented Sep 8, 2015 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)). Commented Sep 9, 2015 at 7:27

4 Answers 4

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))
Sign up to request clarification or add additional context in comments.

7 Comments

aget should not be used for obtaining values of JS objects: clojurescript.org/news/2017-07-14-checked-array-access
@kamituel, thanks for pointing that out. I have updated my answer to use goog.object/get.
To keywordize, it seemed as easy as wrapping the whole (if ...) with (clojure.walk/keywordize-keys (if ...))
@RobertJBerger, an optimization on that would be to change key on line 8 to (keyword key).
@NikoNyrh if you do away with the isObject check and trust the caller, you can type hint obj in the function's signature like [^js/Object obj]
|
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

1 Comment

aget should not be used for obtaining values of JS objects: clojurescript.org/news/2017-07-14-checked-array-access
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.

3 Comments

Interesting. Any advantages compared to the other solutions?
@nha I actually ended up using a different approach (JSON serialization). Updated my answer to explain both approaches, and pros/cons of each.
Object.assign() isn't working for window.performance.timing.
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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.