数据类型

JS中有8种数据类型,分为基本数据类型引用数据类型

  1. 基本数据类型(值类型): String字符串、Number数值、BigInt(ES6)大型数值、Boolean布尔值、Null空值、Undefined未定义、Symbol(ES6)。
  2. 引用数据类型(引用类型): Object 对象(除了基本数据类型之外,都可称之为Object类型)。

存储方式:

  1. 基本类型直接保存在
  2. 引用类型存放在中。在栈空间中只保留了数据在堆中的地址,访问时,通过栈中的引用地址来访问堆中实际的数据。

至于V8堆栈框架,以后再说。

null 和 undefined

undefined 表示值的缺失,null 表示对象的缺失。当没有值时,通常默认为 undefined。

null 是一个关键字,但是 undefined 是一个普通的标识符,恰好是一个全局属性。

早期 undefined 作为全局属性可以被赋值,所以有些人使用 void 0 来获取 undefined。

null 与 undefined 不严格相等
1
2
console.log(null == undefined); // true
console.log(null === undefined); // false

null 转为数字时为 0,undefined 转为数字时为 NaN。

1
2
console.log(+null) // 0
console.log(+undefined) // NaN

判断类型

typeof

typeof 运算符,返回数据类型的字符串,对于引用类型,除了函数,都会返回 object。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
console.log(typeof '123'); // string
console.log(typeof 123); // number
console.log(typeof 123n); // bigint
console.log(typeof NaN); // number
console.log(typeof true); // boolean
console.log(typeof Symbol()); // symbol
console.log(typeof undefined); // undefined
console.log(typeof undeclaredVariable); // undefined
// console.log(typeof document.all) // undefined
// 对象
console.log(typeof null); // object
console.log(typeof []); // object
console.log(typeof {}); // object
console.log(typeof new Date()); // object
console.log(typeof /123/); // object
// 函数
console.log(typeof new Function()); // function
console.log(typeof (()=>{})); // function
console.log(typeof class A{}); // function

null与object:
在 JS 的最初版本中,还广泛使用着32位系统,JS 用32个二进制位标识值,其中低三位表示值的类型。

typeof通过判断存储的机器码的低三位来进行类型判断。

1
2
3
4
5
6
7
8
数据类型	机器码标识
对象(Object) 000
整数 1
浮点数 010
字符串 100
布尔 110
undefined -2^31(全为 132 位带符号整数)
null 全为0

null的机器码标识为全0,而对象的机器码低位标识为000。所以typeof null被误判为Object。

即使在 ES6 时有提案要修复这个bug,但因为兼容性问题被否决,这已经是一个特性(feature)不再被修复。

甚至后来出现了typeof null等于undefined的bug,被光速改回了原来的样子。

The history of typeof null

constructor

对象原型上的 constructor 属性,返回实例对象的构造函数的引用。

通过 constructor 判断对象数据类型,但这种方式并不可靠,因为可以通过更改原型链来更改 constructor 属性。

1
2
3
4
5
6
7
8
const arr = [1, 2, 3, 4, 5];
console.log(arr.constructor === Array); // true
console.log(arr.constructor === Object); // false
// 改写原型
const obj = {};
Object.setPrototypeOf(arr, obj);
console.log(arr.constructor === Array); // false
console.log(arr.constructor === Object); // true

instanceof

instanceof 运算符判断一个实例是否属于某种类型

即某个构造函数prototype 属性(原型对象)是否出现在某个实例对象的原型链上。

1
2
3
4
5
class A {}
class B extends A {}
const b = new B();
console.log(b instanceof B); // true
console.log(b instanceof A); // true

继承形成了两条原型链:

  1. 实例的原型对象的 __proto__ 指向父类的原型对象,这是为了继承父类的方法(实例属性在创建对象时,直接作为对象自己的属性实例化,无需通过原型链继承,而实例方法,都在原型上,所以只有实例方法需要通过原型链继承,向上查找)
  2. 构造函数的 __proto__ 指向父类的构造函数,这是为了继承父类的静态属性和方法。
1
2
console.log(B.prototype.__proto__ === A.prototype); // true
console.log(B.__proto__ === A); // true

