6 min read

JavaScript的不可变性

理解和应用不可变性概念对于编写高效、健壮和易于维护的代码至关重要。
JavaScript的不可变性

不可变性

不可变性是指一旦创建了一个值,它就不能被修改。在JavaScript中,原始类型(如字符串、数字和布尔值)是不可变的,而对象和数组则是可变的。

为什么

更改数据会产生难以读取且容易出错的代码。

首先,为什么不变性很重要? 理解和应用不可变性的重要性有以下几个方面:

避免副作用

可变性是引发许多编程问题的根源之一。当数据可以被修改时,很难追踪和理解程序中发生的副作用。通过使用不可变数据,可以降低代码的复杂性和错误的可能性。

提高性能

JavaScript中的不可变性可以使引擎进行更好的优化。由于不可变值在内存中不变,可以更轻松地进行缓存和重用,从而提高代码的执行效率。

支持时间旅行调试

不可变性使得跟踪和调试代码变得更加容易。通过保留历史版本的不可变数据,我们可以在代码中自由地回溯和比较不同时间点的状态,更轻松地定位和修复错误。

像Redux和NGRX类库, 其受欢迎程度激增。他们共同的要求是一个不可变的状态。  
应用程序的状态是按顺序应用于初始状态的操作列表的结果。  
应用程序的每个状态都是不可更改的, 新操作使用现有状态计算新操作。  
这有助于我们通过可变操作避免意外状态更改.

函数式编程

函数式编程鼓励使用不可变数据和纯函数来构建应用程序。通过将不可变性视为核心原则,函数式编程能够降低代码复杂性、增加可测试性,并提供更好的并发性。

总结: 编写不可变的 Javascript 代码是一种好的做法. 不可变性编程与相关编程思想的结合,可以产生更具表达力、可维护性和并发性的代码。通过遵循不可变性原则,我们可以减少副作用、降低代码复杂性,并在多线程或并行环境中更轻松地处理数据共享和状态管理。

应用示例

数组

数组有几个可变操作, 这里回顾其用法与了解新方法

  • push pop
  • shift unshift
  • splice reverse sort

push (从尾推进)

// mutable
const fruits = ['orange', 'apple', 'lemon'];
fruits.push('banana');                                  
console.log(fruits)
// immutable
const fruits  = ['orange', 'apple', 'lemon']
const newFruits = [...fruits, 'banana']   
console.log(newFruits)

pop (从尾拿出)


// mutable
const fruits    = ['orange', 'apple', 'lemon', 'banana'];
const lastFruit = fruits.pop();                            
console.log(fruits)
// immutable
const fruits    = ['orange', 'apple', 'lemon', 'banana'];
const lastFruit = fruits[fruits.length - 1]; // 'banana'
const newFruits = fruits.slice(0, fruits.length - 1);
console.log(newFruits)

unshift (从头推进)

// mutable
const fruits = ['orange', 'apple', 'lemon'];
fruits.unshift('banana');    
console.log(fruits)
// immutable
const fruits    = ['orange', 'apple', 'lemon'];
const newFruits = ['banana', ...fruits];   
console.log(newFruits)

shift (从头拿出)


// mutable
const fruits        = ['orange', 'apple', 'lemon', 'banana'];
const firstFruit    = fruits.shift();         
console.log(firstFruit)
// immutable
const fruits        = ['orange', 'apple', 'lemon', 'banana'];
const firstFruit    = fruits[0]; 
const newFruits     = fruits.slice(1); 
console.log(newFruits)

splice slice (删除项目)

常用`splice`, 不可变用 `slice + spread`

// mutable
// 位置1,删除2个元素, 该位置增加 'strawberry'
const fruits = ['orange', 'apple', 'lemon', 'banana'];
fruits.splice(1, 2, 'strawberry');
console.log(fruits)
// immutable - slice spread
const fruits = ['orange', 'apple', 'lemon', 'banana'];
const newFruits = [...fruits.slice(0, 1), 'strawberry', ...fruits.slice(3)];
console.log(newFruits)

sort reverse (排序与反转)

// mutable
const fruits = ['orange', 'apple', 'lemon', 'banana'];
fruits.sort();
console.log(fruits)
fruits.reverse(); 
console.log(fruits)
// immutable
const fruits = ['orange', 'apple', 'lemon', 'banana'];
const sorted = [...fruits].sort();
console.log(sorted)
const inverted = [...fruits].reverse();
console.log(inverted)
const sortedAndInverted = [...sorted].reverse();
console.log(sortedAndInverted)

