Js还债系列之:深拷贝

416 阅读5分钟

深拷贝虽普通常见,但涉及到的编程能力却不少,大概有:递归编码能力、准确判断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可以用于数组的浅拷贝

实现浅拷贝的方向:

  1. 对基础类型做一个最基本的拷贝

  2. 对引用类型开辟一个新的存储,并且拷贝一层对象属性

    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存在以下问题:

  1. 函数、undefined、symbo的数据类型,拷贝后键值对会消失
  2. Date引用类型会变成字符串
  3. 无法拷贝不可枚举的属性
  4. 无法拷贝原型链
  5. RegExp引用类型会变成空对象
  6. 对象中有NaN、infinity、-infinity,序列后的结果会变成null
  7. 无法拷贝对象的循环引用

手写深拷贝

最简单的深拷贝的实现方式,就是在上面浅拷贝的基础上略加修改,加上对每个对象的属性做递归复制的逻辑,但是这样的处理显然是不全面的,我们并没有解决循环引用,原型链等一系列问题。

我们先对问题进行梳理,深拷贝的时候,我们要解决什么问题呢?

  1. 对象的不可枚举属性怎么处理?
  2. Symbol类型的键名如何处理
  3. 参数为Date、RegExp类型的时候,如何处理?
  4. 原型链怎么处理?
  5. 循环引用怎么处理?

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);