instanceof 就是通过判断实例的原型链上是否有某个构造函数的 prototype 属性(原型对象)来判断实例的类型。

原型链上最后一个原型对象是 Object.prototype,所以所有的实例都属于 Object 类型,也就是万物皆对象。

类本质是构造函数的语法糖,所以也是 Function 类型。

1
2
3
console.log(b instanceof Object); // true
console.log(B instanceof Function); // false
console.log(B instanceof Object); // true

之前的笔记 原型与原型链

手写instanceof

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function myInstanceOf(Fn) {
if (!this) return false; // 如果this不存在,返回false
// 获取Fn的显示原型
const prototype = Fn.prototype;
// 获取this的隐式原型
let obj = Object.getPrototypeOf(this);
// 循环往原型链上查找,直到找到原型链尽头null
while (obj) {
if (obj === prototype) {
return true;
}
// 如果没有找到就继续向上查找
obj = Object.getPrototypeOf(obj);
}
return false;
}
// 挂载到所有对象的原型链上
Object.prototype.myInstanceOf = myInstanceOf;
测试
1
2
3
4
5
6
7
8
9
10
class A {}
class B extends A {}
const b = new B();
class C {}
console.log(b.myInstanceOf(B)); // true
console.log(b.myInstanceOf(A)); // true
console.log(b.myInstanceOf(Object)); // true
console.log(b.myInstanceOf(C)); // false
console.log(B.myInstanceOf(Function)); // true
console.log(B.myInstanceOf(Object)); // true

基本包装类型

基本数据类型没有原型链,所以 instanceof 无法判断基本数据类型。

但通过对应基本包装类型创建的实例,可以通过 instanceof 判断。

三种基本包装类型:String, Number, Boolean。

1
2
3
4
5
6
7
8
const s = new String('111');
const n = new Number(111);
console.log(s instanceof String); // true
console.log(n instanceof Number); // true
const ss = '111';
const nn = 111;
console.log(ss instanceof String); // false
console.log(nn instanceof Number); // false

基本数据类型在调用属性和方法时,会进行装箱操作,把基本类型用它们相应的引用类型包装起来,使其具有对象的性质。会产生一些临时对象。

is方法

isPrototypeOf 判断一个对象是否是另一个对象的原型。通用会在原型链上查找。

1
2
3
4
5
6
class A {}
class B extends A {}
const b = new B();
const bp = Object.getPrototypeOf(b);
console.log(bp.isPrototypeOf(b)); // true
console.log(A.prototype.isPrototypeOf(b)); // true

其它基本包装类型和内置类型也提供了一些 is 方法

1
2
3
4
5
6
7
console.log(Array.isArray([])); // true
console.log(Array.isArray({})); // false
console.log(Number.isNaN(NaN)); // true
console.log(Number.isNaN(123)); // false
console.log(Number.isInteger(123)); // true
console.log(Number.isInteger(123.1)); // false
console.log(Number.isFinite(Infinity)) // false

isNaN 和 isFinite

全局和 Number 对象上都有 isNaNisFinite 方法,用于判断是否是 NaN 和 有限数。但全局方法会进行隐式类型转换再判断,而 Number 对象上的方法则不会。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
console.log(isNaN(NaN)) // true
console.log(isNaN({})) // true, Number({}) => NaN
console.log(isNaN('NaN')) // true, Number('NaN') => NaN
console.log(isNaN('123')) // false, Number('123') => 123
console.log(Number.isNaN(NaN)) // true
console.log(Number.isNaN({})) // false
console.log(Number.isNaN('NaN')) // false
console.log(Number.isNaN('123')) // false

console.log(isFinite({})) // false
console.log(isFinite(Infinity)) // false
console.log(isFinite('123')) // true, Number('123') => 123
console.log(Number.isFinite({})) // false
console.log(Number.isFinite(Infinity)) // false
console.log(Number.isFinite('123')) // false

toString

Object.prototype.toString.call()[object xxx] 的字符串形式返回当前调用者的对象类型。是最稳妥的类型判断方式。

