深拷贝虽普通常见,但涉及到的编程能力却不少,大概有:递归编码能力、准确判断js各种数据类型的能力、边界情况的考虑、解决循环引用的能力。让我们从数据类型入手,逐步实现一个深拷贝吧~
数据类型
ECMAScript变量包含两种不同类型的值:基本类型值和引用类型值。
基本数据类型是按值访问的,因此可以操作保存在变量中的实际的值。
引用类型是保存在内存中的对象。因为js不允许直接访问内存中的位置,所以不能直接操作对象的内存空间,实际上js是在操作对象的引用,因此引用类型的值是按照引用访问的。
ECMA有6种基本类型(原始类型):Undefined、Null、String、Number、Boolean、Symbol。还有一种复杂类型的数据结构:Object;
更加细节的分类如下图所示:
typeof 操作符
对一个值使用typeof会返回下列字符串之一:
"undefined":变量未声明或者声明之后未初始化;
"boolean":表示值为布尔值;
"number":表示值为数值;
"string":表示值是字符串;
"symbol":表示值为符号Symbol;
"function":表示值为函数;
"object":表示值为对象(不能是函数)或者是null
注意的如果字符串/数值/布尔值是通过原始包装创建的:即new String('12')这种方式创建,那么typeof返回的是对象。
instanceof
检测基本类型值的时候typeof可以是一个很好的办法,但是当检测引用类型值的时候,只有function可以返回"function",其余对象一律返回"object“,所以这个操作符用处不大。当检测引用类型值的时候,我们可以使用instanceof操作符。
如果变量是给定引用类型的实例,那么instanceof就返回true。
在ES6中,instanceof操作符会使用Symbol.hasInstance函数来确定关系。这个属性定义在function的原型上,因此默认在所有函数和类上都可以调用。这个属性也可以重新定义。
手写实现instanceof
知识点准备:
Person.prototype.isPrototypeOf(person1)//true 实例是否指向某个原型对象:
Object.getPrototypeOf(person1) == Person.prototype //true 返回给定实例的[[prototype]]
//instanceof 手写实现
function myInstanceof(left,right){
// 如果是基础类型,则直接返回false
if(typeof left !== 'object' || left ==null) return false;
// 获得当前实例的[[prototype]]
let proto = Object.getPrototypeOf(left);
while(true){
if(!proto) return false;
if(proto == right.prototype) return true;
proto = Object.getPrototypeOf(proto);
}
}
安全的类型检测
typeof对正则表达式应用typeof操作符会返回“function”,instanceof在一个页面包含多个iframe的情况下也存在很多问题。基于以上问题,在检测数据类型的时候,我们可以采取更安全的类型检测方法:Object.prototype.toSting.call(value);会返回形如[object Array]这种格式的返回值。
function getProType(value){ return Object.prototype.toString.call(value).slice(8,-1);}
浅拷贝
浅拷贝和赋值有什么不同呢?赋值操作后两个对象指向的是同一个存储空间,两个对象的变化会产生联动效果。浅拷贝是重新开辟一块内存空间,按位拷贝对象,将对象的各个属性进行依次复制,并不会进行递归复制。
object.assign(target,...source)
不会拷贝对象的继承属性
不会拷贝对象的不可枚举属性
可以拷贝Symbol类型的属性
扩展运算符
let obj = {a:1,b:{c:1}};
let obj2 = {...obj}
concat、slice
concat、slice可以用于数组的浅拷贝
实现浅拷贝的方向:
-
对基础类型做一个最基本的拷贝
-
对引用类型开辟一个新的存储,并且拷贝一层对象属性
function shallowClone(target) {
if (typeof target === 'object' && target !== null) {
const cloneTarget = Array.isArray(target) ? [] : {};
for (let key in target) { //筛除掉来自继承的属性 if (target.hasOwnProperty(key)) { cloneTarget[key] = target[key]; } } return cloneTarget;
} else {
return target;
} }
深拷贝
在内存中开辟一个全新的空间存放新对象,对于新对象的修改不会对原对象有影响。
JSON.stringfy
最简单的深拷贝的方法。但是这种方式有很多缺点。由于JSON只支持object,array,string,number,true,false,null这几种数据或者值,其他的比如函数,undefined,Date,RegExp等数据类型都不支持。
所以JSON.stringfy存在以下问题:
- 函数、undefined、symbo的数据类型,拷贝后键值对会消失
- Date引用类型会变成字符串
- 无法拷贝不可枚举的属性
- 无法拷贝原型链
- RegExp引用类型会变成空对象
- 对象中有NaN、infinity、-infinity,序列后的结果会变成null
- 无法拷贝对象的循环引用
手写深拷贝
最简单的深拷贝的实现方式,就是在上面浅拷贝的基础上略加修改,加上对每个对象的属性做递归复制的逻辑,但是这样的处理显然是不全面的,我们并没有解决循环引用,原型链等一系列问题。
我们先对问题进行梳理,深拷贝的时候,我们要解决什么问题呢?
- 对象的不可枚举属性怎么处理?
- Symbol类型的键名如何处理
- 参数为Date、RegExp类型的时候,如何处理?
- 原型链怎么处理?
- 循环引用怎么处理?
function deepClone(target,map = new WeakMap()){
if(typeof target ==='object' && target !== null){
if (target instanceof Date) return new Date(target);
if (target instanceof RegExp) return new RegExp(target);
// 利用weakMap处理循环引用,有就直接返回
if (hash.has(target)) return hash.get(target);
//获得所有属性的描述信息,可处理带有比如enumerable、set、get等描述信息的数据
let allDesc = Object.getOwnPropertyDescriptors(target);
//创建一个新对象,使用现有的对象来提供新创建的对象的proto
let result = Object.create(Object.getPrototypeOf(target), allDesc);
//以上两行逻辑实现了浅拷贝并且处理了原型链
hash.set(target, result);
//Reflect.ownKeys 可获得不可遍历属性和Symbol属性
for(let key of Reflect.ownKeys(target)){
result[key] = deepClone(target[key],hash)
}
return result;
}else{
//基础类型、function。
return target;
}
}
下面放上测试数据 可以快速取用以作验证:
function Foo() {
this.name = 'zs';
this.age = 14
}
Foo.prototype.getName = function () {
return this.name;
}
var f = new Foo();
var obj = {
num: 0,
str: '',
boolean: true,
unf: undefined,
nul: null,
obj: { name: '我是一个对象', id: 1 },
arr: [0, 1, 2,{ name: '我是一个对象', id: 1 }],
func: function () { console.log('我是一个函数') },
date: new Date(0),
reg: new RegExp('/我是一个正则/ig'),
[Symbol()]: 1,
foo: f,
}
Object.defineProperty(obj, 'innumerable', {
enumberable: false,
value: '不可枚举属性'
})
obj.loop = obj;
let _clone = deepClone(obj);
_clone.obj.name = '修改这个对象';
console.log(obj);
console.log(_clone);