This article is Lesson 13 of Andrew's book, Learn ClojureScript
One of the advantages of ClojureScript is its excellent interoperability with JavaScript. When Clojure was first introduced, one of its primary goals was providing simple integration with existing Java code. ClojureScript continues in this spirit of valuing integration with its host platform. We will deal with JavaScript interoperability to a greater extent later, but at this point, we will concern ourselves with creating and manipulating JavaScript data structures.
In this lesson:
- Convert between ClojureScript and JavaScript data types
- Integrate ClojureScript code with an existing JavaScript codebase
- Understand the implications of using mutable JavaScript objects and arrays
Example: Integration With Legacy Code
Imagine that we have decided to slowly migrate a legacy JavaScript application to ClojureScript (an excellent choice). However, due to the size of the codebase, it is more practical to migrate one piece at a time. In the meantime, we need to interact with our legacy application, a classroom management application, from ClojureScript. We will need to read a list of scores from the legacy application, perform modifications in ClojureScript, and send the results back to the JavaScript application. Fortunately for us, ClojureScript has excellent interoperability with JavaScript, so let's learn how it's done!
Using Conversion Functions
When we are working with an existing JavaScript codebase or libraries, chances are that we will be passing JavaScript data structures around, but we would like to treat them as ClojureScript data within our application. ClojureScript provides two handy functions for converting between JavaScript and ClojureScript data structures: js->clj
for converting from JavaScript and clj->js
for converting to JavaScript. We can easily use these functions to convert data to ClojureScript structures coming into our program and back to JavaScript on the way out.
Let's try this out by opening up the REPL and the browser tab that it is connected to. Open the dev tools and create an object called testScores
that looks something like the following:
var testScores = [ // <1>
{ id: 1, score: 86, gradeLetter: "B" }, // <2>
{ id: 2, score: 93, gradeLetter: "A" },
{ id: 3, score: 78, gradeLetter: "C" }
];
Creating a JS Object
- The top-level structure is an array of objects
- The nested objects have
id
,score
, andgradeLetter
properties
This creates a global JavaScript variable called testScores
, which we can access from the REPL. ClojureScript creates a namespace (think a module for collecting functions and data) called js
that contains all of the global variables that are available within the browser. For example, we can access the document
object with js/document
, the window
object with js/window
, etc.
Sharing Data Between Browser and REPL
We can use the REPL to inspect this variable, convert it to a ClojureScript data structure, modify it and write a new version back out the the testScores
variable.
cljs.user=> (def cljs-scores (js->clj js/testScores)) // <1>
#'cljs.user/cljs-scores
cljs.user=> cljs-scores
[{"id" 1, "score" 86, "gradeLetter" "B"}
{"id" 2, "score" 93, "gradeLetter" "A"}
{"id" 3, "score" 78, "gradeLetter" "C"}]
cljs.user=> (conj cljs-scores // <2>
{"id" 4, "score" 87, "gradeLetter" "B"})
[{"id" 1, "score" 86, "gradeLetter" "B"}
{"id" 2, "score" 93, "gradeLetter" "A"}
{"id" 3, "score" 78, "gradeLetter" "C"}
{"id" 4, "score" 87, "gradeLetter" "B"}]
cljs.user=> cljs-scores
[{"id" 1, "score" 86, "gradeLetter" "B"}
{"id" 2, "score" 93, "gradeLetter" "A"}
{"id" 3, "score" 78, "gradeLetter" "C"}]
cljs.user=> (def updated-scores // <3>
(conj cljs-scores {"id" 4, "score" 87, "gradeLetter" "B"}))
#'cljs.user/updated-scores
cljs.user=> (set! js/testScores (clj->js updated-scores)) // <4>
#js [#js {:id 1, :score 86, :gradeLetter "B"}
#js {:id 2, :score 93, :gradeLetter "A"}
#js {:id 3, :score 78, :gradeLetter "C"}
#js {:id 4, :score 87, :gradeLetter "B"}]
Converting between JavaScript and ClojureScript data
- Convert
testScores
to a ClojureScript value - Create a modified value by appending a new score and verify that the value in the var
cljs-scores
was not changed - Bind the updated scores to the
updated-scores
var - Convert the updated scores back to a JavaScript object and update
testScores
to the new value
We can inspect the testScores
variable in the browser to make sure that it has been changed to include the new score.
Checking the Updated Scores
Quick Review
We still have a reference to the js/testScores
variable.
- What will happen if we change this variable in the browser's developer tools and print it out from ClojureScript?
- Will changing this JavaScript variable affect our
cljs-scores
variable?
Note
Since ClojureScript has richer data types than JavaScript,
clj->js
is a lossy operation. For instance, sets get converted to JS arrays, and keywords and symbols get converted to strings. This means that some ClojureScript value contained in the var,x
, is not always equal to(js->clj (clj->js x))
. For instance, if we have a set,#{"Lucy" "Ricky" "Fred" "Ethel"}
, and we convert this to JavaScript, we will end up with and array:["Ricky", "Fred", "Lucy", "Ethel"]
(remember, sets are not ordered, so the order in which the elements appear when converted to an array is arbitrary). If we convert this array back to ClojureScript, we end up with the vector,["Ricky" "Fred" "Lucy" "Ethel"]
, not the set that we started with, as we demonstrate below.cljs.user=> (def characters #{"Lucy" "Ricky" "Fred" "Ethel"}) #'cljs.user/characters cljs.user=> (def js-characters (clj->js characters)) #'cljs.user/js-characters cljs.user=> js-characters #js ["Ricky" "Fred" "Lucy" "Ethel"] cljs.user=> (js->clj js-characters) ["Ricky" "Fred" "Lucy" "Ethel"] cljs.user=> (= characters (js->clj js-characters)) false
You Try It
- Create a JavaScript object from the REPL and make it available as
window.myVar
. - Create a JavaScript object in the dev tools called
jsObj
and modify it using theset!
function in the ClojureScript REPL
Working with JavaScript Data Directly
Although it is very common to convert JavaScript data from the "outside world" to ClojureScript data before working with it, it is also possible to create and modify JavaScript data directly from within ClojureScript. ClojureScript numbers, strings, and booleans are the same as their JavaScript counterparts, so they can be handled natively from ClojureScript.
Using Objects
Objects can be created either with the js-obj
function or the literal syntax, #js {}
.
cljs.user=> (js-obj "isJavaScript" true, "type" "object") ;; <1>
#js {:isJavaScript true, :type "object"}
cljs.user=> #js {"isJavaScript" true, "type" "object"} ;; <2>
#js {:isJavaScript true, :type "object"}
Constructing JavaScript Objects
- Creating an object with the
js-obj
function - Creating an object with the literal
#js {}
syntax
The js-obj
function takes an even number of arguments, which expected to be pairs of key, value. The literal syntax looks like a ClojureScript map proceeded by #js
. Both of these forms produce identical JavaScript objects, but the literal syntax is by far the most common.
We can get and set properties on JavaScript objects with the aget
and aset
functions. aget
takes as arguments the object and the property to get:
cljs.user=> (def js-hobbit #js {"name" "Bilbo Baggins", "age" 111})
#'cljs.user/js-hobbit
cljs.user=> (aget js-hobbit "age")
111
The aget
function also supports accessing properties inside nested objects, similar to chaining property lookups on JavaScript objects. For instance, in JavaScript, we could do the following (if we were confident that none of the intermediate properties were null
):
// JavaScript nested lookup
var settings = { // <1>
personal: {
address: {
street: "123 Rolling Hills Dr"
}
}
};
// Prints "123 Rolling Hills Dr"
console.log(settings.personal.address.street); // <2>
- A nested JavaScript object
- Accessing a nested property
Using aget
in ClojureScript accomplishes the same task:
(println
(aget settings "personal" "address" "street")) ; Prints "123 Rolling Hills Dr"
Just as aget
allows us to access properties on a potentially nested object, aset
lets us mutate properties. aset
, like aget
, takes a JavaScript object and any number of property names, but it also takes some value to set as its last argument
cljs.user=> (aset js-hobbit "name" "Frodo") ;; <1>
"Frodo"
cljs.user=> (aset js-hobbit "age" 33)
33
cljs.user=> js-hobbit ;; <2>
#js {:name "Frodo", :age 33}
- Setting two properties on the
js-hobbit
object -
aset
mutates the object
Experiment
Since the aget
function supports access of nested properties, it only makes sense that the the aset
function would support setting nested properties. Use the REPL to try to find the correct syntax for setting the following student's grade in her Physics class:
(def student #js {"locker" 212
"grades" {"Math" "A",
"Physics" "B",
"English" "A+"}})
Unlike the functions that we have seen that operate on ClojureScript data, aset
actually modifies the object in-place. This is because we are working with mutable JavaScript data.
Direct Property Access
There is an alternate method for getting and setting properties on JavaScript objects: direct property access. Properties can be accessed with
(.-propertyName obj)
and set with(set! (.-propertyName obj) value)
. There are 2 major differences between direct access andaget
/aset
:
- Under its advanced compilation setting, the ClojureScript compiler will minify the property names. This could potentially break code that interacts with existing JavaScript, since the minified property names will not match the property names in the external code.
- Direct property access does not directly support nested property access like
aget
does.Because of these "gotchas", we will usually want to stick with
aget
/aset
, even though they are slightly more verbose.
Using Arrays
Just like there is a function and a literal syntax for creating JavaScript objects, we can use the array
function or the #js []
literal for creating JavaScript arrays.
cljs.user=> (array "foo" "bar" "baz")
#js ["foo" "bar" "baz"]
cljs.user=> #js [1 3 5 7 11]
#js [1 3 5 7 11]
We can use the same aget
and aset
functions to get and set elements at a specific index.
cljs.user=> (def primes #js [1 3 5 7 11]) ;; <1>
#'cljs.user/primes
cljs.user=> (aget primes 2) ;; <2>
5
cljs.user=> (aset primes 5 13) ;; <3>
13
books
cljs.user=> primes ;; <4>
#js [1 3 5 7 11 13]
Getting and Setting Array Elements
- Bind a var to a JavaScript array
- Get the element at index
2
- Get the element at index
5
to13
-
aset
has mutated the array
We can also access the JavaScript array methods by using, (.functionName array args*)
. This is the standard syntax for calling a method on a JavaScript object, which we will explain in much more detail later.
cljs.user=> (.indexOf primes 11) <1>
4
cljs.user=> (.pop primes) <2>
13
cljs.user=> primes
#js [1 3 5 7 11]
Using JavaScript Array Methods
- Call the
indexOf
method onprimes
- equivalent toprimes.indexOf(11)
in JavaScript - Call the
pop
method - equivalent toprimes.pop()
in JavaScript
Quick Review
- Use the JavaScript
Array.prototype.push
function to add a value to the end of this array:#js ["first", "second"]
- Use the JavaScript
Array.prototype.pop
function to remove the value that you just added in the previous exercise
Best Practice
Although ClojureScript makes working with JavaScript objects and arrays simple, we should prefer to use ClojureScript data structures and only convert to and from JavaScript data at the "edges" of our program or when interacting with another library. The advantages that we get from immutable data - particularly the safeguard against all sorts of mutation-related bugs - are significant, so the more of our apps are immutable, the better.
You Try It
Create the following variable in your browser's dev tools:
var books = [
{ title: 'A History of LISP', subjects: ['Common Lisp', 'Scheme', 'Clojure'] },
{ title: 'All About Animals', subjects: ['Piranhas', 'Tigers', 'Butterflies'] }
];
- Write an
aget
expression that will retrieve the value, "Scheme": - Write an
aset
expression that will change the title of, "All About Animals" to "Dangerous Creatures".
Challenge
Write a ClojureScript function that will take a book represented as a ClojureScript map, convert it to a JavaScript object, append it to the books
array, and return the number of elements in books
after adding the new book.
Possible Solution:
(defn add-book [book]
(let [js-book (clj->js book)]
(.push js/books js-book)
(aget js/books "length")))
Summary
ClojureScript has a symbiotic relationship with JavaScript, and to effective use it, we must be comfortable interacting with the host language. In this lesson, we looked at how to work with JavaScript data. We used both the ClojureScript REPL and the browser's JavaScript dev tools to walk through the process of converting between ClojureScript and JavaScript data structures as well as directly modifying JavaScript objects and arrays. We are now able to:
- Create JavaScript objects from ClojureScript code
- Modify JavaScript objects and arrays
- Convert between ClojureScript's data structures and JavaScript objects and arrays
Top comments (1)
My understanding is the
clj->js
is intended to produce a serializable object, and while Sets are now native to JavaScript, they have no serializable (JSON) representation. The same goes for Symbols and Maps, which could potentially represent ClojureScript symbols/keywords and maps respectively.