必须调用 Object 显式原型上的 toString 方法,因为其它数据类,都重写了 toString 方法,返回的不再是 [object xxx] 类型字符串。

1
2
3
4
5
6
7
function typeToString(any) {
return Object.prototype.toString.call(any).slice(8, -1);
}
console.log(typeToString([])); // Array
console.log(typeToString((()=>{}))); // Function
console.log(typeToString(123)); // Number
console.log(typeToString(Symbol('a'))) // Symbol

这种方法只能判断内置类型基本包装类型,即 Arguments, Array, Boolean, Date, Error, Function, JSON, Math, Number, Object, RegExp 和 String。

1
2
const a = new class A {};
console.log(typeToString(a)); // Object

对于基本数据类型,会进行装箱操作,返回对应的引用类型。这会产生一些临时对象,所以通常需要配合 typeof 来判断类型,提高性能。

为什么Object.prototype.toString.call()可以如此准确的判断对象类型?

封装类型判断函数

  1. null 比较特殊,typeof 会返回 object,先严格比较后返回 null。
  2. 基本数据类型直接使用 typeof 获取类型,避免装箱操作,提高性能。
  3. 其它引用类型使用 Object.prototype.toString.call() 获取类型。
1
2
3
4
5
6
7
8
9
10
11
12
function typeJudge(val) {
if (val === null) return 'null';
if (typeof val !== "object") return typeof val;
return Object.prototype.toString.call(val).slice(8, -1).toLowerCase();
}
console.log(typeJudge([])); // array
console.log(typeJudge({})); // object
console.log(typeJudge(new Date())); // date
console.log(typeJudge(null)); // null
console.log(typeJudge(undefined)); // undefined
console.log(typeJudge(123)); // number
console.log(typeJudge('123')); // string

Symbol.toStringTag

Object.prototype.toString 会读取一个对象的 Symbol.toStringTag 属性,该属性返回值将作为 [object xxx] 中的 xxx。用于创建对象的默认字符串描述。

应该返回一个字符串,否则会返回默认值,即 [object Object]

可以用于标识自定义类型。

1
2
3
4
5
6
7
class A {
get [Symbol.toStringTag]() {
return this.constructor.name;
}
}
const a = new A();
console.log(Object.prototype.toString.call(a)); // [object A]

注意:该属性可以被 defineProperty 修改。

1
2
3
4
5
6
7
8
9
10
11
class A {
get [Symbol.toStringTag]() {
return this.constructor.name;
}
}
const a = new A();
console.log(Object.prototype.toString.call(a)); // [object A]
Object.defineProperty(a, Symbol.toStringTag, {
value: 'B',
});
console.log(Object.prototype.toString.call(a)); // [object B]

Promise

Promise 非常特殊,在 Promise A+ 规范并没有设计如何创建、解决和拒绝 Promise,而是专注于提供一个通用的 .then 方法,换而言之,只要一个对象具有 .then 方法,并且符合规范(具有特定参数、返回特定值),就可以称之为 Promise。

Promise A+ 早于 ES6,是社区规范,为了解决回调地狱和异步实现不统一的问题。
ES6 的 Promise 符合 Promise A+ 规范,是对该规范的实现。提供了 Promise 类(构造函数)去构建一个 Promise 对象。并提供了除了 .then 方法之外的一些方法。

许多早期的第三方库使用的并不是 Promise 类构建的 Promise 对象,而是自己实现的 Promise 对象。

下面实现 isPromise(),判断一个对象是否是 Promise 对象。

1
2
3
4
5
6
7
8
9
10
function isObject(val) {
return val !== null && (typeof val === "object" || typeof val === "function");
}
function isPromise(p) {
return p instanceof Promise || (isObject(p) && typeof p.then === "function");
}
const p = Promise.resolve();
const pp = { then: () => {} };
console.log(isPromise(p)); // true
console.log(isPromise(pp)); // true

等比较

对于 ===== 已经非常熟悉了。

  1. == 宽松相等,会进行隐式类型转换,并按照 IEEE754 标准对 NaN、-0 和 +0 进行特殊处理(故 NaN != NaN,且 -0 == +0),IsLooselyEqual
  2. === 严格相等,与 == 逻辑相同,但不会进行隐式类型转换。如果类型不同,则返回 false。IsStrictlyEqual

