ClojureScript中d3互操作:按字符串名调用带参函数遇错求助
Hey there! Let's work through how to fix this decoupling issue between your set-attr function and D3's .attr method in ClojureScript—those interop gotchas can be tricky, especially when D3's chainable API collides with functional patterns.
First, let's unpack that error: "Cannot read property 'each' of null" almost always means you're trying to call a D3 method (like .attr, which internally uses .each) on a null or undefined value. Your plain JS objects work because they don't rely on D3's chainable, selection-based API—D3 expects a valid selection (even an empty one) to call methods on, not null.
Here are a few actionable, idiomatic ClojureScript solutions to decouple set-attr from D3's .attr while avoiding that error:
1. Use Higher-Order Functions to Inject the Attr Logic
Instead of hardcoding .attr into set-attr, pass the attribute-setting function as a parameter. This keeps set-attr agnostic to D3 while preserving the context D3 needs.
(defn set-attr [attr-fn node attrs] ;; First guard against null/undefined nodes to avoid the each error (if (nil? node) node (reduce (fn [current-node [attr-key attr-val]] (attr-fn current-node attr-key attr-val)) node attrs))) ;; When using with D3, pass the .attr method as the attr-fn (set-attr #(.attr %1 %2 %3) d3-node {:fill "crimson" :stroke "black"}) ;; For plain JS objects, pass a different function (set-attr (fn [obj k v] (aset obj k v) obj) js-obj {:name "test" :value 42})
This way, set-attr doesn't need to know anything about D3—it just knows to apply a given function to each attribute pair.
2. Use ClojureScript Protocols for Polymorphic Behavior
If you want set-attr to seamlessly work with both D3 selections and plain JS objects, protocols are perfect for defining type-specific behavior without coupling to D3 directly.
;; Define a protocol for setting attributes (defprotocol ISetAttributes (-set-attrs [node attrs] "Set attributes on the node")) ;; Implement for D3 selections (we'll assume it's a JS object for simplicity) (extend-type js/Object ISetAttributes (-set-attrs [node attrs] (if (nil? node) node (reduce (fn [n [k v]] (.attr n k v)) node attrs)))) ;; Implement for plain JS objects (extend-type js/Object ISetAttributes (-set-attrs [node attrs] (reduce (fn [n [k v]] (aset n k v) n) node attrs))) ;; Public function that dispatches to the protocol (defn set-attr [node attrs] (-set-attrs node attrs))
Now you can call set-attr on either a D3 node or plain JS object, and it'll handle each case correctly—no hardcoded D3 dependencies in the core logic.
3. Guard Against Invalid D3 Selections
Even if you decouple, make sure you're not passing null to set-attr in the first place. D3's select returns an empty selection (not null) if it can't find an element, but if you're manually creating nodes or using other methods, add a check:
;; Check if the D3 selection is valid before calling set-attr (when-not (.empty d3-node) (set-attr d3-node {:fill "blue"}))
Empty D3 selections won't throw errors when you call .attr on them, so this prevents the "each of null" issue entirely.
The key takeaway here is that D3's API relies on maintaining selection context—decoupling doesn't mean stripping that context away, just making your set-attr function agnostic to how attributes are set, while still respecting the requirements of the object it's operating on.
内容的提问来源于stack exchange,提问作者Ryan Asensio

