【JS 基础】深浅拷贝

138 阅读3分钟

认识深拷贝和浅拷贝

javascript 中一般有按值传递按引用传递两种赋值方式:

  1. 按值传递的是基本数据类型(Number, String, Boolean, Null, Undefined, Symbol),一般存放于内存中的栈区,存取速度快,存放量小;
  2. 按引用传递的是引用类型(Object, Array, Function),一般存放与内存中的堆区,存取速度慢,存放量大,其引用指针存于栈区,并指向引用本身。

深拷贝和浅拷贝是相对于引用类型而言的:

  • 浅拷贝: 只拷贝对象的引用
  • 深拷贝:指复制后的新对象重新指向一个新的内存地址,两个对象改变互不影响

浅拷贝

对象类型在赋值的过程中其实是复制了地址,从而会导致改变了一方其他也都被改变的情况。

let a = {
  age: 1
}
let b = a
a.age = 2
console.log(b.age) // 2

浅拷贝的方法

Object.assign... 运算法、sliceconcat

filtermap 是深拷贝吗?不是,当是嵌套数组时,第一层深拷贝,后面的都是浅拷贝

1. Object.assign

let a = {
  age: 1
}
let b = Object.assign({}, a)
a.age = 2

console.log(b.age) // 1
console.log(b === a) // false

嵌套对象时,第一层深拷贝,后面的都是浅拷贝

var obj1 = {name:{a:1}};
var obj2 = Object.assign({}, obj1);
obj2.name.a = 2;
console.log(obj1.name); // {a:2}
console.log(obj2.name);// {a:2}

2. 展开运算符 ...

嵌套对象时,第一层深拷贝,后面的都是浅拷贝

let a = {
  age: 1
}
let b = { ...a }
a.age = 2

console.log(b.age) // 1

3. slice

嵌套数组时,第一层深拷贝,后面的都是浅拷贝

let arr1 = [1, 2, [3]]
let arr2 = arr1.slice(0)
arr1[2][0] = 4

// 4
arr2[2][0]

4. concat

嵌套数组时,第一层深拷贝,后面的都是浅拷贝

let arr1 = [1, 2, [3]]
let arr2 = arr1.concat()
arr1[2][0] = 4

// 4
arr2[2][0]

深拷贝的方法

1. JSON.parse(JSON.stringify(object))

该方法的局限性
  • 丢失 undefined
  • 丢失 symbol
  • 丢失函数
  • 不能解决循环引用的对象
  • 正则
  • Date 类型由 Date 变为 string
  • NaNInfinity 变为 null
  • setmap 丢失
测试用例
function Obj() {
    this.func = function() {
        console.log(1)
    };
    this.obj = {a : 1};
    this.arr = [1, 2, 3];
    this.und = undefined;
    this.reg = /123/;
    this.date = new Date(0);
    this.NaN = NaN;
    this.infinity = Infinity;
    this.sym = Symbol();
    this.set = new Set([1, 2, 3]);
    this.map = new Map([['a', 1], ['b', 2]]);
}
let obj1 = new Obj();
Object.defineProperty(obj1, 'innumerable', {
    enumerable: false,
    value: 'innumerable'
});
let obj2 = JSON.parse(JSON.stringify(obj1));
结果对比
// obj1
Obj {
  func: [Function],
  obj: { a: 1 },
  arr: [ 1, 2, 3 ],
  und: undefined,
  reg: /123/,
  date: 1970-01-01T00:00:00.000Z,
  NaN: NaN,
  infinity: Infinity,
  sym: Symbol(),
  set: Set { 1, 2, 3 },
  map: Map { 'a' => 1, 'b' => 2 }
}
// obj2
{ 
obj: { a: 1 },
  arr: [ 1, 2, 3 ],
  reg: {},
  date: '1970-01-01T00:00:00.000Z',
  NaN: null,
  infinity: null,
  set: {},
  map: {} 
}

2. lodash

// 安装
npm i --save lodash

// 引入
const _ = require('lodash')

// 使用
let obj = {
    a: {
        a: 'hello'
    },
    b: 33
}
let newObj = _.cloneDeep(obj)
newObj.a.a = 'world'

console.log(obj.a.a)
console.log(obj === newObj)
console.log(obj == newObj)
// hello
// false
// false

3. 实现一个简单的深拷贝

const isComplexDataType = obj => (
    (typeof obj === 'object' || typeof obj === 'function') && (obj !== null)
);

const deepClone = function (obj, hash = new WeakMap()) {
    // 解决循环引用,如 obj.loop = obj;
    if (hash.has(obj)) {
        return hash.get(obj);
    }
    let type = [Date, RegExp, Set, Map, WeakSet, WeakMap];
    if (type.includes(obj.constructor)) {
        return new obj.constructor(obj);
    }

    // 获取 obj 属性描述对象
    let allDesc = Object.getOwnPropertyDescriptors(obj);
    // 1、创建一个对象;2、确定继承关系;3、把属性值赋给新对象
    let cloneObj = Object.create(Object.getPrototypeOf(obj), allDesc);
    hash.set(obj, cloneObj);

    // Reflect.ownKeys(obj)可以拷贝不可枚举属性和 symbol 类型
    for(let key of Reflect.ownKeys(obj)) {
        // 如果值是引用类型(非函数)则递归调用 deepClone
        cloneObj[key] = (isComplexDataType(obj[key]) && typeof obj[key] !== 'function')
            ? deepClone(obj[key], hash)
            : obj[key];
    }

    return cloneObj;
}
测试用例
function Obj() {
    this.func = function() {
        console.log(1)
    };
    this.obj = {a : 1};
    this.arr = [1, 2, 3];
    this.und = undefined;
    this.reg = /123/;
    this.date = new Date(0);
    this.NaN = NaN;
    this.infinity = Infinity;
    this.sym = Symbol();
    this.set = new Set([1, 2, 3]);
    this.map = new Map([['a', 1], ['b', 2]]);
}
let obj1 = new Obj();
let obj2 = deepClone(obj1);
拷贝结果
Obj {
  func: [Function],
  obj: { a: 1 },
  arr: Array { '0': 1, '1': 2, '2': 3 },
  und: undefined,
  reg: /123/,
  date: 1970-01-01T00:00:00.000Z,
  NaN: NaN,
  infinity: Infinity,
  sym: Symbol(),
  set: Set { 1, 2, 3 },
  map: Map { 'a' => 1, 'b' => 2 }
}
Object.getPrototypeOf()
function Bar() {}

let obj = new Bar();

Object.getPrototypeOf(obj) == Bar.prototype
// true
Reflect.ownKeys()Object.keys() 区别:
  1. Reflect.ownKeys() 返回所有的属性 key,包括不可枚举类型,Symbol 类型,不包括继承的属性
  2. Object.keys() 返回遍历对象可枚举的属性,不包括继承的属性和 Symbol 类型
let obj = {
    a: 1,
    b: 2
}

Object.defineProperty(obj, Symbol(), {
    value: 1,
    enumerable: true
})

Object.defineProperty(obj, 'method', {
    value: function() {
        console.log('ok')
    },
    enumerable: false
})

console.log(Reflect.ownKeys(obj));
// [ 'a', 'b', 'method', Symbol() ]
console.log(Object.keys(obj));
// [ 'a', 'b' ]