做为前端开发者,你会感觉到JS中对象(Object)这个概念的强大。咱们说“JS中一切皆对象”。最核心的特性,例如从String,到数组,再到浏览器的APIs,“对象”这个概念无处不在。在这里你能够了解到JS Objects中的一切。javascript
同时,随着React的强势崛起,无论你有没有关注过这个框架,也必定据说过一个概念—不可变数据(immutable.js)。究竟什么是不可变数据?这篇文章会从JS源头—对象谈起,让你逐渐了解这个函数式编程里的重要概念。前端
JS中的对象是那么美妙:咱们能够随意复制他们,改变并删除他们的某项属性等。可是要记住一句话:java
“伴随着特权,随之而来的是更大的责任。”
(With great power comes great responsibility)jquery
的确,JS Objects里概念太多了,咱们切不可随意使用对象。下面,我就从基本对象提及,聊一聊不可变数据和JS的一切。git
这篇文章缘起于Daniel Leite在2017年3月16日的新鲜出炉文章:Things you should know about Objects and Immutability in JavaScript,我进行了大体翻译并进行大范围“改造”,同时改写了用到的例子,进行了大量更多的扩展。程序员
不可变数据实际上是函数式编程相关的重要概念。相对的,函数式编程中认为可变性是万恶之源。可是,为何会有这样的结论呢?github
这个问题可能不少程序员都会有。其实,若是你的代码逻辑可变,不要惊慌,这并非“政治错误”的。好比JS中的数组操做,很对都会对原数组进行直接改变,这固然并无什么问题。好比:编程
let arr = [1, 2, 3, 4, 5];
arr.splice(1, 1); // 返回[2];
console.log(arr); // [1, 3, 4, 5];复制代码
这是咱们经常使用的“删除数组某一项”的操做。好吧,他一点问题也没有。数组
问题其实出如今“滥用”可变性上,这样会给你的程序带来“反作用”。先没必要关心什么是“反作用”,他又是一个函数式编程的概念。浏览器
咱们先来看一下代码实例:
const student1 = {
school: 'Baidu',
name: 'HOU Ce',
birthdate: '1995-12-15',
}
const changeStudent = (student, newName, newBday) => {
const newStudent = student;
newStudent.name = newName;
newStudent.birthdate = newBday;
return newStudent;
}
const student2 = changeStudent(student1, 'YAN Haijing', '1990-11-10');
// both students will have the name properties
console.log(student1, student2);
// Object {school: "Baidu", name: "YAN Haijing", birthdate: "1990-11-10"}
// Object {school: "Baidu", name: "YAN Haijing", birthdate: "1990-11-10"}复制代码
咱们发现,尽管建立了一个新的对象student2,可是老的对象student1也被改动了。这是由于JS对象中的赋值是“引用赋值”,即在赋值过程当中,传递的是在内存中的引用(memory reference)。具体说就是“栈存储”和“堆存储”的问题。具体图我就不画了,理解不了能够单找我。
咱们说的“不可变”,实际上是指保持一个对象状态不变。这样作的好处是使得开发更加简单,可回溯,测试友好,减小了任何可能的反作用。
函数式编程认为:
只有纯的没有反作用的函数,才是合格的函数。
好吧,如今开始解释下“反作用”(Side effect):
在计算机科学中,函数反作用指当调用函数时,除了返回函数值以外,还对主调用函数产生附加的影响。例如修改全局变量(函数外的变量)或修改参数。
-维基百科
函数反作用会给程序设计带来没必要要的麻烦,给程序带来十分难以查找的错误,并下降程序的可读性。严格的函数式语言要求函数必须无反作用。
那么咱们避免反作用,建立不可变数据的主要实现思路就是:一次更新过程当中,不该该改变原有对象,只须要新建立一个对象用来承载新的数据状态。
咱们使用纯函数(pure functions)来实现不可变性。纯函数指无反作用的函数。
那么,具体怎么构造一个纯函数呢?咱们能够看一下代码实现,我对上例进行改造:
const student1 = {
school: "Baidu",
name: 'HOU Ce',
birthdate: '1995-12-15',
}
const changeStudent = (student, newName, newBday) => {
return {
...student, // 使用解构
name: newName, // 覆盖name属性
birthdate: newBday // 覆盖birthdate属性
}
}
const student2 = changeStudent(student1, 'YAN Haijing', '1990-11-10');
// both students will have the name properties
console.log(student1, student2);
// Object {school: "Baidu", name: "HOU Ce", birthdate: "1995-12-15"}
// Object {school: "Baidu", name: "YAN Haijing", birthdate: "1990-11-10"}复制代码
须要注意的是,我使用了ES6中的解构(destructuring)赋值。
这样,咱们达到了想要的效果:根据参数,产生了一个新对象,并正确赋值,最重要的就是并无改变原对象。
如今,咱们知道了“不可变”到底指的是什么。接下来,咱们就要分析一下纯函数应该如何实现,进而生产不可变数据。
其实建立不可变数据方式有不少,在使用原生JS的基础上,我推荐的方法是使用现有的Objects API和ES6当中的解构赋值(上例已经演示)。如今看一下Objects.assign的实现方式:
const student1 = {
school: "Baidu",
name: 'HOU Ce',
birthdate: '1995-12-15',
}
const changeStudent = (student, newName, newBday) => Object.assign({}, student, {name: newName, birthdate: newBday})
const student2 = changeStudent(student1, 'YAN Haijing', '1990-11-10');
// both students will have the name properties
console.log(student1, student2);
// Object {school: "Baidu", name: "HOU Ce", birthdate: "1995-12-15"};
// Object {school: "Baidu", name: "YAN Haijing", birthdate: "1990-11-10"};复制代码
一样,若是是处理数组相关的内容,咱们可使用:.map, .filter或者.reduce去达成目标。这些APIs的共同特色就是不会改变原数组,而是产生并返回一个新数组。这和纯函数的思想不谋而合。
可是,再说回来,使用Object.assign请务必注意如下几点:
1)他的复制,是将全部可枚举属性,复制到目标对象。换句话说,不可枚举属性是没法完成复制的。
2)对象中若是包含undefined和null类型内容,会报错。
3)最重要的一点:Object.assign方法实行的是浅拷贝,而不是深拷贝。
第三点很重要,也就是说,若是源对象某个属性的值是对象,那么目标对象拷贝获得的是这个属性对象的引用。这也就意味着,当对象存在嵌套时,仍是有问题的。好比下面代码:
const student1 = {
school: "Baidu",
name: 'HOU Ce',
birthdate: '1995-12-15',
friends: {
friend1: 'ZHAO Wenlin',
friend2: 'CHENG Wen'
}
}
const changeStudent = (student, newName, newBday, friends) => Object.assign({}, student, {name: newName, birthdate: newBday})
const student2 = changeStudent(student1, 'YAN Haijing', '1990-11-10');
// both students will have the name properties
console.log(student1, student2);
// Object {school: "Baidu", name: "HOU Ce", birthdate: "1995-12-15", friends: Object}
// Object {school: "Baidu", name: "YAN Haijing", birthdate: "1990-11-10", friends: Object}
student2.friends.friend1 = 'MA xiao';
console.log(student1.friends.friend1); // "MA xiao"复制代码
对student2 friends列表当中的friend1的修改,同时也影响了student1 friends列表当中的friend1。
以上,咱们分析了纯JS如何实现不可变数据。这样处理带来的一个负面影响在于:一些经典APIs都是shallow处理,好比上文提到的Object.assign就是典型的浅拷贝。若是遇到嵌套很深的结构,咱们就须要手动递归。这样作呢,又会存在性能上的问题。
好比我本身动手用递归实现一个深拷贝,须要考虑循环引用的“死环”问题,另外,当使用大规模数据结构时,性能劣势尽显无疑。咱们熟悉的jquery extends方法,某一版本(最新版本状况我不太了解)的实现是进行了三层拷贝,也没有达到完备的deep copy。
总之,实现不可变数据,咱们必然要关心性能问题。针对于此,我推荐一款已经“大名鼎鼎”的——immutable.js类库来处理不可变数据。
他的实现既保证了不可变性,又保证了性能大限度优化。原理颇有意思,下面这段话,我摘自camsong前辈的文章:
Immutable实现的原理是Persistent Data Structure(持久化数据结构),也就是使用旧数据建立新数据时,要保证旧数据同时可用且不变。
同时为了不deepCopy把全部节点都复制一遍带来的性能损耗,Immutable使用了Structural Sharing(结构共享),即若是对象树中一个节点发生变化,只修改这个节点和受它影响的父节点,其它节点则进行共享。
感兴趣的读者能够深刻研究下,这是颇有意思的。若是有须要,我也愿意再写一篇immutable.js源码分析。
咱们使用JavaScript操纵对象,这样的方式很简单便捷。可是,这样操控的基础是在JavaScript灵活机制的熟练掌握上。否则很容易使你“头大”。
在我开发的百度某部门私信项目中,由于使用了React+Redux技术栈,而且数据结构较为负责,因此我也采用了immutable.js实现。
最后,在前端开发中,函数式编程愈来愈热,而且在某种程度上已经取代了“过程式”编程和面向对象思想。
个人感想是在某些特定的场景下,不要畏惧变化,拥抱将来。
就像我很喜欢的葡萄牙诗人安德拉德一首诗中那样说的:
我一样不知道什么是海,
赤脚站在沙滩上,
急切地等待着黎明的到来。
Happy Coding!
PS:百度知识搜索部大前端继续招兵买马,高级工程师、实习生职位均有,产品、运营一样岗位多多。有意向者火速联系。。。