WeakMap
提供的接口与 Map
相同,但是它们有以下的区别:
Map
Map
对象保存键值对,并且能够记住键的原始插入顺序。任何值(对象或者基本类型)都可以作为一个键或一个值。
创建和设置对象属性
下面展示创建 Map
和它的方法的使用:
// 创建
const users = new Map();
// String 作为 key
users.set("John", { address: "John's Address" });
// Object 作为 key
const obj = { name: "Michael" };
users.set(obj, { address: "Michael's Address"});
// Function 作为 key
const func = () => "Andrew";
users.set(func, { address: "Andrew's Address"});
// NaN 作为 key
users.set(NaN, { address: "NaN's Address"});
一些方法的使用如下:
const contacts = new Map();
contacts.set("Jessie", { phone: "213-555-1234", address: "123 N 1st Ave" });
contacts.has("Jessie"); // true
contacts.get("Hilary"); // undefined
contacts.delete("Raymond"); // false
console.log(contacts.size); // 1
Map vs Object
Object
和 Map
类似的是,它们都允许你按键存取一个值、删除键、检测一个键是否绑定了值。因此(并且也没有其他内建的替代方式了)过去我们一直都把对象当成 Map 使用。
不过 Map
和 Object
有一些重要的区别,在下列情况中使用 Map 会是更好的选择:
WeapMap
WeakMap
对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意的。
创建和设置对象属性
设置对象
const test = new WeakMap()
const o1 = {};
test.set(o1, {name: 'test'});
获取对象
const test = new WeakMap();
const o1 = function () {};
test.set(o1, { name: "test" });
test.get(o1);
删除和查询对象
const test = new WeakMap();
const o1 = function () {};
test.set(o1, { name: "test" });
test.has(o1); // true
test.delete(o1);
test.has(o1); // false
应用
WeakMap
对象的一个用例是存储一个对象的私有数据或隐藏实施细节。
const privates = new WeakMap();
function Public() {
const me = {
// 私有数据
};
privates.set(this, me);
}
Public.prototype.method = function () {
const me = privates.get(this);
// ...
};
module.exports = Public;
总结
Map
和 WeakMap
的功能以及方法(都有 get()
、 set()
、has()
、delete()
)是一样的,它们区别有以下四个方面:
size
属性小练习
Map
和 Object
有什么区别?
答案:见正文。
深拷贝和浅拷贝都是针对数据类型是 Object
而言,因为对象是引用类型,如果要拷贝一个副本,副本中的对象更改后,不影响到原对象(或修改原对象不影响副本),即为深拷贝,否则,为浅拷贝。
这是因为,计算机存储数据有两种内存:stack
和 heap
:
stack
是一个临时存储空间,存放的是原始值变量和对象的引用。heap
存储全局变量,对象的值都是存在 heap
中,stack
中保存的是对象的引用。下图可以帮助更好理解:
这里将介绍 5 种浅拷贝的方法和 4 种深拷贝的方法。
浅拷贝(shallow copy)
浅拷贝对原对象或副本的更改可能也会导致其他对象的更改。它实际上只拷贝了一层,并且只当数组和对象包的值是原始值时才进行拷贝。
赋值运算符 =
如果将一个对象赋值给另一个对象,也是浅拷贝。
const array = [1, 2, 3];
const copyWithEquals = array;
console.log(copyWithEquals === array); // true
扩展运算符 ...
扩展运算符 ...
可以很方便地浅拷贝对象和数组:
const array = [1, 2, 3];
const copyWithEquals = array;
console.log(copyWithEquals === array); // true
const copyWithSpread = [...array];
console.log(copyWithEquals === array); // true
.slice()
数组可以使用内置的方法 .slice()
,它的作用和扩展运算符一样,都可以实现浅拷贝:
const array = [1, 2, 3];
const copyWithSlice = array.slice();
console.log(copyWithSlice === array); // false
// 改变原数组 array
array[0] = 4;
console.log(array); // [4, 2, 3] 原数组改变了
console.log(copyWithSlice); // [1, 2, 3] 拷贝的数组没有受到影响
.assign()
使用 Object.assign()
也可以实现对象和数组的浅拷贝:
const array = [1, 2, 3];
const copyWithEquals = array; // 没有拷贝数组
const copyWithAssign = [];
Object.assign(copyWithAssign, array);
array[0] = 4;
console.log(array); // [4, 2, 3] 原数组改变了
console.log(copyWithAssign); // [1, 2, 3] 拷贝的数组没有受到影响
Array.from()
Array.from() 方法对一个类似数组或可迭代对象创建一个新的,浅拷贝的数组实例。
const array = [1, 2, 3];
const copyWithEquals = array; // 没有拷贝数组
const copyWithArrayFrom = Array.from(array);
array[0] = 4;
console.log(array); // [4, 2, 3] 原数组改变了
console.log(copyWithArrayFrom); // [1, 2, 3] 拷贝的数组没有受到影响
深拷贝(deep copy)
JavaScript 对象和数组如果深度嵌套,浅拷贝只能实现第一层的拷贝,但是深度的值依然和原对象共享引用。
const nestedArray = [[1], [2], [3]];
const nestedCopyWithSpread = [...nestedArray];
nestedArray[0][0] = '4'; // 注意这里
console.log(nestedArray); // [[4], [2], [3]] 原数组被修改了
console.log(nestedCopyWithSpread); // [[4], [2], [3]] 拷贝的数组值也被修改了
我们希望的深拷贝是无论是嵌套多少层数组和对象,拷贝的数组、对象和原来的数组、对象互不受影响。
如果数组和对象中还包含其他数组和对象,拷贝这些对象、数组就需要使用深拷贝,否则,改变嵌套的引用将会影响到嵌套的原数组和对象。
深拷贝和浅拷贝的区别在于,浅拷贝对于数组和对象仅仅包含原始值时表现良好,但是数组和对象中嵌套其他数组和对象便不能正确拷贝。
===
可以帮助我们理解深浅拷贝:
const nestedArray = [[1], [2], [3]];
const nestedCopyWithSpread = [...nestedArray];
console.log(nestedArray[0] === nestedCopyWithSpread[0]); // true
const nestedCopyWithHack = JSON.parse(JSON.stringify(nestedArray)); // 使用 JSON.stringify() 进行深拷贝
console.log(nestedArray[0] === nestedCopyWithHack[0]); // false 深拷贝之后两个对象用 === 比较不相等
第三方库:lodash
、rfdc
深拷贝可以使用 lodash
库,使用方法如下:
import _ from "lodash"
const nestedArray = [[1], [2], [3]];
const shallowCopyWithLodashClone = _.clone(nestedArray);
const deepCopyWithLodashClone = _.cloneDeep(nestedArray);
rfdc 库也可以实现深拷贝,它的优点是速度快:
const clone = require('rfdc')() //
clone({a: 37, b: {c: 3700}}) // {a: 37, b: {c: 3700}}
JSON.parse/stringify
JSON.parse(JSON.stringify(object))
也可以实现深拷贝,但是不同类型的值表现各有区别。因此,不建议使用。
// 仅仅下面的数据类型支持使用 JSON.parse()、JSON.stringify()
const sampleObject = {
string: 'string',
number: 123,
boolean: false,
null: null,
notANumber: NaN, // NaN 会被忽略
date: new Date('1999-12-31T23:59:59'), // Date 将被转成字符串
undefined: undefined, // Undefined 会被忽略
infinity: Infinity, // Infinity 会被忽略
regExp: /.*/, // RegExp 会被忽略
}
console.log(sampleObject) // Object { string: "string", number: 123, boolean: false, null: null, notANumber: NaN, date: Date Fri Dec 31 1999 23:59:59 GMT-0500 (Eastern Standard Time), undefined: undefined, infinity: Infinity, regExp: /.*/ }
console.log(typeof sampleObject.date) // object
const faultyClone = JSON.parse(JSON.stringify(sampleObject))
console.log(faultyClone) // Object { string: "string", number: 123, boolean: false, null: null, notANumber: null, date: "2000-01-01T04:59:59.000Z", infinity: null, regExp: {} }
console.log(typeof faultyClone.date) // string
structuredClone
MDN 定义如下:
全局的 structuredClone() 方法使用结构化克隆算法将给定的值进行深拷贝
structuredClone
将返回一个原对象的深拷贝,它的语法如下:
structuredClone(value, options)
value
为需要进行深拷贝的对象options
可选const nestedArray = [[1], [2], [3]];
const nestedCopyWithStructuredClone = structuredClone(nestedArray); // 深拷贝
console.log(nestedArray === nestedCopyWithStructuredClone); // false
nestedArray[0][0] = 4;
console.log(nestedArray); // [[4], [2], [3]] 修改了原对象
console.log(nestedCopyWithStructuredClone); // [[1], [2], [3]] 复制对象不受影响
注意:structuredClone()
是 ECMAScript 的一部分,虽然被大部分浏览器支持,但是还有部分浏览器不可用(在 360 浏览器中报 undefined
)。
自定义函数实现深拷贝
我们可以手写一版深拷贝,顺便巩固一下对深拷贝的理解。
简单版:处理对象和数组
实现深拷贝的关键就是使用递归复制原对象(或数组)的键和值,判断嵌套的值是否是引用类型(如下代码中的对象或数组),如果是,则继续递归,直到值是原始值为止。
const deepCopy = (obj) => {
// 如果不是对象或者为 null,直接返回
if (typeof obj !== "object" || obj === null) {
return obj;
}
// 创建一个新对象或数组
const newObj = Array.isArray(obj) ? [] : {};
Object.keys(obj).forEach((key) => {
newObj[key] = deepCopy(obj[key]);
});
return newObj;
};
// 测试对象和数组的深拷贝:
const nestedArray = [[1], [2], [3]];
const nestedCopyWithStructuredClone = deepCopy(nestedArray); // 深拷贝
console.log(nestedArray === nestedCopyWithStructuredClone); // false
nestedArray[0][0] = 4;
console.log(nestedArray); // [[4], [2], [3]] 修改了原对象
console.log(nestedCopyWithStructuredClone); // [[1], [2], [3]] 复制对象不受影响
注意:我们需要判断单独判断 null
的类型,因为 typeof null
的结果是 "object"
。
进阶版:处理循环引用
简单版只处理了普通的数组和对象,然而,如果遇到循环引用,上面的代码就不能得到正确的结果了。我们先看一个循环引用的例子:
const nestedObject = {
name: 'Lily'
}
nestedObject.nestedObject = nestedObject; // nestedObject 有个 nestedObject 属性指向 nestedObject,即:自己引用自己
如果使用简单版的深拷贝,会得到下面的报错:
解决循环引用导致的栈内存溢出,我们只需要使用 WeakMap
记录已经复制过的对象,如下面代码中的 cache
,如果已经复制过该对象,则直接返回缓存中的对象,避免了无限递归的问题。
const deepCopy = (obj, cache = new WeakMap()) => {
// 如果不是对象或者为 null,直接返回
if (typeof obj !== "object" || obj === null) {
return obj;
}
// 如果已经复制过该对象,则直接返回
if (cache.has(obj)) {
return cache.get(obj);
}
// 创建一个新对象或数组
const newObj = Array.isArray(obj) ? [] : {};
// 将新对象放入 cache 中
cache.set(obj, newObj);
// 处理循环引用的情况
Object.keys(obj).forEach((key) => {
newObj[key] = deepCopy(obj[key], cache);
});
return newObj;
};
// 测试有循环引用的深拷贝:
const nestedObject = {
name: 'Lily'
}
nestedObject.nestedObject = nestedObject;
const nestedCopyWithStructuredClone = deepCopy(nestedObject); // 深拷贝
console.log(nestedObject === nestedCopyWithStructuredClone); // false
最终版:处理特殊对象:Date 和 RegExp
简单版只处理了对象和数组的情况,这里,我们再增加处理 Date
和 RegExp
,让代码更加完善。
const deepCopy = (obj, cache = new WeakMap()) => {
// 如果不是对象或者为 null,直接返回
if (typeof obj !== "object" || obj === null) {
return obj;
}
// 如果已经复制过该对象,则直接返回
if (cache.has(obj)) {
return cache.get(obj);
}
// 创建一个新对象或数组
const newObj = Array.isArray(obj) ? [] : {};
// 将新对象放入 cache 中
cache.set(obj, newObj);
// 处理特殊对象的情况
if (obj instanceof Date) {
return new Date(obj.getTime());
}
if (obj instanceof RegExp) {
return new RegExp(obj);
}
// 处理循环引用的情况
Object.keys(obj).forEach((key) => {
newObj[key] = deepCopy(obj[key], cache);
});
return newObj;
};
// 测试 Date 和 regExp 的深拷贝:
const date = new Date();
const regExp = /test/g;
const cloneDate = deepCopy(date); // 深拷贝
const cloneRegExp = deepCopy(regExp); // 深拷贝
console.log(cloneDate === date); // false
console.log(cloneRegExp === regExp); // false
当然,除了上述提到的类型外,还有函数、Symbol 等类型,这里不做阐述,感兴趣的可以去看相关库的源码。
小练习
实现深拷贝有哪些方法?
答案:见正文。
阅读量:1373
点赞量:0
收藏量:0