JavaScript 中的相等性判断-MDN

1
2
3
4
console.log(null == undefined) // true
console.log(null === undefined) // false
console.log(NaN == NaN) // false
console.log(+0 == -0) // true

Object.is() 方法判断两个值是否是相同的值。与 === 类似,但是对于 NaN 和 +0 和 -0 的判断有所不同。使用SameValue

  1. Object.is(NaN, NaN) 返回 true,而 NaN == NaN 返回 false
  2. Object.is(+0, -0) 返回 false,而 +0 == -0 返回 true
1
2
console.log(Object.is(NaN, NaN)) // true
console.log(Object.is(+0, -0)) // false

indexOf 和 includes

indexOf 无法判断 NaN,因为使用的是严格相等。而 includes 可以判断 NaN,使用 SameValueZero 算法,NaN等于NaN。

1
2
3
4
const arr = [NaN];
console.log(arr.indexOf(NaN)) // -1
console.log(arr.includes(NaN)) // true
console.log(arr.findIndex((val) => Object.is(val, NaN))) // 0

拷贝

拷贝,也就是复制数据,对于基本数据类型,直接赋值多个变量也互不影响。
但对于引用类型,赋值是浅拷贝,多个变量保存同一个引用,指向同一个堆空间,修改其中一个变量,会影响到其它变量。而深拷贝,就是完全复制一个对象,包括其内部的对象,开辟新的堆空间,多个变量互不影响。

直接赋值就是复制栈空间的值,对于基本类型,栈存放其数据值,而引用类型,栈存放其引用地址,指向实际存放对象数据的堆内存,所以直接赋值是浅拷贝。

总之,基本类型的浅拷贝复制的就是数据值,而引用类型的浅拷贝,复制的是引用地址。

浅拷贝

除了直接赋值,还有一些方法可以实现浅拷贝。

Object.assign

Object.assign() 用于将任意多个对象自身的可枚举属性拷贝给目标对象,然后返回目标对象

第一层基本类型的属性是将值本身复制一份,而引用类型的属性是浅拷贝

1
2
3
4
5
6
7
8
9
10
11
12
13
const obj1 = { 
name: '123',
data: {
val: 1
}
};
const obj2 = {};
Object.assign(obj2, obj1);
console.log(obj2); // { name: '123' }
obj2.name = '456';
console.log(obj1.name); // 123,基本类型,直接复制一份
obj2.data.val = 2;
console.log(obj1.data.val); // 2,引用类型的属性是浅拷贝,修改其中一个对象,会影响到另一个对象

展开运算符…

Object.assign 类似,也是浅拷贝

1
2
3
4
5
6
7
8
9
const obj1 = { 
name: '123',
data: {
val: 1
}
};
const obj2 = { ...obj1 };
obj2.data.val = 2;
console.log(obj1.data.val); // 2

Array.prototype.slice

slice 方法返回一个新的数组,返回原数组指定范围(左闭右开)元素的浅拷贝

1
2
3
4
const arr1 = [1, 2, { val: 3 }];
const arr2 = arr1.slice();
arr2[2].val = 4;
console.log(arr1[2].val); // 4

Array.prototype.concat

concat 方法用于合并若干个数组,返回一个新数组,也是浅拷贝

1
2
3
4
5
6
7
8
const arr1 = [{ val: 3 }];
const arr2 = [{ name: 'aaa' }];
const arr3 = arr1.concat(arr2);
console.log(arr3); // [ { val: 3 }, { name: 'aaa' } ]
arr3[0].val = 4;
arr3[1].name = 'bbb';
console.log(arr1[0].val); // 4
console.log(arr2[0].name); // bbb

手写函数

直接遍历对象属性,再添加给新对象。

1
2
3
4
5
6
7
function clone(target) {
let cloneTarget = {};
for (const key in target) {
cloneTarget[key] = target[key];
}
return cloneTarget;
};

深拷贝

