理解 JS 深浅拷贝
前言
JS 变量两种形式:
premitive 字面量 字面量类型变量存放在栈中,存储的是他的值
reference 引用 引用类型变量存储的是一个内存地址,真实结构在堆中
- 浅拷贝是拷贝了内存中的数据
- 深拷贝是沿着引用类型变量的真实内存地址,去进行深度遍历,遍历所有字面量
JS数据结构
-
栈 heap » 先进后出 后进先出
- 堆 stack » 树状结构
- 以键值对[key-value]的方式存储和读取,无序的
- 队列 queue » 先进先出(FIFO)
- 队列数据结构主要应用在事件循环机制(Event Loop)
浅拷贝的实现
- JS 实现了一些拥有浅拷贝功能的接口,如解构赋值 rest、Object.assgin
- 缺陷在于进行拷贝后,如果改变了被拷贝目标的某个引用属性值,拷贝结构对应也发生改变
- 本质:两者引用了同一个内存区域,区域发生变动,值也发生变化
深度拷贝的实现
- JS 中可以利用 JSON 序列化、反序列化接口进行深拷贝
缺陷:拷贝失真
- 值为 undefined、函数、Symbol 属性,或者为 Symbol 字符串属性,拷贝后会丢失
- 值为 NaN 属性,拷贝后转为了 null
- 值为非标准对象 Object,如 Set、Map、Error、RegExp 等等属性或数组元素,拷贝后。 值转为了空的标准对象,丢失了原来的原型继承关系
- 值为 undefined、NaN、函数的数组元素,拷贝后值转为了 null
拷贝功能缺陷
- 原型链丢失
- 无法拷贝由循环应用的对象
实现深拷贝
追寻深拷贝的实现方式,可以理解为:浅拷贝+深度遍历+特殊情况容错
实现一个深拷贝函数 deepCopy。假定函数接受的输入为 o
// 深拷贝函数
function deepCopy(o) {
}
字面量浅拷贝返回,否则深度遍历
// 深拷贝函数
function deepCopy(o) {
// 如果是字面量,直接返回
if(isPrimitive(o)) return o;
// 否则,进行深度遍历
/**
* 深度遍历代码
*/
}
// 字面量判断
function isPrimitive(o) {
if (typeof o !== 'function' && typeof o !== 'object') return true;
if (o === null) return true;
return false;
}
- 深度遍历有两种选择,一个是递归,一个 while 循环
- 递归遍历有个缺陷,大量函数栈帧入栈,很容易导致内存不足而爆栈,特别是对有循环引入关系的输入
- 采用 while 循环,可以模拟一个栈结构
- 栈如果为空,则结束循环
- 若不为空,则进行循环
- 循环第一步,先出栈,然后处理数据
- 处理完后进入下一次循环判读
- 在 JS 里,模拟栈结构可以用数组,push 和 pop 组合实现后入先出
- 在数据结构与算法里,这叫深度遍历
// 深拷贝函数
function deepCopy(o) {
// 如果是字面量,直接返回; 否则,进行深度遍历
if(isPrimitive(o)) return o;
// 首先,先定义一个观察者,用来记录遍历的结果。等到遍历结束,这个观察者就是深拷贝的结果。
const observer = {};
// 然后,用数组模拟一个栈结构
const nodeList = [];
// 其次,为了每次遍历时能地做一些处理,入栈的数据用对象来表示比较合适。
nodeList.push({
key: null, // 这里,增加一个key属性,用来关联每次遍历所要处理的数据。
});
// 循环遍历
while(nodeList.length > 0) {
const node = nodeList.pop(); // 出栈,深度优先
// 处理节点node
}
}
接下来,就是处理节点 node 了。这里要处理的任务,主要有:
- 特殊情况处理,比如 Symbol 类型的属性虽然无法被 Object.keys 迭代出来,但可以用 Reflect.ownKeys 来解决。又比如,针对循环引用,可以在循环外面建立哈希表,每次循环都判断要处理的输入是否已存在哈希表,如果存在,直接引用,否则,存入哈希表。
// 用WeakMap模拟的哈希表,它的弱引用特性可以避免内存泄露
const hashmap = new WeakMap();
// 遍历包括Symbol类型在内的所有属性
const keys = Reflect.ownKeys(node.value);
- 初始化,将输入 o 挂载到节点里,并存入哈希表。
// 初始化
if (node.key === null) {
node.value = o;
node.observer = observer
// 存入哈希表
hashmap.set(node.value, node.observer)
}
- 对节点的属性进行遍历,属性值为引用类型,将它压入栈,否则,观察者利用关联的 key 记录属性值,然后进入下一次循环。
for (let i = 0; i < keys.length; i++) {
key = keys[i];
value = node.value[key];
// 是字面量,直接记录
if (isPrimitive(value)) {
node.observer[key] = value;
continue;
}
// 否则,入栈
nodeList.push({
key,
value,
observer: node.observer
})
}
- 每次对节点属性进行遍历前,先根据哈希表进行判断
// 查询哈希表,如果不存在对象key,就存入哈希表
if (!hashmap.has(node.value)) {
hashmap.set(node.value, node.observer[node.key] = isArray(node.value) ? [] : {});
// 将对象压入栈
nodeList.push({
key: node.key,
value: node.value,
observer: node.observer[node.key]
})
continue;
}
// 存在哈希表里,则从哈希表里取出,赋值
else if (node.observer !== hashmap.get(node.value)) {
node.observer[node.key] = hashmap.get(node.value)
continue;
}
这里,补上 isArray 函数,用来判断是否为数组
function isArray(o) {
return Object.prototype.toString.call(o) === '[object Array]';
}
到此,深拷贝函数已经成型了。但,还不够完善,因为还没有对输入是函数的情况做处理。
所以,添加两个函数,一个判断是否是函数,一个用例拷贝函数。
// 判断函数
function isFunction(o) {
return Object.prototype.toString.call(o) === '[object Function]';
}
// 拷贝函数
function copyFunction(fnc) {
const f = eval(`(${fnc.toString()})`)
Object.setPrototypeOf(f, Object.getPrototypeOf(fnc))
Object.keys(fnc).map(key => f[key] = deepCopy(fnc[key]))
return f;
}
循环遍历之前,加一层对函数的判断
// 是函数,则拷贝函数
if (isFunction(o)) return copyFunction(o);
遍历的时候,也要加一层对函数的判断
// 函数直接赋值
else if (isFunction(node.value)) {
node.observer[node.key] = copyFunction(node.value)
continue;
}
循环结束后,我们还要对原型链进行处理,深拷贝,不能把继承关系给弄丢,这也是输入无论是数组还是对象都能获得正确拷贝结果的一个技巧
// 继承原型
Object.setPrototypeOf(observer, Object.getPrototypeOf(o))
最后,返回观察者对象,即深拷贝结果。
// 返回深拷贝结果
return observer;
完整代码
// 判断字面量
function isPrimitive(o) {
if (typeof o !== 'function' && typeof o !== 'object') return true;
if (o === null) return true;
return false;
}
// 判断数组
function isArray(o) {
return Object.prototype.toString.call(o) === '[object Array]';
}
// 判断函数
function isFunction(o) {
return Object.prototype.toString.call(o) === '[object Function]';
}
// 拷贝函数
function copyFunction(fnc) {
const f = eval(`(${fnc.toString()})`)
Object.setPrototypeOf(f, Object.getPrototypeOf(fnc))
Object.keys(fnc).map(key => f[key] = deepCopy(fnc[key]))
return f;
}
// 哈希表
const hashmap = new WeakMap();
// 深拷贝函数
function deepCopy(o) {
// 如果是字面量,直接返回; 否则,进行深度遍历
if (isPrimitive(o)) return o;
// 是函数,则拷贝函数
if (isFunction(o)) return copyFunction(o);
// 首先,先定义一个观察者,用来记录遍历的结果。等到遍历结束,这个观察者就是深拷贝的结果。
const observer = {};
// 然后,用数组模拟一个栈结构
const nodeList = [];
// 其次,为了每次遍历时能地做一些处理,入栈的数据用对象来表示比较合适。
nodeList.push({
key: null, // 这里,增加一个key属性,用来关联每次遍历所要处理的数据。
});
// 提升变量,尽量少在循环里创建变量
let node;
let keys;
let key;
let value;
// 循环遍历
while (nodeList.length > 0) {
node = nodeList.pop(); // 出栈,深度优先
// 处理节点node
// 初始化
if (node.key === null) {
node.value = o;
node.observer = observer
// 存入哈希表
hashmap.set(node.value, node.observer)
}
// 是函数,则直接记录
else if (isFunction(node.value)) {
node.observer[node.key] = copyFunction(node.value)
continue;
}
// 查询哈希表,如果不存在对象key,就存入哈希表
else if (!hashmap.has(node.value)) {
hashmap.set(node.value, node.observer[node.key] = isArray(node.value) ? [] : {});
// 是对象,入栈
nodeList.push({
key: node.key,
value: node.value,
observer: node.observer[node.key]
})
continue;
}
// 存在哈希表里,则从哈希表里取出,赋值
else if (node.observer !== hashmap.get(node.value)) {
node.observer[node.key] = hashmap.get(node.value)
continue;
}
// 遍历包括Symbol类型的所有属性
keys = Reflect.ownKeys(node.value);
for (let i = 0; i < keys.length; i++) {
key = keys[i];
value = node.value[key];
// 是字面量,直接存储
if (isPrimitive(value)) {
node.observer[key] = value;
continue;
}
// 否则,入栈
nodeList.push({
key,
value,
observer: node.observer
})
}
}
// 继承原型
Object.setPrototypeOf(observer, Object.getPrototypeOf(o))
// 返回深拷贝结果
return observer;
}
转载
参见