How to `apply` with the console api
Over in the cljs slack channel, a question came up about using apply
with js/console.log
.
=> (apply js/console.log ["log" "me"])
;; throws Uncaught TypeError: Illegal invocation
Using cljs.core/enable-console-print! as an example, we see the proper way:
=> (.apply (.-log js/console) js/console (into-array ["log" "me"]))
;; logs "log me"
This works with each of the console methods (e.g. warn, error, etc.). Additionally, check out shodan, which provides wrappers for the console api.
Why doesn’t `apply` work?
What follows is just some exploration; it isn’t necessary to understand and (unfortunately) doesn’t provide a solution. I used Chrome for this exploration.
When apply
is called with a JavaScript function, you generally end up at this branch:
(defn apply
([f args]
(let [fixed-arity (.-cljs$lang$maxFixedArity f)]
(if (.-cljs$lang$applyTo f)
(let [bc (bounded-count args (inc fixed-arity))]
(if (<= bc fixed-arity)
(apply-to f bc args)
(.cljs$lang$applyTo f args)))
;;You hit this branch
(.apply f f (to-array args)))))
...)
So our example code ends up as:
(apply js/console.log ["log" "me"])
;;compiles to =>
(.apply js/console.log js/console.log (to-array args))
;;should be =>
(.apply js/console.log js/console (to-array args))
The js method .apply
expects its first argument to be the this
value for the function call. So our code passes the actual function as this
, when we really want the object. Why is apply
written this way and not considered broken?
apply
does work in some cases. Static methods work, likely because they don’t rely on this
:
Math.max.apply(null,[2,3,1,5,3]); //works
Math.max.apply(Math,[2,3,1,5,3]); //also works
Math.max.apply(Math.max,[2,3,1,5,3]); //Yep; compiled cljs version
Date.UTC.apply(null,[2000,7,3]); //works
Date.UTC.apply(Date,[2000,7,3]); //also works
Date.UTC.apply(Date.UTC,[2000,7,3]); //yep; compiled cljs version
console.log.apply(null,["log","me"]); //doesn't work
console.log.apply(console,["log","me"]); //works
console.log.apply(console.log,["log","me"]); //doesn't work; compiled cljs version
document.createElement.apply(null,["p"]); //doesn't work
document.createElement.apply(document,["p"]); //works
document.createElement.apply(document.createElement,["p"]); //Nope; compiled cljs version
If we could somehow get the instance from the method call (e.g. get console
from console.log
), we could fix cljs apply
to work in more (all?) cases. But we can’t do that in js, so I guess we’re kinda sunk. ¯\_(ツ)_/¯