There are two pairs of complementary functions to set properties on objects in ClojureScript. One is aset
and aget
, another is set!
and .-propname
:
(def scope (js-obj)) (aset scope "var1" "Value") (aget scope "var1")
(def scope (js-obj)) (set! (.-var2 scope) "Value") (.-var2 scope)
Are they equivalent? Is syntax the only difference?
For a demo, let’s say we set and print two fields on an object, like this:
(def scope (js-obj)) (aset scope "var1" "Value") (set! (.-var2 scope) "Value") (.log js/console (str "(aget scope \"var1\") ->" (aget scope "var1"))) (.log js/console (str "(.-var2 scope) ->" (.-var2 scope))) (.log js/console (str "(.-var1 scope) ->" (.-var1 scope))) (.log js/console (str "(aget scope \"var2\") ->" (aget scope "var2")))
The resulting “setting” instructions are deceptively similar:
mynamespace.scope = {}; mynamespace.scope["var1"] = "Value"; mynamespace.scope.var2 = "Value";
And of course all combinations print out correctly – I can see this on the console:
(aget scope "var1") ->Value (.-var2 scope) ->Value (.-var1 scope) ->Value (aget scope "var2") ->Value
So far so good.
Now, if you’re at least a little bit serious about using ClojureScript, you have to consider advanced optimizations. What happens in this mode?
My code got compiled to:
var Xm; Xm = {var1:"Value", lc:"Value"};
And on the console all I see is:
(aget scope "var1") ->Value (.-var2 scope) ->Value (.-var1 scope) -> (aget scope "var2") ->
What happened to var2
? Why is set!
suddenly incompatible with aget
and aset
with .-field
?
If you look at compiler output in both cases, you should see that in one case the value is bound by name and in the other by symbol. In my understanding, it has a few consequences:
- Advanced compiler renames symbols, but leaves strings intact.
aset
andaget
operate on names.set!
and.-var
operate on symbols. If you set a variable using “symbolic reference”, the compiler will rename the symbol so you won’t be able to get it by name anymore. If you set it by name withaset
, reading by symbol as.-var
will not work either because the symbol is renamed.- If you deal only with ClojureScript, you should probably use the symbolic references. They look more idiomatic and will benefit from advanced compiler features such as minimization. On the other hand, in this case you often may want to leverage the idiomatic data bindings – vars, atoms and such.
- As soon as you want your names to be accessible from outside, or you want to access names set outside, use only
aset
/aget
. It obviously applies to interop with JavaScript libraries. Less obvious case (for me) was libraries that operate on DOM, like Angular or Knockout. If you want that{{user.name}}
to work, you cannot use the symbolic version.
It all makes sense now, but I haven’t seen it documented. Well, that’s still true for most ClojureScript.