第四章:对象—下-灵析社区

懒人学前端

五、Map 和 WeakMap 有什么区别

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 使用。

不过 MapObject 有一些重要的区别,在下列情况中使用 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())是一样的,它们区别有以下四个方面:

  • 定义
  • 键值(key)
  • 垃圾回收
  • 有无 size 属性

小练习

MapObject 有什么区别?

答案:见正文。

六、如何实现深拷贝和浅拷贝?

深拷贝和浅拷贝都是针对数据类型是 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 深拷贝之后两个对象用 === 比较不相等

第三方库lodashrfdc

深拷贝可以使用 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