数据类型
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。
console.log(null == undefined); // trueconsole.log(null === undefined); // falsenull 转为数字时为 0,undefined 转为数字时为 NaN。
console.log(+null) // 0console.log(+undefined) // NaN判断类型
typeof
typeof 运算符,返回数据类型的字符串,对于引用类型,除了函数,都会返回 object。
console.log(typeof '123'); // stringconsole.log(typeof 123); // numberconsole.log(typeof 123n); // bigintconsole.log(typeof NaN); // numberconsole.log(typeof true); // booleanconsole.log(typeof Symbol()); // symbolconsole.log(typeof undefined); // undefinedconsole.log(typeof undeclaredVariable); // undefined// console.log(typeof document.all) // undefined// 对象console.log(typeof null); // objectconsole.log(typeof []); // objectconsole.log(typeof {}); // objectconsole.log(typeof new Date()); // objectconsole.log(typeof /123/); // object// 函数console.log(typeof new Function()); // functionconsole.log(typeof (()=>{})); // functionconsole.log(typeof class A{}); // functionnull与object: 在 JS 的最初版本中,还广泛使用着32位系统,JS 用32个二进制位标识值,其中低三位表示值的类型。
typeof通过判断存储的机器码的低三位来进行类型判断。
数据类型 机器码标识对象(Object) 000整数 1浮点数 010字符串 100布尔 110undefined -2^31(全为 1 的 32 位带符号整数)null 全为0null的机器码标识为全0,而对象的机器码低位标识为000。所以typeof null被误判为Object。
即使在 ES6 时有提案要修复这个bug,但因为兼容性问题被否决,这已经是一个**特性(feature)**不再被修复。
甚至后来出现了typeof null等于undefined的bug,被光速改回了原来的样子。
constructor
对象原型上的 constructor 属性,返回实例对象的构造函数的引用。
通过 constructor 判断对象数据类型,但这种方式并不可靠,因为可以通过更改原型链来更改 constructor 属性。
const arr = [1, 2, 3, 4, 5];console.log(arr.constructor === Array); // trueconsole.log(arr.constructor === Object); // false// 改写原型const obj = {};Object.setPrototypeOf(arr, obj);console.log(arr.constructor === Array); // falseconsole.log(arr.constructor === Object); // trueinstanceof
instanceof 运算符判断一个实例是否属于某种类型。
即某个构造函数的 prototype 属性(原型对象)是否出现在某个实例对象的原型链上。
class A {}class B extends A {}const b = new B();console.log(b instanceof B); // trueconsole.log(b instanceof A); // true继承形成了两条原型链:
- 实例的原型对象的
__proto__指向父类的原型对象,这是为了继承父类的方法(实例属性在创建对象时,直接作为对象自己的属性实例化,无需通过原型链继承,而实例方法,都在原型上,所以只有实例方法需要通过原型链继承,向上查找) - 构造函数的
__proto__指向父类的构造函数,这是为了继承父类的静态属性和方法。
console.log(B.prototype.__proto__ === A.prototype); // trueconsole.log(B.__proto__ === A); // true而 instanceof 就是通过判断实例的原型链上是否有某个构造函数的 prototype 属性(原型对象)来判断实例的类型。
原型链上最后一个原型对象是 Object.prototype,所以所有的实例都属于 Object 类型,也就是万物皆对象。
类本质是构造函数的语法糖,所以也是 Function 类型。
console.log(b instanceof Object); // trueconsole.log(B instanceof Function); // falseconsole.log(B instanceof Object); // true之前的笔记 原型与原型链
手写instanceof
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;class A {}class B extends A {}const b = new B();class C {}console.log(b.myInstanceOf(B)); // trueconsole.log(b.myInstanceOf(A)); // trueconsole.log(b.myInstanceOf(Object)); // trueconsole.log(b.myInstanceOf(C)); // falseconsole.log(B.myInstanceOf(Function)); // trueconsole.log(B.myInstanceOf(Object)); // true基本包装类型
基本数据类型没有原型链,所以 instanceof 无法判断基本数据类型。
但通过对应基本包装类型创建的实例,可以通过 instanceof 判断。
三种基本包装类型:String, Number, Boolean。
const s = new String('111');const n = new Number(111);console.log(s instanceof String); // trueconsole.log(n instanceof Number); // trueconst ss = '111';const nn = 111;console.log(ss instanceof String); // falseconsole.log(nn instanceof Number); // false基本数据类型在调用属性和方法时,会进行装箱操作,把基本类型用它们相应的引用类型包装起来,使其具有对象的性质。会产生一些临时对象。
is方法
isPrototypeOf 判断一个对象是否是另一个对象的原型。通用会在原型链上查找。
class A {}class B extends A {}const b = new B();const bp = Object.getPrototypeOf(b);console.log(bp.isPrototypeOf(b)); // trueconsole.log(A.prototype.isPrototypeOf(b)); // true其它基本包装类型和内置类型也提供了一些 is 方法
console.log(Array.isArray([])); // trueconsole.log(Array.isArray({})); // falseconsole.log(Number.isNaN(NaN)); // trueconsole.log(Number.isNaN(123)); // falseconsole.log(Number.isInteger(123)); // trueconsole.log(Number.isInteger(123.1)); // falseconsole.log(Number.isFinite(Infinity)) // falseisNaN 和 isFinite
全局和 Number 对象上都有 isNaN 和 isFinite 方法,用于判断是否是 NaN 和 有限数。但全局方法会进行隐式类型转换再判断,而 Number 对象上的方法则不会。
console.log(isNaN(NaN)) // trueconsole.log(isNaN({})) // true, Number({}) => NaNconsole.log(isNaN('NaN')) // true, Number('NaN') => NaNconsole.log(isNaN('123')) // false, Number('123') => 123console.log(Number.isNaN(NaN)) // trueconsole.log(Number.isNaN({})) // falseconsole.log(Number.isNaN('NaN')) // falseconsole.log(Number.isNaN('123')) // false
console.log(isFinite({})) // falseconsole.log(isFinite(Infinity)) // falseconsole.log(isFinite('123')) // true, Number('123') => 123console.log(Number.isFinite({})) // falseconsole.log(Number.isFinite(Infinity)) // falseconsole.log(Number.isFinite('123')) // falsetoString
Object.prototype.toString.call() 以 [object xxx] 的字符串形式返回当前调用者的对象类型。是最稳妥的类型判断方式。
必须调用 Object 显式原型上的 toString 方法,因为其它数据类,都重写了 toString 方法,返回的不再是 [object xxx] 类型字符串。
function typeToString(any) { return Object.prototype.toString.call(any).slice(8, -1);}console.log(typeToString([])); // Arrayconsole.log(typeToString((()=>{}))); // Functionconsole.log(typeToString(123)); // Numberconsole.log(typeToString(Symbol('a'))) // Symbol这种方法只能判断内置类型和基本包装类型,即 Arguments, Array, Boolean, Date, Error, Function, JSON, Math, Number, Object, RegExp 和 String。
const a = new class A {};console.log(typeToString(a)); // Object对于基本数据类型,会进行装箱操作,返回对应的引用类型。这会产生一些临时对象,所以通常需要配合 typeof 来判断类型,提高性能。
为什么Object.prototype.toString.call()可以如此准确的判断对象类型?
封装类型判断函数
- null 比较特殊,typeof 会返回 object,先严格比较后返回 null。
- 基本数据类型直接使用 typeof 获取类型,避免装箱操作,提高性能。
- 其它引用类型使用
Object.prototype.toString.call()获取类型。
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([])); // arrayconsole.log(typeJudge({})); // objectconsole.log(typeJudge(new Date())); // dateconsole.log(typeJudge(null)); // nullconsole.log(typeJudge(undefined)); // undefinedconsole.log(typeJudge(123)); // numberconsole.log(typeJudge('123')); // stringSymbol.toStringTag
Object.prototype.toString 会读取一个对象的 Symbol.toStringTag 属性,该属性返回值将作为 [object xxx] 中的 xxx。用于创建对象的默认字符串描述。
应该返回一个字符串,否则会返回默认值,即 [object Object]。
可以用于标识自定义类型。
class A { get [Symbol.toStringTag]() { return this.constructor.name; }}const a = new A();console.log(Object.prototype.toString.call(a)); // [object A]**注意:**该属性可以被 defineProperty 修改。
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 对象。
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)); // trueconsole.log(isPromise(pp)); // true等比较
对于 == 和 === 已经非常熟悉了。
==宽松相等,会进行隐式类型转换,并按照 IEEE754 标准对 NaN、-0 和 +0 进行特殊处理(故 NaN != NaN,且 -0 == +0),IsLooselyEqual===严格相等,与==逻辑相同,但不会进行隐式类型转换。如果类型不同,则返回 false。IsStrictlyEqual
console.log(null == undefined) // trueconsole.log(null === undefined) // falseconsole.log(NaN == NaN) // falseconsole.log(+0 == -0) // trueObject.is() 方法判断两个值是否是相同的值。与 === 类似,但是对于 NaN 和 +0 和 -0 的判断有所不同。使用SameValue
Object.is(NaN, NaN)返回true,而NaN == NaN返回falseObject.is(+0, -0)返回false,而+0 == -0返回true
console.log(Object.is(NaN, NaN)) // trueconsole.log(Object.is(+0, -0)) // falseindexOf 和 includes
indexOf 无法判断 NaN,因为使用的是严格相等。而 includes 可以判断 NaN,使用 SameValueZero 算法,NaN等于NaN。
const arr = [NaN];console.log(arr.indexOf(NaN)) // -1console.log(arr.includes(NaN)) // trueconsole.log(arr.findIndex((val) => Object.is(val, NaN))) // 0拷贝
拷贝,也就是复制数据,对于基本数据类型,直接赋值多个变量也互不影响。 但对于引用类型,赋值是浅拷贝,多个变量保存同一个引用,指向同一个堆空间,修改其中一个变量,会影响到其它变量。而深拷贝,就是完全复制一个对象,包括其内部的对象,开辟新的堆空间,多个变量互不影响。
直接赋值就是复制栈空间的值,对于基本类型,栈存放其数据值,而引用类型,栈存放其引用地址,指向实际存放对象数据的堆内存,所以直接赋值是浅拷贝。
总之,基本类型的浅拷贝复制的就是数据值,而引用类型的浅拷贝,复制的是引用地址。
浅拷贝
除了直接赋值,还有一些方法可以实现浅拷贝。
Object.assign
Object.assign() 用于将任意多个对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。
第一层基本类型的属性是将值本身复制一份,而引用类型的属性是浅拷贝。
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 类似,也是浅拷贝。
const obj1 = { name: '123', data: { val: 1 }};const obj2 = { ...obj1 };obj2.data.val = 2;console.log(obj1.data.val); // 2Array.prototype.slice
slice 方法返回一个新的数组,返回原数组指定范围(左闭右开)元素的浅拷贝。
const arr1 = [1, 2, { val: 3 }];const arr2 = arr1.slice();arr2[2].val = 4;console.log(arr1[2].val); // 4Array.prototype.concat
concat 方法用于合并若干个数组,返回一个新数组,也是浅拷贝。
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); // 4console.log(arr2[0].name); // bbb手写函数
直接遍历对象属性,再添加给新对象。
function clone(target) { let cloneTarget = {}; for (const key in target) { cloneTarget[key] = target[key]; } return cloneTarget;};深拷贝
浅拷贝是常见的,而为了实现引用数据类型的深拷贝,需要借助其它的方法。
JSON方法
JSON.parse(JSON.stringify()) 先将对象转为JSON字符串,再将JSON转为对象。
const obj = { data: { val: 1 }}const obj2 = JSON.parse(JSON.stringify(obj));obj2.data.val = 2;console.log(obj.data.val); // 1缺点:
- 无法复制函数、正则、Date、undefined、Symbol等内置对象。
- 无法解析循环引用的对象,会报错。
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.parse 和 JSON.stringify 可以传入第二个参数,是一个函数,用于自定义过滤和转换结果。
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借助这第二个参数,完全可以实现任何对象的深拷贝。但为了更加灵活可控,还是需要手写递归。
手写递归
深拷贝需要找到对象多层嵌套的最深层的基础数据类型,很显然,需要用到递归。
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属性,避免内存泄漏。
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 循环性能最好。
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封装一个通用的遍历函数,用于遍历对象和数组。
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 遍历对象。
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等,需要根据其特性进行特殊处理
判断数据类型的函数,在前面已经封装过了,这里直接拿来用。
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 一个新的对象,但是其内部的数据可能是引用类型,所以还是需要递归深拷贝。
- 函数和错误类型通常不需要深拷贝,深拷贝没有意义,直接返回即可。
// 遍历函数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]} */当然还有一些问题没解决:
- 对象的原型链没处理
- 对象的属性描述符没处理
- 如果是dom节点,应该使用
cloneNode方法 - 对象不可枚举的属性也没处理
structuredClone
写递归太麻烦了,也许可以引入第三方库,比如lodash的_.cloneDeep方法。
JS本身也提供了深拷贝全局方法 structuredClone,且支持循环引用。需要注意兼容性,Chrome98、Node17 等以上才支持
具有两个参数:
value要克隆的对象:任意结构化可克隆类型。transfer可转移的对象的数组:其中的可转移对象将被移动到新的对象,而不是克隆至新的对象。
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); // 1structuredClone 使用结构化克隆算法,不能克隆函数、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
if (typeof MessageChannel === 'function') { var channel = new MessageChannel(); var port = channel.port2; channel.port1.onmessage = nextHandler; port.postMessage(1);}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 则会自动开始接收消息。在开始接收消息前,另一个端口发送的消息会进入缓冲区。
消息在发送和接收的过程需要序列化和反序列化,可以实现深拷贝,同时也意味着消息只能基本类型或结构化可克隆对象。
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 封装深拷贝函数。
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] }});
评论