Before we move on to writing safe mutable code, we need to discuss references and values. A value can be considered anything that is a primitive type. Primitive types, in JavaScript, are anything that are not considered objects. To put it simply, numbers, strings, Booleans, null, and undefined are values. This means that if you create a new variable and assign it to the original, it will actually give it a new value. What does this mean for our code then? Well, we saw earlier with Redux that it was not able to see that we updated a property in our state system, so our previous state and current state showed they were the same. This is due to a shallow equality test. This basic test tests whether the two variables that were passed in are pointing to the same object. A simple example of this is seen with the following code:
let x = {};
let y = x;
console.log( x === y );
y = Object.assign({}, x);
console.log( x === y );
We will see that the first version says that the two items are equal. But, when we create a copy of the object, it states that they are not equal. y now has a brand-new object and this means that it points to a new location in memory. While a deeper understanding of pass by value and pass by reference can be good, this should be sufficient to move on to mutable code.
When writing safe mutable code, we want to give the illusion that we are writing immutable code. In other words, the interface should look like we are utilizing immutable systems, but we are instead utilizing mutable systems internally. Hence, there is a separation of the interface from the implementation.
We can make the implementation very fast by writing in a mutable way but give an interface that looks immutable. An example of this is as follows:
Array.prototype._map = function(fun) {
if( typeof fun !== 'function' ) {
return null;
}
const arr = new Array(this.length);
for(let i = 0; i < this.length; i++) {
arr[i] = fun(this[i]);
}
return arr;
}
We have written a _map function on the array prototype so that every array gets it and we write a simple map function. If we now test run this code, we will see that some browsers perform better with this, while others perform better with the built-in option. As stated before, the built-ins will eventually get faster, but, more often than not, a simple loop is going to be faster. Let's now look at another example of a mutable implementation, but with an immutable interface:
Array.prototype._reduce = function(fun, initial=null) {
if( typeof fun !== 'function' ) {
return null;
}
let val = initial ? initial : this[0];
const startIndex = initial ? 0 : 1;
for(let i = startIndex; i < this.length; i++) {
val = fun(val, this[i], i, this);
}
return val;
}
We wrote a reduce function that performs better in every browser. Now, it does not have the same amount of type checking, which could lead to better performance, but it does showcase how we can write functions that can perform better but give the same type of interface that a user of our system expects.
What we have talked about so far is if we were writing a library for someone to use to make their lives easier. What happens if we are writing something that we or an internal team is going to utilize, as is the case for most application developers?
We have two options in this case. First, we may find that we are working on a legacy system and that we are going to have to try to program in a similar style to what has already been done, or we are developing something rather new and we are able to start off from scratch.
With a new system, we are able to write how we want and, with proper documentation, we can write something that is quite fast but is also easy for someone else to pick up. In this case, we can write mutable code that may have side effects in the functions, but we are able to document these cases.
Side effects are conditions that occur when a function does not just return a new variable or even a reference that the variable passed in. It is when we update another variable that we do not have current scope over that this constitutes a side effect. An example of this is as follows:
var glob = 'a single point system';
const implement = function(x) {
glob = glob.concat(' more');
return x += 2;
}
We have a global variable called glob that we are changing inside our function. Technically, this function has scope over glob, but we should try to define the scope of implement to be only what was passed into it and the temporary variables that implement have defined inside. Since we are mutating glob, we have introduced a side effect into our code base.
Now, in some situations, side effects are needed. We may need to update a single point, or we may need to store something in a single location, but we should try to implement an interface that does this for us instead of us directly affecting the global item (this should start to sound a lot like Redux). By writing a function or two to affect the out-of-scope items, we can now diagnose where an issue may come in because we have those single points of entry.
So what might this look like? We could create a state object just as a plain old object. Then, we could write a function on the global scope called updateState that would look like the following:
const updateState = function(update) {
const x = Object.keys(update);
for(let i = 0; i < x.length; i++) {
state[x[i]] = update[x[i]];
}
}
Now, while this may be good, we are still vulnerable to someone updating our state object through the actual global property. Luckily, by making our state object and our function const, we can make sure that erroneous code cannot touch these actual names. Let's update our code so our state is protected from being updated directly. There are two ways that we could do this. The first approach would be to code with modules and then our state objects which will be scoped to that module. We will look at modules and the import syntax further in the book. Instead, on this occasion, we are going to use the second method, code the Immediately Invoked Function Expression (IIFE) way. The following showcases this implementation:
const state = {};
(function(scope) {
const _state = {};
scope.update = function(obj) {
const x = Object.keys(obj);
for(let i = 0; i < x.length; i++) {
_state[x[i]] = obj[x[i]];
}
}
scope.set = function(key, val) {
_state[key] = val;
}
scope.get = function(key) {
return _state[key];
}
scope.getAll = function() {
return Object.assign({}, _state);
}
})(state);
Object.freeze(state);
First, we create a constant state. We then IIFE and pass in the state object, setting a bunch of functions on it. It works on an internally scoped _state variable. We then have all the basic functions that we would expect for an internal state system. We also freeze the external state object so it can no longer be messed with. One question that may arise is why we are passing back a new object instead of a reference. If we are trying to make sure that we don't want anyone able to touch the internal state, then we cannot pass a reference out; we have to pass a new object.
We still have a problem. What happens if we want to update more than one layer deep? We will start running into reference issues again. That means that we will need to update our update function to perform a deep update. We can do this in a variety of ways, but one way would be to pass the value in as a string and we will split on the decimal point.
So, we will have a method that will now look like the following:
const getNestedProperty = function(key) {
const tempArr = key.split('.');
let temp = _state;
while( tempArr.length > 1 ) {
temp = temp[tempArr.shift()];
if( temp === undefined ) {
throw new Error('Unable to find key!');
}
}
return {obj : temp, finalKey : tempArr[0] };
}
scope.set = function(key, val) {
const {obj, finalKey} = getNestedProperty(key);
obj[finalKey] = val;
}
scope.get = function(key) {
const {obj, finalKey} = getNestedProperty(key);
return obj[finalKey];
}
What we are doing is breaking the key upon the decimal character. We are also grabbing a reference to the internal state object. While we still have items in the list, we move one level down in the object. If we find that it is undefined, then we will throw an error. Otherwise, once we are one level above where we want to be, we return an object with that reference and the final key. We will then use this in the getter and setter to replace those values.
Now, we still have a problem. What if we want to make a reference type be the property value for our internal state system? Well, we will run into the same issues that we saw before. We will have references outside the single state object. This means we will have to clone each step of the way to make sure that the external reference does not point to anything in the internal copy. We can create this system by adding a bunch of checks and making sure that when we get to a reference type, we clone it in a way that is efficient. This looks like the following code:
const _state = {},
checkPrimitives = function(item) {
return item === null || typeof item === 'boolean' || typeof item ===
'string' || typeof item === 'number' || typeof item === 'undefined';
},
cloneFunction = function(fun, scope=null) {
return fun.bind(scope);
},
cloneObject = function(obj) {
const newObj = {};
const keys = Object.keys(obj);
for(let i = 0; i < keys.length; i++) {
const key = keys[i];
const item = obj[key];
newObj[key] = runUpdate(item);
}
return newObj;
},
cloneArray = function(arr) {
const newArr = new Array(arr.length);
for(let i = 0; i < arr.length; i++) {
newArr[i] = runUpdate(arr[i]);
}
return newArr;
},
runUpdate = function(item) {
return checkPrimitives(item) ?
item :
typeof item === 'function' ?
cloneFunction(item) :
Array.isArray(item) ?
cloneArray(item) :
cloneObject(item);
};
scope.update = function(obj) {
const x = Object.keys(obj);
for(let i = 0; i < x.length; i++) {
_state[x[i]] = runUpdate(obj[x[i]]);
}
}
What we have done is write a simple clone system. Our update function will go through the keys and run the update. We will then check for various conditions, such as if we are a primitive type. If we are, we just copy the value, otherwise, we need to figure out the complex type we are. We first search to see whether we are a function; if we are, we just bind the value. If we are an array, we will run through all of the values and make sure that none of them are complex types. Finally, if we are an object, we will run through all of the keys and try to update these running the same checks.
However, we have just done what we have been avoiding; we have created an immutable state system. We can add more bells and whistles to this centralized state system, such as eventing, or we can implement a coding standard that has been around for quite some time, called Resource Allocation Is Initialization (RAII).
There is a really nice built-in web API called proxies. These are essentially systems where we are able to do something when something happens on an object. At the time of writing, these are still quite slow and should not really be used unless it is on an object that we are not worried about for time-sensitive activities. We are not going to talk about them extensively, but they are available for those readers who want to check them out.