数据类型 JS中有8种 数据类型,分为基本数据类型 和引用数据类型
基本数据类型(值类型): String字符串、Number数值、BigInt(ES6)大型数值、Boolean布尔值、Null空值、Undefined未定义、Symbol(ES6)。
引用数据类型(引用类型): Object 对象(除了基本数据类型之外,都可称之为Object类型)。
存储方式:
基本类型 直接保存在栈 中
引用类型 存放在堆 中。在栈空间中只保留了数据在堆中的地址,访问时,通过栈中的引用地址来访问堆中实际的数据。
至于V8堆栈框架,以后再说。
null 和 undefined undefined 表示值的缺失,null 表示对象的缺失。当没有值时,通常默认为 undefined。
null 是一个关键字,但是 undefined 是一个普通的标识符,恰好是一个全局属性。
早期 undefined 作为全局属性可以被赋值,所以有些人使用 void 0
来获取 undefined。
null 与 undefined 不严格相等 1 2 console .log (null == undefined ); console .log (null === undefined );
null 转为数字时为 0,undefined 转为数字时为 NaN。
1 2 console .log (+null ) console .log (+undefined )
判断类型 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' ); console .log (typeof 123 ); console .log (typeof 123n ); console .log (typeof NaN ); console .log (typeof true ); console .log (typeof Symbol ()); console .log (typeof undefined ); console .log (typeof undeclaredVariable); console .log (typeof null ); console .log (typeof []); console .log (typeof {}); console .log (typeof new Date ()); console .log (typeof /123 /); console .log (typeof new Function ()); console .log (typeof (()=> {})); console .log (typeof class A {});
null与object: 在 JS 的最初版本中,还广泛使用着32位系统,JS 用32个二进制位标识值,其中低三位表示值的类型。
typeof通过判断存储的机器码的低三位来进行类型判断。
1 2 3 4 5 6 7 8 数据类型 机器码标识 对象(Object ) 000 整数 1 浮点数 010 字符串 100 布尔 110 undefined -2 ^31 (全为 1 的 32 位带符号整数)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 ); console .log (arr.constructor === Object ); const obj = {};Object .setPrototypeOf (arr, obj);console .log (arr.constructor === Array ); console .log (arr.constructor === Object );
instanceof instanceof
运算符判断一个实例 是否属于某种类型 。
即某个构造函数 的 prototype
属性(原型对象)是否出现在某个实例对象的原型链 上。
1 2 3 4 5 class A {}class B extends A {}const b = new B ();console .log (b instanceof B); console .log (b instanceof A);
继承形成了两条原型链:
实例的原型对象的 __proto__
指向父类的原型对象,这是为了继承父类的方法 (实例属性在创建对象时,直接作为对象自己的属性实例化,无需通过原型链继承,而实例方法,都在原型上,所以只有实例方法需要通过原型链继承,向上查找)
构造函数的 __proto__
指向父类的构造函数,这是为了继承父类的静态 属性和方法。
1 2 console .log (B.prototype .__proto__ === A.prototype ); console .log (B.__proto__ === A);
而 instanceof
就是通过判断实例的原型链上是否有某个构造函数的 prototype
属性(原型对象)来判断实例的类型。
原型链上最后一个原型对象是 Object.prototype
,所以所有的实例都属于 Object
类型,也就是万物皆对象。
类本质是构造函数的语法糖,所以也是 Function
类型。
1 2 3 console .log (b instanceof Object ); console .log (B instanceof Function ); console .log (B instanceof Object );
之前的笔记 原型与原型链
手写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 ; const prototype = Fn .prototype ; let obj = Object .getPrototypeOf (this ); 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)); console .log (b.myInstanceOf (A)); console .log (b.myInstanceOf (Object )); console .log (b.myInstanceOf (C)); console .log (B.myInstanceOf (Function )); console .log (B.myInstanceOf (Object ));
基本包装类型 基本数据类型没有原型链,所以 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 ); console .log (n instanceof Number ); const ss = '111' ;const nn = 111 ;console .log (ss instanceof String ); console .log (nn instanceof Number );
基本数据类型在调用属性和方法时,会进行装箱操作
,把基本类型用它们相应的引用类型包装起来,使其具有对象的性质。会产生一些临时对象。
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)); console .log (A.prototype .isPrototypeOf (b));
其它基本包装类型和内置类型也提供了一些 is 方法
1 2 3 4 5 6 7 console .log (Array .isArray ([])); console .log (Array .isArray ({})); console .log (Number .isNaN (NaN )); console .log (Number .isNaN (123 )); console .log (Number .isInteger (123 )); console .log (Number .isInteger (123.1 )); console .log (Number .isFinite (Infinity ))
isNaN 和 isFinite 全局和 Number 对象上都有 isNaN
和 isFinite
方法,用于判断是否是 NaN 和 有限数。但全局方法会进行隐式类型转换 再判断,而 Number 对象上的方法则不会。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 console .log (isNaN (NaN )) console .log (isNaN ({})) console .log (isNaN ('NaN' )) console .log (isNaN ('123' )) console .log (Number .isNaN (NaN )) console .log (Number .isNaN ({})) console .log (Number .isNaN ('NaN' )) console .log (Number .isNaN ('123' )) console .log (isFinite ({})) console .log (isFinite (Infinity )) console .log (isFinite ('123' )) console .log (Number .isFinite ({})) console .log (Number .isFinite (Infinity )) console .log (Number .isFinite ('123' ))
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 ([])); console .log (typeToString ((()=> {}))); console .log (typeToString (123 )); console .log (typeToString (Symbol ('a' )))
这种方法只能判断内置类型 和基本包装类型 ,即 Arguments, Array, Boolean, Date, Error, Function, JSON, Math, Number, Object, RegExp 和 String。
1 2 const a = new class A {};console .log (typeToString (a));
对于基本数据类型,会进行装箱操作 ,返回对应的引用类型。这会产生一些临时对象,所以通常需要配合 typeof
来判断类型,提高性能。
为什么Object.prototype.toString.call()可以如此准确的判断对象类型?
封装类型判断函数
null 比较特殊,typeof 会返回 object,先严格比较后返回 null。
基本数据类型直接使用 typeof 获取类型,避免装箱操作,提高性能。
其它引用类型使用 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 ([])); console .log (typeJudge ({})); console .log (typeJudge (new Date ())); console .log (typeJudge (null )); console .log (typeJudge (undefined )); console .log (typeJudge (123 )); console .log (typeJudge ('123' ));
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));
注意: 该属性可以被 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 .defineProperty (a, Symbol .toStringTag , { value : 'B' , }); console .log (Object .prototype .toString .call (a));
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)); console .log (isPromise (pp));
等比较 对于 ==
和 ===
已经非常熟悉了。
==
宽松相等 ,会进行隐式类型转换 ,并按照 IEEE754 标准对 NaN、-0 和 +0 进行特殊处理(故 NaN != NaN,且 -0 == +0),IsLooselyEqual
===
严格相等 ,与 ==
逻辑相同,但不会 进行隐式类型转换 。如果类型不同,则返回 false。IsStrictlyEqual
JavaScript 中的相等性判断-MDN
1 2 3 4 console .log (null == undefined ) console .log (null === undefined ) console .log (NaN == NaN ) console .log (+0 == -0 )
Object.is()
方法判断两个值是否是相同的值。与 ===
类似,但是对于 NaN 和 +0 和 -0 的判断有所不同。使用SameValue
Object.is(NaN, NaN)
返回 true
,而 NaN == NaN
返回 false
Object.is(+0, -0)
返回 false
,而 +0 == -0
返回 true
1 2 console .log (Object .is (NaN , NaN )) console .log (Object .is (+0 , -0 ))
indexOf 和 includes indexOf
无法判断 NaN,因为使用的是严格相等。而 includes
可以判断 NaN,使用 SameValueZero 算法,NaN等于NaN。
1 2 3 4 const arr = [NaN ];console .log (arr.indexOf (NaN )) console .log (arr.includes (NaN )) console .log (arr.findIndex ((val ) => Object .is (val, NaN )))
拷贝 拷贝,也就是复制数据,对于基本数据类型 ,直接赋值多个变量也互不影响。 但对于引用类型 ,赋值是浅拷贝 ,多个变量保存同一个引用,指向同一个堆空间,修改其中一个变量,会影响到其它变量。而深拷贝 ,就是完全复制一个对象,包括其内部的对象,开辟新的堆空间,多个变量互不影响。
直接赋值就是复制栈空间的值,对于基本类型,栈存放其数据值,而引用类型,栈存放其引用地址,指向实际存放对象数据的堆内存,所以直接赋值是浅拷贝。
总之,基本类型的浅拷贝复制的就是数据值,而引用类型的浅拷贝,复制的是引用地址。
浅拷贝 除了直接赋值,还有一些方法可以实现浅拷贝。
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); obj2.name = '456' ; console .log (obj1.name ); obj2.data .val = 2 ; console .log (obj1.data .val );
展开运算符… 与 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 );
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 );
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); arr3[0 ].val = 4 ; arr3[1 ].name = 'bbb' ; console .log (arr1[0 ].val ); console .log (arr2[0 ].name );
手写函数 直接遍历对象属性,再添加给新对象。
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 );
缺点:
无法复制函数、正则、Date、undefined、Symbol等内置对象。
无法解析循环引用的对象,会报错。
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); console .log (obj2.a instanceof Date );
第二参数 真的没办法复制函数和其它数据类型吗?其实可以。JSON.parse
和 JSON.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); 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); obj3.fn ();
借助这第二个参数,完全可以实现任何对象的深拷贝。但为了更加灵活可控,还是需要手写递归。
手写递归 深拷贝需要找到对象多层嵌套的最深层的基础数据类型,很显然,需要用到递归。
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); console .log (obj2);
循环引用 手写深拷贝目的之一就是解决循环引用,例如原型链等就存在循环引用。
可以使用 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.set (target, cloneTarget); 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); console .log (obj2); console .log (obj1.obj === obj2.obj );
遍历性能优化 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); } console .timeEnd (); console .time ();Object .keys (obj).forEach ((key ) => { console .log (key); }); console .timeEnd (); console .time ();const keys1 = Object .keys (obj);const length1 = keys1.length ;for (let i = 0 ; i < length1; i++) { console .log (keys1[i]); } console .timeEnd (); console .time ();let i = -1 ;const keys2 = Object .keys (obj);const length2 = keys2.length ;while (++i < length2) { console .log (keys2[i]); } console .timeEnd ();
封装一个通用的遍历函数,用于遍历对象和数组。
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 ) { 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); }); const arr = [1 , 2 , 3 ];forEach (arr, (val, index ) => { console .log (val, index); });
修改原来的深拷贝函数,使用封装的 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.set (target, cloneTarget); forEach (target, (val, key ) => { cloneTarget[key] = deepClone (val, map); }); return cloneTarget; } else { return target; } } function forEach (obj, callback ) { 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 (); }
想要处理其它数据类型,就需要掌握它们的特性,这也是为什么,本文先讲数据类型再讲拷贝的原因。
可继续遍历的类型: Object、Array、Map、Set,对于这种类型,可以和之前一样继续遍历其属性,递归深拷贝。
不可继续遍历的类型: Bool、Number、String、Date、Error、RegExp这几种类型可以直接用其构造函数和原始数据创建一个新对象。
需要注意的点:
由基本包装类型的构造函数 new 出来的对象,是 object 类型。
Map 和 Set 虽然可以直接使用原数据 new 一个新的对象,但是其内部的数据可能是引用类型,所以还是需要递归深拷贝。
函数和错误类型通常不需要深拷贝,深拷贝没有意义,直接返回即可。
完整代码 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 ) { 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; } let cloneTarget = {}; if (canTraverse (type)) { const Ctor = getConstructor (target); cloneTarget = new Ctor (); } 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);
当然还有一些问题没解决:
对象的原型链没处理
对象的属性描述符没处理
如果是dom节点,应该使用 cloneNode
方法
对象不可枚举的属性也没处理
structuredClone 写递归太麻烦了,也许可以引入第三方库,比如lodash的_.cloneDeep方法。
JS本身也提供了深拷贝全局 方法 structuredClone
,且支持循环引用。需要注意兼容性 ,Chrome98、Node17 等以上才支持
具有两个参数:
value
要克隆的对象:任意结构化可克隆类型。
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);obj2.a .b = 2 ; console .log (obj1.a .b );
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 ); 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 ); 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); console .log (res); });