浅拷贝是常见的,而为了实现引用数据类型的深拷贝,需要借助其它的方法。

JSON方法

JSON.parse(JSON.stringify()) 先将对象转为JSON字符串,再将JSON转为对象。

1
2
3
4
5
6
7
8
const obj = {
data: {
val: 1
}
}
const obj2 = JSON.parse(JSON.stringify(obj));
obj2.data.val = 2;
console.log(obj.data.val); // 1

缺点:

  1. 无法复制函数、正则、Date、undefined、Symbol等内置对象。
  2. 无法解析循环引用的对象,会报错。
1
2
3
4
5
6
7
8
9
const obj = {
a: new Date(),
b: /123/,
c: function() {},
d: Symbol('123'),
}
const obj2 = JSON.parse(JSON.stringify(obj));
console.log(obj2); // { a: '2024-03-13T02:34:16.846Z', b: {} }
console.log(obj2.a instanceof Date); // false

第二参数

真的没办法复制函数和其它数据类型吗?其实可以。
JSON.parseJSON.stringify 可以传入第二个参数,是一个函数,用于自定义过滤转换结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const obj1 = {
a: 1,
fn: function () {
console.log("123");
},
};
const obj2 = JSON.parse(JSON.stringify(obj1));
console.log(obj2); // { a: 1 }
// 传入第二个参数,对函数类型属性进行处理
const obj3 = JSON.parse(
JSON.stringify(obj1, (key, value) => {
if (typeof value === "function") {
// 将函数转为字符串,并且加上特殊标识
return value.toString() + '#function#';
}
return value;
}),
(key, value) => {
// 将函数字符串转为函数
if (typeof value === "string" && value.includes("#function#")) {
// 去掉特殊标识
value = value.replace("#function#", "");
return new Function(`return ${value}`)();
}
return value;
}
);
console.log(obj3); // { a: 1, fn: [Function (anonymous)] }
obj3.fn(); // 123

借助这第二个参数,完全可以实现任何对象的深拷贝。但为了更加灵活可控,还是需要手写递归。

手写递归

深拷贝需要找到对象多层嵌套的最深层的基础数据类型,很显然,需要用到递归。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function deepClone(target) {
// 值为空,或为基础类型,直接返回
if (!target || typeof target !== "object") return target;
// 对于对象、数组类型,则遍历其属性,递归深拷贝
let obj = Array.isArray(target) ? [] : {};
for (key in target){
obj[key] = deepClone(target[key]);
}
return obj;
}
const obj1 = {
a: {
b: 1,
c: [1,2,3]
},
}
const obj2 = deepClone(obj1);
obj2.a.b = 2;
obj2.a.c[0] = 2;
console.log(obj1); // { a: { b: 1, c: [ 1, 2, 3 ] } }
console.log(obj2); // { a: { b: 2, c: [ 2, 2, 3 ] } }

循环引用

手写深拷贝目的之一就是解决循环引用,例如原型链等就存在循环引用。

可以使用 hashMap 来存储已经拷贝过的对象,遇到循环引用时,直接返回 hashMap 中的对象。

在这里,map仅作为缓存,可以使用 WeakMap,其键必须是对象且是弱引用,不会阻止GC对作为键的对象的回收,无需手动清除Map属性,避免内存泄漏。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
function deepClone(target, map = new WeakMap()) {
// 如果是对象,即引用类型
if(typeof target === 'object') {
// 判断是数组还是对象
let cloneTarget = Array.isArray(target) ? [] : {};
// 如果已经克隆过了,直接返回之前的对象
if(map.get(target)) {
return map.get(target);
}
// 没有克隆过,就将当前克隆对象存入map中
map.set(target, cloneTarget);
// 遍历key,对象是属性,数组是索引
for(let key in target) {
cloneTarget[key] = deepClone(target[key], map);
}
// 返回克隆对象
return cloneTarget;
} else {
// 不是对象直接返回
return target;
}
}
const obj1 = {
a: {
b: 1,
c: [1,2,3]
},
}
obj1.obj = obj1; // 循环引用
const obj2 = deepClone(obj1);
obj2.a.b = 2;
obj2.a.c[0] = 2;
console.log(obj1); // { a: { b: 1, c: [ 1, 2, 3 ] }, obj: [Circular *1] }
console.log(obj2); // { a: { b: 2, c: [ 2, 2, 3 ] }, obj: [Circular *1] }
console.log(obj1.obj === obj2.obj); // false

