今天是手撕深拷贝的一天,首先来回忆一下相关的知识点吧

基础知识模块

  1. JS的类型
    基本类型:Number、String、Boolean、Null、Undefined、Symbol
    引用类型:Object、Array、Function、Date、RegExp、Map、Set、WeakMap、WeakSet
    存储差异:
    声明变量时不同的内存地址分配:
    简单类型的值存放在栈中,在栈中存放的是对应的值
    引用类型对应的值存储在堆中,在栈中存放的是指向堆内存的地址
    不同的类型数据导致赋值变量时的不同:
    简单类型赋值,是生成相同的值,两个对象对应不同的地址
    复杂类型赋值,是将保存对象的内存地址赋值给另一个变量。也就是两个变量指向堆内存中同一个对象

那么,问题来了,如何判断js变量的类型呢><,继续往下看吧

  1. 判断JS类型的方法

typeof:判断基础类型及函数

console.log(typeof 1)    // number
console.log(typeof 'a')   // string
console.log(typeof undefined)  //undefined
console.log(typeof true)  // boolean
console.log(typeof null)   // object
console.log(typeof {a:1})  // object
console.log(typeof [1,2,3])  // object
console.log(typeof function a(){return 1})  // function

instanceof:判断引用类型

console.log(1 instanceof Number)    // false
console.log('a' instanceof String)   // false
console.log(true instanceof Boolean)  // false
console.log(null instanceof Object)   // false
console.log({a:1} instanceof Object)  // true
console.log([1,2,3] instanceof Object)  // true
console.log(function a(){return 1} instanceof Object)  // true
console.log([1,2,3] instanceof Array)  // true
console.log(function a(){return 1} instanceof Function)  // true

Object.prototype.toString.call():判断所有类型 => 衍生知识点:原型链

console.log(Object.prototype.toString.call(1)) // [object Number]
console.log(Object.prototype.toString.call('1'))  // [object String]
console.log(Object.prototype.toString.call(undefined))  // [object Undefined]
console.log(Object.prototype.toString.call(null))  // [object Null]
console.log(Object.prototype.toString.call(true))  // [object Boolean]
console.log(Object.prototype.toString.call({a:1})) // [object Object]
console.log(Object.prototype.toString.call([1,2,3]))  // [object Array]
console.log(Object.prototype.toString.call(function a(){}))  // [object Function]
console.log(Object.prototype.toString.call(new Map()))  // [object Map]
console.log(Object.prototype.toString.call(new Set()))  // [object Set]
  1. 浅拷贝和深拷贝的区别
    浅拷贝:对于基础类型来说拷贝的是值;对于引用类型来说拷贝的引用类型的存储地址,如果其中一个对象改变了这个地址,就会影响到另一个对象。
    浅拷贝:将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象。
    image.png
    赋值 浅拷贝 深拷贝的区别
    当我们把一个对象赋值给一个新的变量时,赋的其实是该对象的在栈中的地址,而不是堆中的数据。也就是两个对象指向的是同一个存储空间,无论哪个对象发生改变,其实都是改变的存储空间的内容,因此,两个对象是联动的。
    浅拷贝:重新在堆中创建内存,拷贝前后对象的基本数据类型互不影响,但拷贝前后对象的引用类型因共享同一块内存,会相互影响。
    深拷贝:从堆内存中开辟一个新的区域存放新对象,对对象中的子对象进行递归拷贝,拷贝前后的两个对象互不影响。
    image.png
  2. 常见的浅拷贝和深拷贝方法
    浅拷贝方法
    1.Object.assign()
    Object.assign() 方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。
let obj1 = { person: {name: "kobe", age: 41},sports:'basketball' };
let obj2 = Object.assign({}, obj1);
obj2.person.name = "wade";
obj2.sports = 'football'
console.log(obj1); // { person: { name: 'wade', age: 41 }, sports: 'basketball' }

2.函数库lodash的_.clone方法

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

3.展开运算符...
展开运算符是一个 es6 / es2015特性,它提供了一种非常方便的方式来执行浅拷贝,这与 Object.assign ()的功能相同。

let obj1 = { name: 'Kobe', address:{x:100,y:100}}
let obj2= {... obj1}
obj1.address.x = 200;
obj1.name = 'wade'
console.log('obj2',obj2) // obj2 { name: 'Kobe', address: { x: 200, y: 100 } }

4.Array.prototype.concat()

let arr = [1, 3, {
    username: 'kobe'
    }];
let arr2 = arr.concat();    
arr2[2].username = 'wade';
arr2[1] = 'wade';
console.log(arr); //[ 1, 3, { username: 'wade' } ]

5.Array.prototype.slice()

let arr = [1, 3, {
    username: 'kobe'
    }];
let arr2 = arr.slice();    
arr2[2].username = 'wade';
arr2[1] = 'wade';
console.log(arr); //[ 1, 3, { username: 'wade' } ]

深拷贝方法
1.JSON.parse(JSON.stringify())
可以应对大部分的应用场景,但是它还是有很大缺陷的,比如拷贝正则、拷贝函数、循环引用等情况。

