[CLJS-2408] js->clj does not work on objects with null prototype Created: 21/Nov/17  Updated: 25/Apr/18

Status: Open
Project: ClojureScript
Component/s: None
Affects Version/s: 1.9.908
Fix Version/s: None

Type: Defect Priority: Major
Reporter: Kurt Harriger Assignee: Unassigned
Resolution: Unresolved Votes: 1
Labels: None


 Description   

Version: 1.9.946
Follow up from issue CLJS-1998

cljs.user=> (js->clj #js {})
{}

cljs.user=> (js->clj (.create js/Object nil))
#object[Object]

cljs.user=> (keys (js->clj (.create js/Object nil)))
org.mozilla.javascript.EcmaError: TypeError: Cannot find default value for object. (.cljs_rhino_repl/goog/../.cljs_rhino_repl/cljs/core.js#9915)
	 (.cljs_rhino_repl/cljs/core.cljs:2930:10)
	 cljs$core$seq (.cljs_rhino_repl/cljs/core.cljs:1212:13)
	 cljs$core$keys (.cljs_rhino_repl/cljs/core.cljs:8648:3)
	 (NO_SOURCE_FILE <cljs repl>:1:0)
	 (NO_SOURCE_FILE <cljs repl>:1:0)


 Comments   
Comment by Kurt Harriger [ 21/Nov/17 8:44 PM ]

For more context the source of the object with null prototype is generated in a third party library I am using here https://github.com/ProseMirror/prosemirror-model/blob/cdded3d/src/schema.js#L13

Comment by Kurt Harriger [ 21/Mar/18 7:47 PM ]

```
(def a (.create js/Object null)) => #'cljs.user/a
(type a) => nil
```

The implementation of js->clj explicitly checks for type js/Object.

```
(defn js->clj
"Recursively transforms JavaScript arrays into ClojureScript
vectors, and JavaScript objects into ClojureScript maps. With
option ':keywordize-keys true' will convert object fields from
strings to keywords."
([x] (js->clj x :keywordize-keys false))
([x & opts]
(let [{:keys [keywordize-keys]} opts
keyfn (if keywordize-keys keyword str)
f (fn thisfn [x]
(cond
(satisfies? IEncodeClojure x)
(js>clj x (apply array-map opts))

(seq? x)
(doall (map thisfn x))

(coll? x)
(into (empty x) (map thisfn x))

(array? x)
(vec (map thisfn x))

(identical? (type x) js/Object)
(into {} (for [k (js-keys x)]
[(keyfn k) (thisfn (unchecked-get x k))]))

:else x))
```

I think perhaps this last test should be changed to `(or (nil? (type a)) (identical? (type x) js/Object))` because the only way to create an object with a null prototype is explicitly and they do so because they are intended to be used as associative objects. (http://adripofjavascript.com/blog/drips/creating-objects-without-prototypes.html)

Comment by Kurt Harriger [ 22/Mar/18 5:55 PM ]

The above also suggests that maybe one could extend IEncodeClojure on nil?
`a` is not `nil` but its type is... so maybe extending protocol on nil should this work?

But it doesn't... maybe this is a bug in `satisfies?`?

```
cljs.user=> (def a (.create js/Object nil))
#'cljs.user/a
cljs.user=> (aset a "test" 1)
1
cljs.user=> (extend-protocol IEncodeClojure
nil
(js>clj [x opts]
(let [{:keys [keywordize-keys]} opts
keyfn (if keywordize-keys keyword str)]
(into {} (for [k (js-keys x)]
[(keyfn k) (js->clj (unchecked-get x k)) opts])))))
nil
cljs.user=> (js->clj a)
#object[Object]
cljs.user=> (js-keys IEncodeClojure)
#js ["null"]
cljs.user=> (satisfies? IEncodeClojure a)
false
cljs.user=> (type a)
nil

```

Comment by Hendrik Poernama [ 25/Apr/18 4:04 AM ]

What about `(identical? (goog/typeOf x) "object")` instead of `(identical? (type x) js/Object)`?

Closure's typeOf correctly returns 'null' and 'array' and has some browser specific fixes.
Current test looks like a lot less code, so may need to performance test.

Example:
(identical? "object" (goog/typeOf (.create js/Object nil #js {:a #js {:value 1}}))) => true

Generated at Thu Apr 18 17:33:01 CDT 2019 using JIRA 4.4#649-r158309.