深拷贝是前端面试题中经常用来考察前端开发工程师能力的一个题目,该题目会涉及如下考点:

  • JS基本数据类型和引用数据类型概念
  • 函数递归
  • while循环
  • 外部对象
  • 循环引用
  • 拷贝原型链属性
  • 拷贝特殊对象类型
  • 拷贝不可枚举属性
  • 拷贝Symbol属性

本文针对上述考点,由浅入深讨论JS深拷贝的实现。

数据类型

基本类型

  • String 字符串
  • Number 整型+浮点型
  • Boolean 布尔值
  • Null 空对象
  • Undefined undefined
  • Symbol ES6新增,用来定义全局变量的唯一性,因此没有属性

引用类型

Object (Array, Function, Date, RegExp, JSON etc.)

代码示例

var a1 = 0;
var a2 = 'this is str';
var a3 = null;
var c = [1, 2, 3];
var b = { m: 20 };

浅拷贝

浅拷贝会新建了一个对象,然后将源对象的自身属性通过遍历的方式依次复制到新对象,浅拷贝只复制了源对象第一层的属性。

function shallowCopy(src) {
  var dst = {};
  for (var prop in src) {
    if (src.hasOwnProperty(prop)) {
      dst[prop] = src[prop];
    }
  }
  return dst;
}

或者

var obj2 = Object.assgin({}, obj);

深拷贝

序列化反序列化

先把对象序列化成字符串,再反序列化回对象:

function deepClone(obj) {
  return JSON.parse(JSON.stringify(obj));
}

它只能深拷贝对象和数组,对于其他种类的对象,就无能为力了。这种方法比较适合在平常开发使用,因为通常不需要考虑对象和数组之外的类型。

递归迭代

遍历对象,如果属性值为对象则进行递归,直到是基本数据类型,再去复制。

function deepCopy(src) {
  var dst = {};
  for (var prop in src) {
    if (src.hasOwnProperty(prop)) {
      var val = src[prop];
      if (typeof val === 'object') {
        dst[prop] = deepCopy(val);
      } else {
        dst[prop] = val;
      }
    }
  }
  return dst;
}

while循环

深度/广度优先遍历

function deepClone(obj) {
  var res = {};
  var nodeQueue = [obj];
  var copyQueue = [res];
  while (nodeQueue.length > 0) {
    // 深度优先
    var node = nodeQueue.pop();
    var copy = copyQueue.pop();

    // 广度优先
    // var node = nodeQueue.shift();
    // var copy = copyQueue.shift();
    for (var key in node) {
      var val = node[key];
      if (typeof val !== 'object') {
        copy[key] = val;
      } else {
        nodeQueue.push(val);
        copy[key] = {};
        copyQueue.push(copy[key]);
      }
    }
  }
  return res;
}

外部对象

当拷贝的对象属性引用了外部对象,克隆时需要将新对象属性指向外部对象,[一般可忽略考虑]

function deepCopy(src) {
  var dst = {};
  for (var prop in src) {
    if (src.hasOwnProperty(prop)) {
      var val = src[prop];
      var ref = isScope(val, this);
      if (ref) {
        dst[prop] = ref;
        continue;
      }
      if (typeof val === 'object') {
        dst[prop] = deepCopy(val);
      } else {
        dst[prop] = val;
      }
    }
  }
  return dst;
}
// 检测是否为外部对象
function isScope(obj, scope) {
  for (var key in scope) {
    if (scope[key] === obj) {
      return scope[key];
    }
  }
  return false;
}

循环引用

如果待拷贝的对象存在循环引用,则可以通过 new 一个WeakMap来记录拷贝的对象, 如果存在引用就直接返回

function deepCopy(src, hash) {
  var dst = {};
  var hash = hash || new WeakMap();
  if (hash.has(src)) {
    return hash.get(src);
  }
  hash.set(src, dst);
  for (var prop in src) {
    if (src.hasOwnProperty(prop)) {
      var val = src[prop];
      var ref = isScope(val, this);
      if (ref) {
        dst[prop] = ref;
        continue;
      }
      if (typeof val === 'object') {
        dst[prop] = deepCopy(val, hash);
      } else {
        dst[prop] = val;
      }
    }
  }
  return dst;
}

拷贝原型链属性

function deepCopy(src, hash) {
  var dst = {};
  var hash = hash || new WeakMap();
  if (hash.has(src)) {
    return hash.get(src);
  }
  hash.set(src, dst);
  for (var prop in src) {
    if (src.hasOwnProperty(prop)) {
      var val = src[prop];
      var ref = isScope(val, this);
      if (ref) {
        dst[prop] = ref;
        continue;
      }
      if (typeof val === 'object') {
        dst[prop] = deepCopy(val, hash);
      } else {
        dst[prop] = val;
      }
    }
  }
  Object.setPrototypeOf(dst, Object.getPrototypeOf(src));
  return dst;
}

拷贝特殊对象类型

Array、Date、RegExp、Blob、File、FileList、ArrayBuffer、ArrayBufferView、ImageData、Map、Set 利用结构化克隆算法

function deepCopy(src, hash) {
  var dst = {};
  var hash = hash || new WeakMap();
  var Constructor = src.constructor;
  switch (Constructor) {
    case Object:
      dst = new Constructor(src);
      break;
    case RegExp:
      dst = new Constructor(src);
      break;
    case Date:
      dst = new Constructor(src.getTime());
      break;
    default:
      if (hash.has(src)) {
        return hash.get(src);
      }
      dst = new Constructor();
      hash.set(src, dst);
  }
  for (var prop in src) {
    if (src.hasOwnProperty(prop)) {
      var val = src[prop];
      var ref = isScope(val, this);
      if (ref) {
        dst[prop] = ref;
        continue;
      }
      if (val && val.constructor === Constructor) {
        dst[prop] = deepCopy(val, hash);
      } else {
        dst[prop] = val;
      }
    }
  }
  Object.setPrototypeOf(dst, Object.getPrototypeOf(src));
  return dst;
}

拷贝不可枚举属性

不可枚举类型属性,无法通过for in获取到,可以通过Reflect.ownKeys方法获取

function deepCopy(src, hash) {
  var dst = {};
  var hash = hash || new WeakMap();
  var Constructor = src.constructor;
  switch (Constructor) {
    case Object:
      dst = new Constructor(src);
      break;
    case RegExp:
      dst = new Constructor(src);
      break;
    case Date:
      dst = new Constructor(src.getTime());
      break;
    default:
      if (hash.has(src)) {
        return hash.get(src);
      }
      dst = new Constructor();
      hash.set(src, dst);
  }
  var keys = Reflect.ownKeys(src);
  for (var i = 0; i < keys.length; i++) {
    var prop = keys[i];
    if (src.hasOwnProperty(prop)) {
      var val = src[prop];
      var ref = isScope(val, this);
      if (ref) {
        dst[prop] = ref;
        continue;
      }
      if (val && val.constructor === Constructor) {
        dst[prop] = deepCopy(val, hash);
      } else {
        dst[prop] = val;
      }
    }
  }
  Object.setPrototypeOf(dst, Object.getPrototypeOf(src));
  return dst;
}

拷贝Symbol属性

同上