let arr = [1, 3, {
    username: ' kobe'
},function(){}];
let arr4 = JSON.parse(JSON.stringify(arr));
arr4[2].username = 'duncan'; 
console.log(arr, arr4)

2.函数库lodash的_.cloneDeep方法
3.手写递归方法

// 深拷贝解决循环引用问题
// 解决循环引用问题,我们可以额外开辟一个存储空间,来存储当前对象和拷贝对象的对应关系,当需要拷贝当前对象时,先去存储空间中找,有没有拷贝过这个对象,如果有的话直接返回,如果没有的话继续拷贝,这样就巧妙化解的循环引用的问题。

// 优化:如果我们要拷贝的对象非常庞大时,使用Map会对内存造成非常大的额外消耗,而且我们需要手动清除Map的属性才能释放这块内存,而WeakMap会帮我们巧妙化解这个问题。
function deepClone(obj,  map = new Map()) {
  // 基础类型
  if (typeof obj !== "object" || obj === null) {
    return obj;
  }
  // 其他类型
  if (
    obj instanceof Date ||
    obj instanceof Number ||
    obj instanceof String ||
    obj instanceof Boolean
  ) {
    return new obj.constructor(obj.valueOf());
  }
  if (obj instanceof RegExp) {
    const newReg = new RegExp(obj.source, obj.flags);
    newReg.lastIndex = obj.lastIndex;
    return newReg;
  }
  if (obj instanceof Symbol) {
    return Symbol(obj.description);
  }
  if (obj instanceof Map) {
    let newMap = new Map();
    for (let [key, value] of obj) {
      newMap.set(key, deepClone(value,map));
    }
    return newMap;
  }
  if (obj instanceof Set) {
    let newSet = new Set();
    for (let value of obj) {
      newSet.add(deepClone(value,map));
    }
    return newSet;
  }
  // 函数类型
  if (typeof obj === "function") {
    return new Function("return " + obj.toString())();
  }
  // 数组类型及对象类型
  if (obj instanceof Array || obj instanceof Object) {
    let cloneObj = new obj.constructor();
    // 检查map中有无克隆过的对象
    // 有 - 直接返回
    // 没有 - 将当前对象作为key,克隆对象作为value进行存储
    // 继续克隆
    if (map.has(obj)) {
      return map.get(obj);
    };
    map.set(obj, cloneObj);
    for (let key in obj) {
      cloneObj[key] = deepClone(obj[key], map);
    }
    return cloneObj;
  }
}

var obj = {
  a: 1,
  b: {
    bb: 2,
    bbb: 3,
  },
  c: new Date(),
  d: new RegExp(/\[\]/, "g"),
  e: new Map([
    ["key1", "value1"],
    ["key2", "value2"],
  ]),
  f: new Set([1, 2, 3]),
  g: function () {
    console.log(1);
  },
  h: [1, 2, 3],
  i:Symbol("1")
};
obj.obj = obj;   // 循环引用出现爆栈

var obj2 = deepClone(obj);
console.log(obj2);
console.log(obj2.obj);

思路可以参考如何写出一个惊艳面试官的深拷贝?

  1. 可迭代对象和类数组
    类数组对象(array-like object):具有数值索引和length属性的对象。
    类数组转数组
    类数组对象没有数组上的push、pop等方法,要想使用的话有两个办法:
    a.转换为数组,使用Array.from(arrayLike)
    b.使用call、apply、bind改变this的指向,借用Array原型上的方法,比如[].push.call(arrayLike, 'xxx')

    可迭代对象:可以使用for of遍历的对象就是可迭代对象。
    可迭代对象的遍历
    可迭代的本质是该对象实现了Symbol.iterator迭代器方法。迭代器返回的是一个对象,即是所谓的迭代器对象。迭代器对象有next()方法,next()方法的返回值也是一个对象,遵循这样的规范{value: 'xxx', done: true|false}。
    for of实际就是调用了一下[Symbol.iterator]()方法,获取了迭代器对象,然后每次遍历时调用一次迭代器对象的next()方法,获取到value的值,当标识符done === true时,表示遍历完成,结束for of。

  2. 递归
    定义:函数在执行过程中调用自身,这种调用方式叫做递归。
    特点:
    递归函数必须有明确的结束条件,否则会导致无限递归循环。
    递归调用会导致栈的不断增长,可能导致栈溢出错误。
    function factorial(n) {
      if (n === 0) {
        return 1;
      } else {
        return n * factorial(n - 1);
      }
    }
  1. 尾递归优化
    定义:递归函数调用的最后一步是调用自身,并且没有其他操作(比如加减乘除等)。
    特点:
    由于尾递归只保留一个函数调用记录,可以避免栈溢出错误,从而优化递归。
    只有在严格模式下才能使用尾递归优化。
    function factorialTail(n, accumulator = 1) {
      if (n === 0) {
        return accumulator;
      } else {
        return factorialTail(n - 1, n * accumulator); // 尾递归
      }
    }

Jessica
0 声望1 粉丝