JavaScript中,引用类型数据 存储于堆内存中,因为JavaScript不允许直接访问内存位置,因此操作对象实际上是操作该对象的引用,即其内存位置所处地址。
对于引用类型数据的操作,如果直接修改了对象的值,即修改了该引用内存地址上的数据,将影响其他引用此地址的对象。
拷贝对象就是经典的操作,了解区分浅拷贝/深拷贝操作,可以避免实际开发中的一些问题。

浅拷贝

浅拷贝只会对一层对象开辟新的内存空间进行存放,对于嵌套对象的深层属性仍是原来的引用地址。

Object.assign()

  1. const obj = {
  2. name: 'donggua',
  3. props: {
  4. value: 1
  5. }
  6. }
  7. const newObj = Object.assign({}, obj);
  8. newObj.name = '_donggua';
  9. newObj.props.value++;
  10. obj; // { name: 'donggua', props: { value: 2 } }
  11. newObj; // { name: '_donggua', props: { value: 2 } }

Array.prototype.concat()

  1. const arr = [1, 2, 3, [4, 5]];
  2. const ary = arr.concat();
  3. ary[0] = 4;
  4. ary[3][0] = 1;
  5. arr; // [1, 2, 3, [1, 5]];
  6. ary; // [4, 2, 3, [1, 5]];

Array.prototype.slice()

  1. const arr = [1, 2, 3, [4, 5]];
  2. const ary = arr.slice();
  3. ary[0] = 4;
  4. ary[3][0] = 1;
  5. arr; // [1, 2, 3, [1, 5]];
  6. ary; // [4, 2, 3, [1, 5]];

ES6 扩展运算符

  1. const obj = {
  2. name: 'donggua',
  3. props: {
  4. value: 1
  5. }
  6. }
  7. const newObj = { ...obj };
  8. newObj.name = '_donggua';
  9. newObj.props.value++;
  10. obj; // { name: 'donggua', props: { value: 2 } }
  11. newObj; // { name: '_donggua', props: { value: 2 } }
  1. const arr = [1, 2, 3, [4, 5]];
  2. const ary = [...arr];
  3. ary[0] = 4;
  4. ary[3][0] = 1;
  5. arr; // [1, 2, 3, [1, 5]];
  6. ary; // [4, 2, 3, [1, 5]];

手写实现

  1. function clone(obj) {
  2. if (typeof obj === "object" && obj !== null) {
  3. const result = Array.isArray(obj) ? [] : {};
  4. for (const key in obj) {
  5. if (obj.hasOwnProperty(key)) {
  6. result[key] = obj[key];
  7. }
  8. }
  9. return result;
  10. }
  11. return obj;
  12. }

深拷贝

深拷贝将对象深层进行完整的精确拷贝,对新对象的任何修改不会影响原对象数据。

JQuery.extend()

  1. import $ from "jquery";
  2. const obj = {
  3. name: 'donggua',
  4. props: {
  5. value: 1
  6. }
  7. }
  8. const newObj = $.extend(true, {}, obj1);
  9. newObj.name = '_donggua';
  10. newObj.props.value++;
  11. obj; // { name: 'donggua', props: { value: 1 } }
  12. newObj; // { name: '_donggua', props: { value: 2 } }

JSON.parse(JSON.stringify())

  1. const obj = {
  2. name: 'donggua',
  3. props: {
  4. value: 1
  5. }
  6. }
  7. const newObj = JSON.parse(JSON.stringify(obj));
  8. newObj.name = '_donggua';
  9. newObj.props.value++;
  10. obj; // { name: 'donggua', props: { value: 1 } }
  11. newObj; // { name: '_donggua', props: { value: 2 } }

JSON.stringify() 存在一定的弊端:

  • Date 类型,转换后会调用 toJSON 转为字符串类型

    1. const d = new Date();
    2. d.toJSON() === JSON.parse(JSON.stringify(d)); // true
  • undefinedSymbol、任意函数将被忽略 ```javascript const obj = { fn: function() {}, value: undefined, unique: Symbol() }

const o = JSON.parse(JSON.stringify(obj)); o; // {}

  1. - `RegExp``Error``Set``Map` 等特殊对象属性转换后将得到空对象
  2. ```javascript
  3. const obj = {
  4. reg: new RegExp('\\w+'),
  5. e: new Error('err')
  6. }
  7. const o = JSON.parse(JSON.stringify(obj))
  8. o; // { reg: {}, err: {} }
  • JSON.stringify() 只能序列化对象的可枚举的自有属性,对于构造函数生成的对象属性,将丢失