遍历性能优化

forEach 和传统的 for 循环性能差不多,而 for in 性能遥遥落后,while 循环性能最好。

性能测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const obj = {
a: {},
b: {},
c: {},
};
console.time();
for (const key in obj) {
console.log(key); // a b c
}
console.timeEnd(); // default: 8.696ms

console.time();
Object.keys(obj).forEach((key) => {
console.log(key); // a b c
});
console.timeEnd(); // default: 0.908ms

console.time();
const keys1 = Object.keys(obj);
const length1 = keys1.length;
for (let i = 0; i < length1; i++) {
console.log(keys1[i]); // a b c
}
console.timeEnd(); // default: 1.022ms

console.time();
let i = -1;
const keys2 = Object.keys(obj);
const length2 = keys2.length;
while (++i < length2) {
console.log(keys2[i]); // a b c
}
console.timeEnd(); // default: 0.68ms

封装一个通用的遍历函数,用于遍历对象和数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function forEach(obj, callback) {
// 无需区分数组和对象,因为数组也是对象,直接获取所有key
const keys = Object.keys(obj);
const length = keys.length;
let i = -1;
while (++i < length) {
// 回调函数传入当前遍历的值和索引
callback(obj[keys[i]], keys[i]);
}
}

const obj = {
a: {}, b: {}, c: {},
};
forEach(obj, (val, index) => {
console.log(val, index); // {} a, {} b, {} c
});
const arr = [1, 2, 3];
forEach(arr, (val, index) => {
console.log(val, index); // 1 0, 2 1, 3 2
});

修改原来的深拷贝函数,使用封装的 forEach 遍历对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
function deepClone(target, map = new WeakMap()) {
// 如果是对象,即引用类型
if (typeof target === "object") {
// 判断是数组还是对象
let cloneTarget = Array.isArray(target) ? [] : {};
// 如果已经克隆过了,直接返回之前的对象
if (map.get(target)) {
return map.get(target);
}
// 没有克隆过,就将当前克隆对象存入map中
map.set(target, cloneTarget);
// 使用封装的forEach遍历
forEach(target, (val, key) => {
cloneTarget[key] = deepClone(val, map);
});
// 返回克隆对象
return cloneTarget;
} else {
// 不是对象直接返回
return target;
}
}
function forEach(obj, callback) {
// 无需区分数组和对象,因为数组也是对象,直接获取所有key
const keys = Object.keys(obj);
const length = keys.length;
let i = -1;
while (++i < length) {
// 回调函数传入当前遍历的值和索引
callback(obj[keys[i]], keys[i]);
}
}

其它数据类型

目前只处理了可遍历的对象和数组,还有一些内置对象,如函数、正则、Date、Map、Set等,需要根据其特性进行特殊处理

判断数据类型的函数,在前面已经封装过了,这里直接拿来用。

判断数据类型
1
2
3
4
5
function typeJudge(val) {
if (val === null) return 'null';
if (typeof val !== "object") return typeof val;
return Object.prototype.toString.call(val).slice(8, -1).toLowerCase();
}

想要处理其它数据类型,就需要掌握它们的特性,这也是为什么,本文先讲数据类型再讲拷贝的原因。

  1. 可继续遍历的类型:Object、Array、Map、Set,对于这种类型,可以和之前一样继续遍历其属性,递归深拷贝。
  2. 不可继续遍历的类型:Bool、Number、String、Date、Error、RegExp这几种类型可以直接用其构造函数和原始数据创建一个新对象。

需要注意的点:

  1. 由基本包装类型的构造函数 new 出来的对象,是 object 类型。
  2. Map 和 Set 虽然可以直接使用原数据 new 一个新的对象,但是其内部的数据可能是引用类型,所以还是需要递归深拷贝。
  3. 函数和错误类型通常不需要深拷贝,深拷贝没有意义,直接返回即可。
