Building software by creating pure functions, avoid shared state, mutable data, and side-effects.
application state flows through pure functions.
Side Effects and Pure Functions
Notice the data is the array of users.
We have our mutable function that literally uses scope to access users and change its contents.
For the pure functions we take in parameters, and we do not modify these inputs. Instead, we return new arrays with shallow copied data. Notice we never touch param arr, nor name. We simply get the data we need, and return new arrays.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
var users = [{name: "James",score: 30,tries: 1}, {name: "Mary", score: 110,tries: 4}, {name: "Henry",score: 80,tries: 3}]; //Mutable Functions var recordData = function(arr, prop) { users.forEach(function(val, i, a) { if (val.name.toLowerCase() === arr[0].toLowerCase()) { a[i][prop] = arr[1]; } }); }; //Pure Functions var getScore = function(arr, name) { let score; for (let i = 0; i < arr.length; i++) { if (arr[i].name.toLowerCase() === name.toLowerCase()) { score = arr[i].score; break; } }; return [name, score]; }; var getTries = function(arr, name) { let tries; for (let i = 0; i < arr.length; i++) { if (arr[i].name.toLowerCase() === name.toLowerCase()) { tries = arr[i].tries; break; } }; return [name, tries]; }; var updateScore = function(arr, amt) { let newAmt = arr[1] + amt; return [arr[0], newAmt]; }; var updateTries = function(arr) { let newTries = arr[1] + 1; return [arr[0], newTries]; }; |
We then use the pure functions to calculate new scores.
Then give it to our mutable functions to update our data.
1 2 3 |
let newScore = updateScore(getScore(users, "Henry"), 30); recordData(newScore, "score"); recordData(updateTries(getTries(users, "Henry")),"tries"); |
Avoid Shared State
Javascript stores data in objects and variables. This is called state.
Notice how users are shared via closure scope. We need to void this by passing state (users)
from one function to another.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
var MAINAPP = (function(nsp) { var currentUser = 0, users = [{name: "James",score: 30,tries: 1}, {name: "Mary", score: 110,tries: 4}, {name: "Henry",score: 80,tries: 3}]; var updateScore = function(newAmt) { users[currentUser].score += newAmt; }; var returnUsers = function() { return users; }; var updateTries = function() { users[currentUser].tries++; }; var updateUser = function(newUser) { currentUser = newUser; }; nsp.updateUser = updateUser; nsp.updateTries = updateTries; nsp.updateScore = updateScore; nsp.returnUsers = returnUsers; return nsp; })(MAINAPP || {}); setTimeout(function() {MAINAPP.updateUser(2);}, 300); setTimeout(function() {MAINAPP.updateScore(20);}, 100); setTimeout(function() {MAINAPP.updateTries();}, 200); |
Mutation
1 2 3 4 5 6 7 8 9 10 11 12 13 |
"use strict"; const arr = [3,4,2,5,1,6]; Object.freeze(arr); const sortArray = function(arr1) { return arr1.sort(); // throw error here because we have freezed the arr object. }; const newNums = sortArray(arr); console.log(newNums); console.log(arr); |
how to avoid mutating? Clone it
1 2 3 4 5 6 7 8 |
let obj = { fName: "Steven", lName: "Hancock", score: 85, completion: true, }; let obj2 = Object.assign({}, obj) |
but…for objects with nested data:
1 2 3 4 5 6 7 8 9 10 |
let obj = { fName: "Steven", lName: "Hancock", score: 85, completion: true, questions: { q1: {success: true, value: 1}, q2: {success: false, value: 1} } }; |
we need to convert it to string, then parse it back into an object.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
const cloneObj = function(obj) { return JSON.parse(JSON.stringify(obj)); // deep clones nested object }; let obj = { fName: "Steven", lName: "Hancock", score: 85, completion: true, questions: { q1: {success: true, value: 1}, q2: {success: false, value: 1} } }; const newObj = cloneObj(obj); newObj.questions.q1 = {success: false, value: 0}; console.log(obj); console.log(newObj); |
We can do the same with arrays:
1 2 3 4 5 6 7 8 9 10 11 12 |
"use strict"; const arr = [3,4,2,5,1,6]; Object.freeze(arr); const cloneObj = function(obj) { return JSON.parse(JSON.stringify(obj)); }; const newNums = cloneObj(arr).sort(); console.log(newNums); // [1,2,3,4,5,6] console.log(arr); // [3,4,2,5,1,6] |
Object.assign and spread operator works the same way.
For Arrays, spreading a null would give an error
1 2 |
const z = null; [...z]; |
For Objects, both silently exclude the null value with no error.
1 2 3 4 |
const x = null; const y = {a: 1, b: 2}; const z = {...x, ...y}; console.log(z) // {a: 1, b: 2} |
1 2 3 4 |
const x = null; const y = {a: 1, y:2}; const z = Object.assign({}, x, y); console.log(z); // {a: 1, b: 2} |