constructor 的正确指向

  1. function fn() {}
  2. const obj = {
  3. value: new fn()
  4. }
  5. obj.value.constructor; // ƒ fn() {}
  6. const o = JSON.parse(JSON.stringify(obj))
  7. o.value.constructor; // ƒ Object() { [native code] }
  • NaNInfinity-Infinity 将被 当成 null 处理
  • 循环引用的对象将报错

    messageChannel

    vue.nextTick 源码曾使用的Web API,在了解这个API时发现可以用于深拷贝,详见 知乎 - 亦河的文章

  1. function cloneUsingChannel(obj) {
  2. return new Promise(resolve => {
  3. const channel = new MessageChannel();
  4. channel.port1.onmessage = e => resolve(e.data)
  5. channel.port2.postMessage(obj);
  6. })
  7. }

但该方法存在一个缺陷,当拷贝对象带有函数属性时,将抛出错误:image.png

手写递归实现

  • 深拷贝就是在手写浅拷贝基础上加上递归
  • 借用 Map 数据映射记录已拷贝对象,解决循环引用问题
  • 对特殊对象,通过其原型方法实例化新的对象(Map、Set、RegExp、Error、Date)
  • 函数拷贝注意区分箭头函数(借鉴引用了掘金 - ConardLi的文章) ```javascript function deepClone(obj, map = new Map()) { if (!isObject) { return obj; } // 对象映射,对重复对象直接获取缓存,解决循环引用栈溢出 if (map.get(obj)) { return obj; } map.set(obj, true); const _ctor = obj.constructor; // 获取对象原型,部分特殊对象使用其原型方法创建新的实例 let result; switch (_ctor) { case Object: case Array:
    1. result = Array.isArray(obj) ? [] : {};
    2. for (const key in obj) {
    3. if (obj.hasOwnProperty(key)) {
    4. result[key] = deepClone(obj[key]);
    5. }
    6. }
    7. // 冻结对象多一步处理
    8. if (Object.isFrozen(obj)) {
    9. result = Object.freeze(result);
    10. }
    case Map: case Set:
    1. result = new _ctor([...obj]);
    2. break;
    case RegExp: case Date:
    1. result = new RegExp(obj);
    2. break;
    case Error:
    1. result = new Error(obj.message);
    2. break;
    case Function:
    1. result = cloneFunc(obj);
    2. break;
    default:
    1. result = obj;
    } return result; }

/**

  • 拷贝函数
  • 出处: https://juejin.cn/post/6844903929705136141#heading-12 */ function cloneFunc(fn) { const bodyReg = /(?<={)(.|\n)+(?=})/m; const paramReg = /(?<=().+(?=)\s+{)/; const fnString = fn.toString(); // 箭头函数 采用 new Funtion 创建新的匿名函数或直接返回其本身 if (!fn.prorotype) {
    1. try {
    2. return Function("return (" + fnString + ")()")
    3. } catch {
    return fn; } } // 分别匹配 函数参数 和 函数体 const param = paramReg.exec(fnString); const body = bodyReg.exec(fnString); if (!body) return null; if (param) { const paramArr = param[0].split(‘,’); return new Function(…paramArr, body[0]); } else { return new Function(body[0]); } }

function isObject(val) { return [‘object’, ‘function’].includes(typeof val) && val !== null; } ```

总结

实际开发中常用的拷贝可能更多的是浅拷贝,对于深拷贝操作更多的是借用第三方库如 lodash.cloneDeep()
对于 JSON.stringify() 更应该了解注意其局限性。
而通过动手实现递归深拷贝,从实践中体会实现一个 API 需要考虑的各类情况和边界条件,代入实际开发中应该更加细心全方位的思考。