对象

修改/添加属性

const state = {
selected: 'apple',
quantity: 13,
fruits: ['orange', 'apple', 'lemon', 'banana']
};
// mutable
state.selected = 'orange';
state.quantity = 5;
state.origin = 'imported from Spain';
/*
state = {
selected: 'orange',
quantity: 5,
fruits: ['orange', 'apple', 'lemon', 'banana'],
origin: 'imported from Spain'
}
*/
console.log(state)
// immutable
const state = {
   selected: 'apple',
   quantity: 13,
   fruits: ['orange', 'apple', 'lemon', 'banana']
};
const newState = {
...state,
selected: 'orange',
quantity: 5,
origin: 'imported from Spain'
};
/*
newState = {
fruits: ['orange', 'apple', 'lemon', 'banana'],
selected: 'orange',
quantity: 5,
origin: 'imported from Spain'
}
*/
console.log(newState)

删除属性

// mutable 只需调用 delete
const state = {
selected: 'apple',
quantity: 13,
fruits: ['orange', 'apple', 'lemon', 'banana']
};
delete state.quantity;
/*
state = {
selected: 'apple',
fruits: ['orange', 'apple', 'lemon', 'banana']
}
*/
console.log(state)
const state = {
selected: 'apple',
quantity: 13,
fruits: ['orange', 'apple', 'lemon', 'banana']
};
// immutable (rest + spread) 提取属性
const { quantity, ...newState } = state;
/*
quantity = 13
newState = {
selected: 'apple',
fruits: ['orange', 'apple', 'lemon', 'banana']
}
*/
console.log(newState)

复杂结构

当具有嵌套数组或对象时.

const state = {
   selected: 4,
   gang: [
       'Mike',
       'Dustin',
       'Lucas',
       'Will',
       'Jane'
   ]
};
// mutable
// 对复杂结构执行操作会使结构的较浅(第一级)复制.
// 在这里,它只复制对数组的引用,而不是实际数组。
const newState = { ...state };
newState.selected = 11;
newState.gang.push('Max');
newState.gang.push('Suzie');
/*
state = {
   selected: 4,
   gang: [
       'Mike',
       'Dustin',
       'Lucas',
       'Will',
       'Jane'
       'Max',
       'Suzie'
   ]
}
newState = {
   selected: 11,
   gang: [
       'Mike',
       'Dustin',
       'Lucas',
       'Will',
       'Jane'
       'Max',
       'Suzie'
   ]
}
*/
console.log(state.gang === newState.gang) //true

// immutable
// 向数组添加新元素对 和 . 都有影响。要解决这个问题,我们需要单独分布数组。
const state = {
   selected: 4,
   gang: [
       { id: 1, name: 'Mike' },
       { id: 2, name: 'Dustin' },
       { id: 3, name: 'Lucas' },
       { id: 4, name: 'Will' },
       { id: 11, name: 'Jane' }
       ]
}
const newState = {
   selected: 11,
   gang: [...state.gang]
}
newState.gang[4].name = 'Eleven';
/*
state = {
   selected: 4,
   gang: [
       { id: 1, name: 'Mike' },
       { id: 2, name: 'Dustin' },
       { id: 3, name: 'Lucas' },
       { id: 4, name: 'Will' },
       { id: 11, name: 'Eleven' }
   ]
}
newState = {
   selected: 11,
   gang: [
       { id: 1, name: 'Mike' },
       { id: 2, name: 'Dustin' },
       { id: 3, name: 'Lucas' },
       { id: 4, name: 'Will' },
       { id: 11, name: 'Eleven' }
   ]
}
*/
console.log(state.gang === newState.gang) //false

关于性能的一句话
性能如何,创建新对象的时间和内存不费时吗?  
嗯,真的,它附带多一点开销。但与优势相比,这种劣势非常小。

Javascript 中更复杂的操作之一是跟踪对象是否更改。

这样的解决方案相当沉重。
但是,如果保持状态不变,您可以只依靠检查状态是否更改,这就小了许多CPU要求  

第二大优势是代码质量。  

确保状态不变会迫使您更好地考虑应用程序结构。它鼓励以更实用的方式编程,使代码易于遵循,并减少了讨厌错误的可能性。

参考:
https://dev.to/glebec/four-ways-to-immutability-in-javascript-3b3l