JavaScript
包含两种不同数据类型的值:
- 基本类型(原始值):
string
、number
、boolean
、null
、undefined
、symbol
、bigInt
- 引用类型:
Object(object、array、function)
在将一个值赋给变量时,解析器必须确定这个值是基本类型还是引用类型
- 基本数据类型是
按值访问
的,因为可以操作保存在变量中的实际的值 - 引用类型是存储在内存中的对象,栈内存储的是变量的标识符、对象在堆內的存储地址(JS 不允许直接访问内存中的位置,即不能直接操作对象的内存空间,因此在操作对象时实际上是对操作对象的引用而不是实际的对象),当需要访问引用类型(如对象、数组等)的值时,首先从栈中获得该对象的地址指针,然后再从对应的堆内存中取得所需数据
JavaScript 的变量存储方式:
栈(stack)
:自动分配内存空间,系统自动释放,里面存放的是基本类型的值和引用类型的地址指针堆(heap)
:动态分配内存,大小不定,也不会自动释放,里面存放引用类型的值
基本类型与引用类型最大的区别实际就是传值与传址的区别
-
值传递
:基本类型采用的是值传递let a = 1; let b = a; b++; console.log(a, b) // 1, 2
-
址传递
:引用类型则是地址传递,将存放在栈内存中的地址赋值给接收的变量let a = ['a', 'b', 'c']; let b = a; b.push('d'); console.log(a) // ['a', 'b', 'c', 'd'] console.log(b) // ['a', 'b', 'c', 'd']
上面代码中,
a
是数组是引用类型,赋值给b
就是将a
的地址赋值给b
,因此a
和b
指向同一个地址(该地址都指向了堆内存中引用类型的实际的值)当
b
改变这个值时,因为a
的地址也指向了这个值,故a
的值也跟着变化,就好比a
租了一间房,将房间的地址给了b
,b
通过地址找到了房间,则b
对房间做的任何改变对a
来说肯定同样是可见的
这里就引出了浅拷贝
或深拷贝
了,JS
的基本类型不存在浅拷贝
还是深拷贝
的问题,主要是针对引用类型
浅拷贝与深拷贝
-
浅拷贝
:级别浅,指的是创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝,若属性是基本类型,拷贝的就是基本类型的值,若属性是引用类型,拷贝的就是内存地址(即,浅拷贝是指复制对象时只对第一层键值对进行复制,若对象内还有对象则只能复制嵌套对象的地址指针,新旧对象还是共享同一块内存)浅拷贝的缺点:当有一个属性是引用值(数组或对象)时,按照这种克隆方式,只是把这个引用值的地址指针赋给了新的目标对象,一旦改变了源对象或目标对象的该引用值属性,另一个也会跟着改变
-
深拷贝
:拷贝级别更深,指复制对象时是完全拷贝,将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,新对象跟原对象不共享内存,因此拷贝后两者也相互不影响,修改新对象不会影响原对象
总结:前提都是拷贝类型为引用类型,从上面可以看出,深浅拷贝都创建出一个新对象,但在复制对象属性时行为不一样
-
浅拷贝
:拷贝前后对象的基本数据类型互不影响,但因只复制属性指向某个对象的内存指针,而不是复制对象本身,拷贝前后对象的引用类型因共享同一块内存,修改引用属性值会互相影响 -
深拷贝
:对对象中的子对象进行递归拷贝,另外开辟一块新区域存放,拷贝前后的两个对象指向不同的地址,不共享内存,修改互不影响
浅拷贝的实现
Object.assign()
该方法可以把任意多个的源对象自身的可枚举属性
拷贝给目标对象,然后返回目标对象,Object assign()
对对象的拷贝还是浅拷贝
let arr = {
a: 'one',
b: 'two',
c: 'three'
};
let newArr = Object.assign({}, arr)
newArr.d = 'four'
console.log(arr); // {a: "one", b: "two", c: "three"}
console.log(newArr); // {a: "one", b: "two", c: "three", d: "four"}
let arr = {
a: 'one',
b: 'two',
c: {a: 1}
};
let newArr = Object.assign({}, arr);
newArr.c.a = 3;
console.log(arr); // {a: "one", b: "two", c: {a: 3}}
console.log(newArr); // {a: "one", b: "two", c: {a: 3}}
具体详见 Object.assign 原理及其实现
Array.prototype.slice()、
Array.prototype.concat()
若是数组,数组元素均为基本数据类型,可利用数组的一些方法如 slice
、concat
返回一个新数组的特性来实现拷贝;若数组的元素是引用类型(Object,Array),slice
和 concat
对对象数组的拷贝还是浅拷贝,拷贝之后数组各个元素的指针还是指向相同的存储地址
let arr = ['one', 'two', 'three'];
let newArr = arr.concat();
newArr.push('four');
console.log(arr); // ["one", "two", "three"]
console.log(newArr); // ["one", "two", "three", "four"]
let arr = ['one', 'two', 'three'];
let newArr = arr.slice();
newArr.push('four');
console.log(arr); // ["one", "two", "three"]
console.log(newArr); // ["one", "two", "three", "four"]
let arr = [{a:1}, 'two', 'three'];
let newArr = arr.concat();
newArr[0].a = 2;
console.log(arr); // [{a: 2},"two","three"]
console.log(newArr); // [{a: 2},"two","three"]
展开运算符 ...
展开运算符是一个 ES6
特性,它提供了一种非常方便的方式来执行浅拷贝,与 Object.assign()
的功能相同
let obj1 = {
name: 'Kobe',
address:{x:100, y:100}
};
let obj2= {...obj1};
obj1.address.x = 200;
obj1.name = 'wade';
console.log(obj1); // {name: "wade", address: {x: 200, y: 100}}
console.log(obj2); // {name: "Kobe", address: {x: 200, y: 100}}
lodash 库的 _.clone 方法
该函数库也有提供 _.clone
用来做 ShallowCopy
var _ = require('lodash');
var obj1 = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3]
};
var obj2 = _.clone(obj1);
console.log(obj1.b.f === obj2.b.f); // true
浅拷贝手写封装
创建一个新的对象,遍历需要克隆的对象,将需要克隆对象的属性依次添加到新对象上,返回
function shallowClone(obj) {
if(typeof obj !== 'object') return;
// 根据 obj 类型判断是新建一个数组还是对象
let newObj = Object.prototype.toString.call(obj) == '[objecj Array]' ? [] : {};
for(let prop in obj) {
if(obj.hasOwnProperty(prop)) {
newObj[prop] = obj[prop];
}
}
return newObj;
}
深拷贝的实现
JSON.parse(JSON.stringify(arr))
不仅适用于数组还适用于对象。利用 JSON.stringify
将对象转成 JSON
字符串,再用 JSON.parse
把字符串解析成对象,一去一来,新的对象产生了,而且会开辟新的内存空间存放,开辟新的栈存放不同的内存指针,实现深拷贝
let a = {
name: "tn",
book: {
title: "JS",
price: "45"
}
};
let b = JSON.parse(JSON.stringify(a));
console.log(b); // {name: "tn", book: {title: "JS", price: "45"}}
a.name = "change";
a.book.price = "55";
console.log(a); // {name: "change", book: {title: "JS", price: "55"}}
console.log(b); // {name: "tn", book: {title: "JS", price: "45"}}
// 对数组深拷贝后,改变原数组也不会影响到拷贝后的数组
let a = [0, "1", [2, 3]];
let b = JSON.parse(JSON.stringify( a.slice(1) ));
console.log(b); // ["1", [2, 3]]
a[1] = "99";
a[2][0] = 4;
console.log(a); // [0, "99", [4, 3]]
console.log(b); // ["1", [2, 3]]
由上代码可以看出,改变变量 a
中的引用属性后对 b
没有任何影响,这就是深拷贝的魔力
这种方法虽然可以实现数组或对象深拷贝,但该方法有局限性:
- 会忽略
undefined
、symbol
- 不能序列化函数
- 不能解决循环引用的对象
- 不能正确处理
new Date()
- 不能处理正则
let obj = {
name: "tn",
a: undefined,
b: Symbol("tn"),
c: function() {}
}
let b = JSON.parse(JSON.stringify(obj));
console.log(b); // {name: "tn"}
// 数组中函数会变成 null
let arr = [1, 3, {username: 'kobe'}, function(){}];
let newArr = JSON.parse(JSON.stringify(arr));
newArr[2].username = 'duncan';
console.log(arr); // [1, 3, {username: "kobe"}, ƒ ()]
console.log(newArr); // [1, 3, {username: 'duncan'}, null]
// 循环引用情况下会报错
let obj = {
a: 1,
b: {c: 2, d: 3}
}
obj.a = obj.b;
obj.b.c = obj.a;
let b = JSON.parse(JSON.stringify(obj)); // Uncaught TypeError: Converting circular structure to JSON
// `new Date` 情况下转换结果不正确
new Date(); // Fri Nov 18 2022 02:04:11 GMT+0800 (China Standard Time)
JSON.stringify(new Date()); // '"2022-11-17T18:04:16.747Z"'
JSON.parse(JSON.stringify(new Date())); // '2022-11-17T18:04:20.190Z'
// 以上解决方法转成字符串或时间戳
let date = (new Date()).valueOf();
JSON.parse(JSON.stringify(date));
// 正则情况下,变为空对象
let obj = {name: "tn", a: /'123'/};
console.log(obj); // {name: "tn", a: /'123'/}
let b = JSON.parse(JSON.stringify(obj));
console.log(b); // {name: "tn", a: {}}
lodash 库 _.cloneDeep 方法
var _ = require('lodash');
var obj1 = {a: 1, b: {f: {g: 1}}, c: [1, 2, 3]};
var obj2 = _.cloneDeep(obj1);
console.log(obj1.b.f === obj2.b.f); // false
jQuery.extend() 方法
用法
$.extend(deepCopy, target, object1, [objectN]) // 第一个参数为 true 就是深拷贝
var $ = require('jquery');
var obj1 = {a: 1, b: {f: {g: 1}}, c: [1, 2, 3]};
var obj2 = $.extend(true, {}, obj1);
console.log(obj1.b.f === obj2.b.f); // false
深拷贝手写封装
在拷贝时判断一下属性值的类型,若是对象则递归调用深拷贝函数,深拷贝是完全拷贝了原对象的内容并寄存在新的内存空间,指向新的内存地址
function deepClone(obj) {
if (obj === null) return obj;
if (typeof obj !== "object") return obj;
let target = Object.prototype.toString.call(obj) == '[object Array]' ? [] : {};
for (let prop in obj) {
if (obj.hasOwnProperty(prop)) {
target[prop] = deepClone(obj[prop]);
}
}
return target;
}
// test
var a = {
name: "tn",
book: {
title: "JS",
price: "45"
},
a1: undefined,
a2: null,
a3: 123,
field: [2, 4, 8]
};
var b = deepClone(a);
console.log(b);
// {
// a1: undefined,
// a2: null,
// a3: 123,
// book: {
// title: "JS",
// price: "45"
// },
// name: "tn",
// field: [2, 4, 8]
// }
解决循环引用
有种特殊情况需注意就是对象可能存在循环引用
的情况,即对象的属性间接或直接的引用了自身的情况,结果是容易进入死循环导致栈内存溢出
为了解决循环引用问题,我们可以额外开辟一个存储空间,来存储当前对象和拷贝对象的对应关系,当需要拷贝当前对象时,先去存储空间中找,有没有拷贝过这个对象,若有则直接返回,若没有则继续拷贝,这样就巧妙化解的循环引用的问题
这个存储空间,需要可以存储 key-value
形式的数据,且 key
可以是一个引用类型,我们可以选择 Map
这种数据结构:
- 检查
map
中有无克隆过的对象 - 有
-
直接返回 - 没有
-
将当前对象作为key
,克隆对象作为value
进行存储 - 继续克隆
function deepClone(obj, map = new Map()) {
if (obj === null) return obj;
if (typeof obj !== "object") return obj;
let target = Object.prototype.toString.call(obj) == '[object Array]' ? [] : {};
if (map.get(obj)) {
return map.get(obj);
}
map.set(obj, target);
for (let prop in obj) {
if (obj.hasOwnProperty(prop)) {
target[prop] = deepClone(obj[prop], map);
}
}
return target;
}
接下来,可以使用 WeakMap
代替 Map
。为什么要这样做?先来看看 WeakMap
的作用:WeakMap
对象是一组键/值对的集合,其键必须是对象,值可以是任意的,且键是弱引用
什么是弱引用?
- 在计算机程序设计中,弱引用与强引用相对的,是指不能确保其引用的对象不会被垃圾回收器回收的引用
- 一个对象若只被弱引用所引用,则被认为是不可访问(或弱可访问)的,并因此可能在任何时刻被回收
创建一个对象 const obj = {}
默认创建了一个强引用
的对象,只有手动将 obj = null
,它才会被垃圾回收机制进行回收,而若是弱引用
对象,垃圾回收机制会自动进行回收
若使用 Map
则对象间是存在强引用关系的:
let obj = {name : 'tn'};
const target = new Map();
target.set(obj,'code');
obj = null;
虽然手动将 obj
进行释放,target
依然对 obj
存在强引用
关系,所以这部分内存依然无法被释放
再来看 WeakMap
:
let obj = {name : 'tn'};
const target = new WeakMap(); target.set(obj,'code');
obj = null;
若是 WeakMap
则 target
和 obj
存在的就是弱引用
关系,当下一次垃圾回收机制执行时这块内存就会被释放掉
若要拷贝的对象非常庞大时,使用 Map
会对内存造成非常大的额外消耗,而且需要手动清除 Map
的属性才能释放这块内存,而 WeakMap
会巧妙化解这个问题
因此可以使用 WeakMap
来解决循环引用问题
function deepClone(obj, map = new WeakMap()) {
if (obj === null) return obj;
if (typeof obj !== "object") return obj;
let target = Object.prototype.toString.call(obj) == '[object Array]' ? [] : {};
if (map.has(obj)) {
return map.get(obj);
}
map.set(obj, target);
for (let prop in obj) {
if (obj.hasOwnProperty(prop)) {
target[prop] = deepClone(obj[prop], map);
}
}
return target;
}
上面使用了 ES6
中的 WeakMap
来处理,在 ES5
下可以使用数组来处理
function deepClone(obj, uniqueList) {
if (obj === null) return obj;
if (typeof obj !== "object") return obj;
let target = Object.prototype.toString.call(obj) == '[object Array]' ? [] : {};
if (!uniqueList) uniqueList = []; // 新增代码,初始化数组
// 数据已经存在,返回保存的数据
var uniqueData = find(uniqueList, src);
if (uniqueData) {
return uniqueData.target;
};
// 数据不存在,保存源数据,以及对应的引用
uniqueList.push({
source: src,
target: target
});
for (let prop in obj) {
if (obj.hasOwnProperty(prop)) {
target[prop] = deepClone(obj[prop], uniqueList);
}
}
return target;
}
// 用上面用例测试 OK
拷贝 Symbol
这里主要针对对象的键是 Symbol
的情况
Symbol
在 ES6
下才有,需要一些方法来检测出 Symble
类型
-
方法一:
Object.getOwnPropertySymbols(...)
该方法可以查找一个给定对象的符号属性时返回一个 symbol 类型的数组。注意:每个初始化的对象都是没有自己的 symbol 属性的,因此这个数组可能为空,除非你已经在对象上设置了 symbol 属性 -- MDN
var obj = {}; var a = Symbol("a"); // 创建新的 symbol 类型 var b = Symbol.for("b"); // 从全局的 symbol 注册表设置和取得 symbol obj[a] = "localSymbol"; obj[b] = "globalSymbol"; var objectSymbols = Object.getOwnPropertySymbols(obj); console.log(objectSymbols.length); // 2 console.log(objectSymbols) // [Symbol(a), Symbol(b)] console.log(objectSymbols[0]) // Symbol(a)
思路就是先查找有没有
Symbol
属性,若查找到则先遍历处理Symbol
情况,然后再处理正常情况function deepClone(obj, map = new WeakMap()) { if (obj === null) return obj; if (typeof obj !== "object") return obj; let target = Object.prototype.toString.call(obj) == '[object Array]' ? [] : {}; if (map.has(obj)) { return map.get(obj); } map.set(obj, target); let symKeys = Object.getOwnPropertySymbols(obj); // 查找 if (symKeys.length) { // 查找成功 symKeys.forEach(symKey => { target[symKey] = deepClone(obj[symKey], map); }) } for (let prop in obj) { if (obj.hasOwnProperty(prop)) { target[prop] = deepClone(obj[prop], map); } } return target; } var a = { name: "tn", book: { title: "JS", price: "45" }, a1: undefined, a2: null, a3: 123, field: [2, 4, 8] }; var sym1 = Symbol("a"); // 创建新的 symbol 类型 var sym2 = Symbol.for("b"); // 从全局的symbol注册表设置和取得 symbol a[sym1] = "localSymbol"; a[sym2] = "globalSymbol"; var b = deepClone(a); console.log(b); // { // a1: undefined // a2: null // a3: 123, // book: {title: "JS", price: "45"}, // field: (3) [2, 4, 8], // name: "tn", // Symbol(a): "localSymbol", // Symbol(b): "globalSymbol" // }
-
方法二:
Reflect.ownKeys(...)
返回一个由目标对象自身的属性键组成的数组。它的返回值等同于
Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target))
-- MDNReflect.ownKeys({z: 3, y: 2, x: 1}); // [ "z", "y", "x" ] Reflect.ownKeys([]); // ["length"] var sym = Symbol.for("comet"); var sym2 = Symbol.for("meteor"); var obj = { [sym]: 0, "str": 0, "773": 0, "0": 0, [sym2]: 0, "-1": 0, "8": 0, "second str": 0 }; Reflect.ownKeys(obj); // [ "0", "8", "773", "str", "-1", "second str", Symbol(comet), Symbol(meteor) ] // 注意顺序 // Indexes in numeric order, // strings in insertion order, // symbols in insertion order
这里使用了
Reflect.ownKeys()
获取所有的键值,同时包括Symbol
,对src
遍历赋值即可function deepClone(obj, map = new WeakMap()) { if (obj === null) return obj; if (typeof obj !== "object") return obj; let target = Object.prototype.toString.call(obj) == '[object Array]' ? [] : {}; if (map.has(obj)) { return map.get(obj); } map.set(obj, target); Reflect.ownKeys(obj).forEach(key => { target[key] = deepClone(obj[key], map); }) return target; } // 使用上面例子测试 OK
性能优化
在上面的代码中,遍历数组和对象都使用了 for in
方式,实际上 for in
在遍历时效率是非常低的,我们来对比下常见的三种循环 for、while、for in
的执行效率:
由上可以看到 while
的效率是最好的,因此可以想办法把 for in
遍历改变为 while
遍历
我们先使用 while
来实现一个通用的 forEach
遍历,callback
是遍历的回调函数,其可以接收每次遍历的 value
和 index
两个参数:
function forEach(arr, callback) {
let index = -1;
const length = arr.length;
while (++ index < length) {
callback(arr[index], index);
}
return arr;
}
下面对的 deepClone
函数进行改写,当遍历数组时直接使用 forEach
进行遍历,当遍历对象时先使用Object.keys
取出所有的 key
再进行遍历,然后在遍历时 forEach
会调函数的 value
当作 key
使用:
function deepClone(obj, map = new WeakMap()) {
if (obj === null) return obj;
if (typeof obj !== "object") return obj;
let target = Object.prototype.toString.call(obj) == '[object Array]' ? [] : {};
if (map.has(obj)) {
return map.get(obj);
}
map.set(obj, target);
const keys = isArray ? undefined : Reflect.ownKeys(obj);
forEach(keys || target, (value, key) => {
if(keys) {
key = value;
}
target[key] = deepClone(obj[key], map); });
return target;
}
兼容其他数据类型
首先,判断是否为引用类型
,需要考虑 function
和 null
两种特殊的数据类型:
function isObject(target) {
const type = typeof target;
return target !== null && (type === 'object' || type === 'function');
}
获取数据类型
我们可以使用 toString
来获取准确的引用类型:
每个引用类型都有
toString
方法,默认情况下toString()
方法被每个Object
对象继承。若此方法在自定义对象中未被覆盖,toString()
返回"[object type]"
,其中type
是对象的类型
注意,此方法在自定义对象中未被覆盖,
toString
才会达到预想的效果,事实上大部分引用类型如Array、Date、RegExp
等都重写了toString
方法
可直接调用 Object
原型上未被覆盖的 toString()
方法,使用 call
来改变 this
指向来达到想要的效果
function getType(target) {
return Object.prototype.toString.call(target);
}
以下抽离出一些常用的数据类型以便后面使用:
const mapType = '[object Map]';
const setType = '[object Set]';
const arrType = '[object Array]';
const objectType = '[object Object]';
const boolType = '[object Boolean]';
const dateType = '[object Date]';
const errorType = '[object Error]';
const numberType = '[object Number]';
const regexpType = '[object RegExp]';
const stringType = '[object String]';
const symbolType = '[object Symbol]';
在上面的集中类型中,我们简单将他们分为两类,分别为它们做不同的拷贝
-
可以继续遍历的类型
- 上面的
object
、array
、Map
,Set
等都属于可以继续遍历的类型,另外还有等都是可以继续遍历的类型,因为它们内存都还可以存储其他数据类型的数据,这里我们只考虑这四种 - 这几种类型还需要继续进行递归,首先需要获取它们的初始化数据,如
[]
和{}
,我们可以通过拿到new target.constructor()
的方式来通用的获取(注意:也可以使用Object.create(target.constructor.prototype)
,但这这种方式仅使用在Object/Array
上,若是Map/Set
还是需要使用new target.constructor()
来创建一个空集合,否则调用Map/Set
上的方法会出现问题,所以使用时在类型上还需要加以区别) - 如:
const target = {}
是const target = new Object()
的语法糖,这种方法还有一个好处:因为使用了原对象的构造方法,所以它可以保留对象原型上的数据,若直接使用普通的{}
则原型必然是丢失了的
function getInit(obj) { return new obj.constructor(); }
改写
deepClone
函数,对可继续遍历的数据类型进行处理:function deepClone(obj, map = new WeakMap()) { // 克隆原始类型 if (!isObject(obj)) return obj; // 初始化 const type = getType(obj); let target = getInit(obj); // 防止循环引用 if (map.has(obj)) return map.get(obj); map.set(obj, target); // 克隆 set if (type === setType) { obj.forEach(value => { target.add(deepClone(value, map)); }); return target; } // 克隆 map if (type === mapType) { obj.forEach((value, key) => { target.set(key, deepClone(value, map)); }); return target; } // 克隆对象和数组 const keys = type === arrType ? undefined : Reflect.ownKeys(obj); forEach(keys || target, (value, key) => { if(keys) { key = value; } target[key] = deepClone(obj[key], map); }); return target; } var a = { name: "tn", book: {title: "JS", price: "45"}, a1: undefined, a2: null, a3: 123, field: [2, 4, 8], }; var sym1 = Symbol("a"); // 创建新的 symbol 类型 var sym2 = Symbol.for("b"); // 从全局的symbol注册表设置和取得 symbol a[sym1] = "localSymbol"; a[sym2] = "globalSymbol"; var map = new Map(); var set = new Set(); map.set('a', 1); map.set('b', 2); set.add('a'); set.add(33); a.map = map; a.set = set; var b = deepClone(a); console.log(b); // { // a1: undefined, // a2: null, // a3: 123, // book: {title: 'JS', price: '45'}, // field: (3) [2, 4, 8], // map: Map(2) {'a' => 1, 'b' => 2}, // name: "tn", // set: Set(2) {'a', 33}, // Symbol(a): "localSymbol", // Symbol(b): "globalSymbol" // }
- 上面的
-
不可以继续遍历的类型
- 其他剩余的类型我们把它们统一归类成不可处理的数据类型,依次进行处理
Bool
、Number
、String
、String
、Date
、Error
、RegExp
这几种类型都可以直接用构造函数和原始数据创建一个新对象
// 克隆 symbol function cloneSymbol(obj) { return Object(Symbol.prototype.valueOf.call(obj)); } // 克隆正则 function cloneReg(obj) { const reFlags = /\w*$/; const result = new obj.constructor(obj.source, reFlags.exec(obj)); result.lastIndex = obj.lastIndex; return result; } function cloneOtherType(obj, type) { switch (type) { case boolType: case numberType: case stringType: case errorType: case dateType: return new obj.constructor(obj); case regexpTag: return cloneReg(obj); case symbolTag: return cloneSymbol(obj); default: return null; } }
-
简单处理函数
function cloneFunc(target) { const isFunc = typeof target == 'function'; return isFunc ? target : {}; }
当然处理函数涉及到多层柯里化、函数本身的原型等,这里只是简单返回
-
当然还有很多数据类型这里没有写到
完整代码
// 常见数据类型
const mapType = '[object Map]';
const setType = '[object Set]';
const arrType = '[object Array]';
const objectType = '[object Object]';
const boolType = '[object Boolean]';
const dateType = '[object Date]';
const errorType = '[object Error]';
const numberType = '[object Number]';
const regexpType = '[object RegExp]';
const stringType = '[object String]';
const symbolType = '[object Symbol]';
const funcType = '[object Function]';
// 可继续遍历的数据类型
const typeArr = [mapType, setType, arrType, objectType];
// 封装 while 循环
function forEach(arr, callback) {
let index = -1;
const len = arr.length;
while(++ index < len) {
callback && callback(arr[index], index)
}
return arr;
}
// 判断是否是引用类型
function isObject(target) {
const type = typeof target;
return target !== null && (type === 'object' || type === 'function');
}
// 获取实际数据类型
function getType(target) {
return Object.prototype.toString.call(target);
}
// 初始化被克隆对象
function getInit(obj) {
return new obj.constructor();
}
// 复制 symbol
function cloneSymbol(target) {
return Object(Symbol.prototype.valueOf.call(target));
}
// 复制正则
function cloneReg(target) {
const reFlags = /\w*$/;
const result = new target.constructor(target.source, reFlags.exec(target));
return result;
}
// 复制函数
function cloneFunc(target) {
const isFunc = typeof target == 'function';
return isFunc ? target : {};
}
// 复制其他类型
function cloneOtherType(target, type) {
switch(type) {
case boolType:
case numberType:
case stringType:
case dateType:
case errorType:
return new target.constructor(target);
case regexpType:
return cloneReg(target);
case symbolType:
return cloneSymbol(target);
case funcType:
return cloneFunc(target);
default:
return null;
}
}
// 深拷贝函数
function deepClone(obj, map = new WeakMap()) {
// 基本数据类型,直接返回
if(!isObject(obj)) return obj;
// 根据数据类型进行操作
const type = getType(obj);
let target;
if(typeArr.includes(type)) {
target = getInit(obj);
} else {
return cloneOtherType(obj, type);
}
// 防止循环引用
if(map.has(obj)) return obj;
map.set(obj, target);
// 处理 set 类型
if(type === setType) {
obj.forEach(value => {
target.add(deepClone(value));
})
return target;
}
// 处理 map 类型
if(type === mapType) {
obj.forEach((value, key) => {
target.set(key, deepClone(value));
})
return target;
}
// 处理对象和数组
const keys = type === arrType ? undefined : Object.keys(obj);
forEach(keys || obj, (value, key) => {
if(keys) {
key = value;
}
target[key] = deepClone(obj[key], map);
})
return target;
}
// test
var map = new Map();
map.set('a', 'sss');
map.set('b', 22);
var set = new Set();
set.add('tn');
set.add(33);
var a = {
name: "tn",
book: {title: "JS", price: "45"},
a1: undefined,
a2: null,
a3: 123,
field: [2, 4, 8],
map,
set,
bool: new Boolean(true),
num: new Number(2),
str: new String(2),
date: new Date(),
reg: /\d+/,
error: new Error(),
symbol: Object(Symbol(1)),
func1: () => { console.log('code秘密花园'); },
func2: function (a, b) { return a + b; }
};
var sym1 = Symbol("a"); // 创建新的 symbol 类型
var sym2 = Symbol.for("b"); // 从全局的symbol注册表设置和取得 symbol
a[sym1] = "localSymbol";
a[sym2] = "globalSymbol";
var b = deepClone(a);
console.log(b);
未来的深拷贝
其实,浏览器自己实现了深拷贝函数,这个 Web API
为 structuredClone()
,详情可访问 MDN 和 HTML5 规范
很显然,这是一个新的 API
,从兼容性来考虑,很多浏览器不支持
另外 MDN
也介绍了实现这个 API
用到的算法:Structured_clone_algorithm,感兴趣的可以去看看
应用场景
-
浅拷贝
- 对于一层结构的
Array
和Object
想要拷贝一个副本时使用 vue
的mixin
是浅拷贝的一种复杂型式
- 对于一层结构的
-
深拷贝
- 复制深层次的
object
数据结构,如想对某个数组或对象的值进行修改,但又要保留原数组或对象的值不被修改,此时就可以用深拷贝来创建一个新的数组或对象
- 复制深层次的
总结
关于深/浅拷贝的使用选择,我认为保险的做法是所有的拷贝都用深拷贝,且在实际开发中一般是直接引用三方库,毕竟自己写的深拷贝可能存在各种边界情况考虑不到的问题
另外,若无统一规范,可根据实际的目的而进行选择,如:能用 JSON.parse(JSON.stringify(object))
实现你的功能,就没必要去引入 lodash
库的 cloneDeep
方法,这样反倒徒增项目的打包体积
当然,若团队有规范,为了统一代码风格或为了避免潜在的风险,要求统一用第三方库的方法,增加一些打包的体积也没关系,毕竟企业级的项目还是要严谨一点
总之,各取所长就行
参考资料
如何写出一个惊艳面试官的深拷贝?
深拷贝的终极探索(99%的人都不知道)
JavaScript 如何完整实现深度Clone对象?
github lodash 源码
MDN 结构化克隆算法