完整代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
// 遍历函数
function forEach(obj, callback) {
// 无需区分数组和对象,因为数组也是对象,直接获取所有key
const keys = Object.keys(obj);
const length = keys.length;
let i = -1;
while (++i < length) {
// 回调函数传入当前遍历的值和索引
callback(obj[keys[i]], keys[i]);
}
return obj;
}
// 获取最准确的数据类型
function typeJudge(val) {
if (val === null) return "null";
if (typeof val !== "object") return typeof val;
return Object.prototype.toString.call(val).slice(8, -1).toLowerCase();
}
// 判断是否是引用数据类型,也就是广泛意义上的对象
function isObject(val) {
return val !== null && typeof val === "object";
}
// 判断是否是函数
function isFunction(val) {
return typeof val === "function";
}
// 获取对象的构造函数
function getConstructor(val) {
return val.constructor;
}
// 判断是否是可继续遍历类型
function createCanTraverse() {
const deepType = ["object", "array", "map", "set", "weakmap", "weakset"];
return (type) => deepType.includes(type);
}
const canTraverse = createCanTraverse();
// 克隆可遍历对象
function createCloneCanTraverse(type, map) {
switch (type) {
case "set":
case "weakset":
return (cloneTarget, target) => {
target.forEach((val) => {
cloneTarget.add(deepClone(val, map));
});
};
case "map":
case "weakmap":
return (cloneTarget, target) => {
target.forEach((val, key) => {
cloneTarget.set(key, deepClone(val, map));
});
};
default:
return (cloneTarget, target) => {
forEach(target, (val, key) => {
cloneTarget[key] = deepClone(val, map);
});
};
}
}
// 克隆其它内置对象类型
function cloneOtherObject(val, type) {
// 获取构造函数
const Ctor = getConstructor(val);
switch (type) {
case "number":
case "string":
case "boolean":
case "regexp":
case "date":
return new Ctor(val);
case "error":
return val; // 错误对象无需克隆
default:
return null;
}
}
function deepClone(target, map = new WeakMap()) {
// 如果是对象,即引用类型
if (isObject(target)) {
// 如果已经克隆过了,直接返回之前的对象
if (map.get(target)) {
return map.get(target);
}
// 获取当前对象的类型
const type = typeJudge(target);
// 先尝试克隆其它对象类型
const result = cloneOtherObject(target, type);
if (result) {
return result;
}
// 如果result为null,说明是可继续遍历类型,或者修改了toString描述的自定义对象
let cloneTarget = {};
// 如果是可继续遍历类型,就创建一个对应的新的对象
if (canTraverse(type)) {
const Ctor = getConstructor(target);
cloneTarget = new Ctor();
}
// 没有克隆过,就将当前克隆对象存入map中
map.set(target, cloneTarget);
// 克隆可遍历对象
createCloneCanTraverse(type, map)(cloneTarget, target);
// 返回克隆对象
return cloneTarget;
}
if (isFunction(target)) {
// 如果是函数,直接返回,克隆也没什么意义
return target;
} else {
// 基本数据类型直接返回
return target;
}
}

const obj1 = {
a: {
b: 1,
c: [1, 2, 3],
reg: /123/,
date: new Date(),
fn: function () {},
sym: Symbol("123"),
},
set: new Set([1, 2, 3]),
map: new Map([
["a", 1],
["b", 2],
["c", { val: 3 }],
]),
};
obj1.obj = obj1; // 循环引用
const obj2 = deepClone(obj1);
obj2.a.b = 2;
obj2.a.c[0] = 2;
obj2.set.add(4);
obj2.map.set("d", 4);
obj2.map.get("c").val = 666;
console.log(obj1);
console.log(obj2);
/* <ref *1> {
a: {
b: 1,
c: [ 1, 2, 3 ],
reg: /123/,
date: 2024-03-13T06:41:58.179Z,
fn: [Function: fn],
sym: Symbol(123)
},
set: Set(3) { 1, 2, 3 },
map: Map(3) { 'a' => 1, 'b' => 2, 'c' => { val: 3 } },
obj: [Circular *1]
}
<ref *1> {
a: {
b: 2,
c: [ 2, 2, 3 ],
reg: /123/,
date: 2024-03-13T06:41:58.179Z,
fn: [Function: fn],
sym: Symbol(123)
},
set: Set(4) { 1, 2, 3, 4 },
map: Map(4) { 'a' => 1, 'b' => 2, 'c' => { val: 666 }, 'd' => 4 },
obj: [Circular *1]
} */

