anjia / blog

博客,积累与沉淀
106 stars 4 forks source link

Immutable JavaScript #14

Open anjia opened 5 years ago

anjia commented 5 years ago

immutable 不可变的 immutability 不变性 mutating data 变异数据

编写 immutable 的 JS 代码,是个比较好的实践。已经有库 Immutable.js 可以帮我们实现这个特性了。这篇博客来聊下,在 ES6+ 里如何使用 Immutable 特性。

问题

为什么 immutability 很重要?因为,变异数据会让代码不易阅读,也容易引入bug。

对于基本类型(比如 number 和 string),书写 immutable 代码是非常简单的,因为基本类型自己不会变。指向基本类型的变量一直指向实际的值,如果你把它赋值给了另一个变量,那另一个变量是得到了该值的一个新拷贝。

对象(和数组)就不一样了,因为它们传的是引用。在这种情况下,新变量和原始变量是指向的同一个对象。不论你修改新变量还是原始变量,都会 mutate 该对象。先来感受下。

const person = {
  name: 'anjia',
  age: 18
};

const newPerson = person;

newPerson.age = 30;

console.log(newPerson === person);   // true
console.log(newPerson, person);  // age 都是30了

这就是问题的所在。当你修改了 newPerson,我们竟然也自动修改了旧对象 person。在大多数情况下,这是不希望的行为,也是不好的编码实践。

那下面,我们看看如何解决这个问题。

Immutable

对象

不传引用,我们可以创建一个全新的对象。

const person = {
  name: 'anjia',
  age: 18
};

const newPerson = Object.assign({}, person, {
  age: 30
});

console.log(newPerson === person);   // false
console.log(newPerson, person);  // newPerson 是30岁,person 还是18岁

Object.assign 是个 ES6 的新特性,它把所有对象合并到第一个。

这样,就保持了旧变量 person 的独立性和完整性,我们把它称之为 immutable。

在 ES6 里,我们有更简洁的写法。可以用 object spread 操作符 ...,这样:

const person = {
  name: 'anjia',
  age: 18
};

const newPerson = {
  ...person,
  age: 30
};

console.log(newPerson === person);   // false
console.log(newPerson, person);  // newPerson 是30岁,person 还是18岁。同上

那么,如何删除一个属性呢?当然,不能用 delete,因为它会 mutate 到原始值。我们可以这样:

const person = {
  name: 'anjia',
  gender: 'female',
  age: 18
};

const property = 'age';   // 删除age属性

const newPerson = Object.keys(person).reduce((obj, key) => {
  if(key !== property){
    return {...obj, [key]: person[key]}
  }
  return obj
}, {});

console.log(newPerson === person);   // false
console.log(newPerson); // 只有 name 和 gender
console.log(person);    // 有 name, gender, age

呃,好吧,删除的话我们需要自己写整个逻辑代码。你可以把它封装成一个公共方法。

数组

下面的例子,以 mutating 的方式向数组里添加新项。

const names = ['an', 'jia'];

const newNames = names;
newNames.push('zora');

console.log(newNames === names);  // true
console.log(newNames, names);   // 都是 ["an", "jia", "zora"]

解决办法,思路同上。

const names = ['an', 'jia'];

const newNames = [...names, 'zora'];

console.log(newNames === names);  // false
console.log(newNames);   // ["an", "jia", "zora"]
console.log(names);      // ["an", "jia"]

这样我们就创建了一个新数组newNames,而且还能让老数组names保持自身的独立性和完整性。

关于数组的,还有一些方法也能非常方便地生成新数组,而不影响老值。比如 map filter 等。详见之前的一篇博客数组遍历。看代码(点进去链接看看),有没有觉得写 immutable 的代码更方便了。再配合着箭头函数,就更简洁了。它们每次都返回一个全新的数组。

当然,有一个例外就是sort()

const names = ['zora', 'anjia', 'an', 'jia'];

const newNames = names.sort();

console.log(newNames === names);  // true
console.log(newNames);   // 都输出 ["an", "anjia", "jia", "zora"]
console.log(names);

有没有解决办法呢?有,如下:

const names = ['zora', 'anjia', 'an', 'jia'];

const newNames = names.slice().sort();   // 利用 slice(),虽然有点 hacky

console.log(newNames === names);  // false
console.log(newNames);   // ["an", "anjia", "jia", "zora"]
console.log(names);      // ["zora", "anjia", "an", "jia"] 原值

如上,现代JS,可以让我们轻松实现 immutability。良好的编码,可以避免让JS变得不可预测。

性能

每次都创建新对象会耗费时间和内存哦?嗯~是的,它会带来一点开销。但是,与它带来的优势来比,这点缺点可以忽略了。

REF:https://wecodetheweb.com/2016/02/12/immutable-javascript-using-es6-and-beyond/