当然还有一些问题没解决:

  1. 对象的原型链没处理
  2. 对象的属性描述符没处理
  3. 如果是dom节点,应该使用 cloneNode 方法
  4. 对象不可枚举的属性也没处理

structuredClone

写递归太麻烦了,也许可以引入第三方库,比如lodash的_.cloneDeep方法。

JS本身也提供了深拷贝全局方法 structuredClone,且支持循环引用。需要注意兼容性,Chrome98、Node17 等以上才支持

具有两个参数:

  1. value 要克隆的对象:任意结构化可克隆类型。
  2. transfer 可转移的对象的数组:其中的可转移对象将被移动到新的对象,而不是克隆至新的对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const obj1 = {
date: new Date(),
reg: /123/,
a: {
b: 1
}
}
obj1.obj = obj1; // 循环引用
const obj2 = structuredClone(obj1);
console.log(obj2);
/* <ref *1> {
date: 2024-03-13T07:55:00.334Z,
reg: /123/,
a: { b: 1 },
obj: [Circular *1]
} */
obj2.a.b = 2;
console.log(obj1.a.b); // 1

structuredClone 使用结构化克隆算法,不能克隆函数、DOM节点、Symbol、不可枚举属性,对象的某些特定参数也不会被保留(属性描述符、setters、getters、原形链上的属性、RegExp对象的lastIndex字段)

MessageChannel

MessageChannel用于在不同的浏览器上下文,比如window.open()打开的窗口、iframe、多个work等之间建立通信管道,并通过两端的端口(port1和port2)以DOM Event的形式发送消息,为宏任务。兼容性非常好。

Vue的nextTick在2.5版本也使用了MessageChannel,setImmediate -> MessageChannel -> setTimeout 0

1
2
3
4
5
6
if (typeof MessageChannel === 'function') {
var channel = new MessageChannel();
var port = channel.port2;
channel.port1.onmessage = nextHandler;
port.postMessage(1);
}
快速上手
1
2
3
4
5
6
7
const mc = new MessageChannel();
const [p1, p2] = [mc.port1, mc.port2];
p2.postMessage(123);
p1.onmessage = (e) => {
console.log(e.data); // 123
p1.close();
};

close 方法断开该端口的连接,停止流向该端口的消息,也不再能发送消息。

可以使用 addEventListener 监听 message 事件,但要显式调用 start() 接收在端口上排队的消息,DOM 0级事件 onmessage 则会自动开始接收消息。在开始接收消息前,另一个端口发送的消息会进入缓冲区。

消息在发送和接收的过程需要序列化和反序列化,可以实现深拷贝,同时也意味着消息只能基本类型或结构化可克隆对象。

1
2
3
4
5
6
7
8
9
10
11
12
const mc = new MessageChannel();
const [p1, p2] = [mc.port1, mc.port2];
const obj1 = {
a: { b: 1 },
};
p1.postMessage(obj1);
p2.onmessage = (e) => {
let obj2 = e.data;
obj2.a.b = 2;
console.log(obj1.a.b); // 1
p2.close();
};

通过 promise 封装深拷贝函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function deepClone(obj) {
return new Promise((resolve) => {
const mc = new MessageChannel();
mc.port2.postMessage(obj);
mc.port1.onmessage = (e) => {
mc.port1.close();
resolve(e.data);
};
});
}
const obj = {
a: { b: 1 },
};
obj.obj = obj; // 循环引用
deepClone(obj).then((res) => {
console.log(res === obj); // false
console.log(res); // <ref *1> { a: { b: 1 }, obj: [Circular *1] }
});