JS 深拷贝-策略模式实战

分析

深拷贝函数也是一个老生常谈的话题了,它的实现有很多函数库的版本,例如 lodash 的 _.cloneDeep

或者图个省事就直接 JSON.parse(JSON.stringify()),当然这么做有许多缺点,没有考虑循环引用问题,也没有考虑其他一些数据类型的不便如 BigInt,Map,Set,Date,其中 BigInt 还是基础类型。

那么综上所述,我们该做的深拷函数不必像 lodash 那么复杂,一段函数清晰明了,也可以兼容处理 JSON.parse 的那些缺点。那就给出下面的结构开始实现吧

function deepCloneDFS(origin) {
    
}


var res = deepCloneDFS({
  num: 1,
  string: 'abc',
  arr: [1, 2],
  obj: {
    nul: null,
    undef: undefined
  }
});

console.log(res);

策略模式版本

促使笔者复习深拷,并重写它,就是因为最近学习到了策略模式,以及复习到了 DFS 深搜。

递归 origin,并拿到它的类型进行策略判定,满足某一个策略,就用这个策略来执行并返回,若没有命中策略,返回自身。代码就非常简单:

function deepCloneDFS(origin) {
  // 命中策略
  const constructor = Object.prototype.toString.call(origin);
  const fn = strategy[constructor];
  if (fn) {
    return fn(origin);
  }

  // 没有命中策略的,使用自身的构造函数重建一个
  const constructor = origin.constructor;
  return new constructor(origin);
}

// 策略
var strategy = {
  '[object Number]': function (origin) { return origin },
  '[object String]': function (origin) { return origin },
  '[object Boolean]': function (origin) { return origin },
  '[object Null]': function (origin) { return origin },
  '[object Undefined]': function (origin) { return origin },
  '[object Array]': function(origin) {
    let result = [];
    origin.forEach((item, index) => {
      result[index] = deepCloneDFS(item);
    })
    return result;
  },
  '[object Object]': function(origin) {
    let result = {};
    Object.keys(origin).forEach(key => {
      result[key] = deepCloneDFS(origin[key]);
    })
    return result;
  }
}

基础版本就这样简单,我们在此还没有判定循环引用问题,和更多类型的问题。

解决循环引用

什么是循环引用?举个例子,执行下面的代码就会栈溢出,因为它无限递归:

var obj = {}
obj.test = obj;
var res = deepCloneDFS(obj);
console.log(res);

// RangeError: Maximum call stack size exceeded

那么解决的核心思想就是遍历的每一级,需要检测对象和数组是否之前已经出现过,那我们就需要准备一个缓存数据。Map 结构可以以对象和数组做键名去存放数据,这就非常适合这个场景。

代码如下,下文注释的地方有修改:

function deepCloneDFS(origin, map = new Map()) {
  // 循环引用检测
  if (map.get(origin)) {
    return origin;
  }
  // 把对象作为键名
  map.set(origin, true)

  const constructor = Object.prototype.toString.call(origin);
  const fn = strategy[constructor];
  if (fn) {
    // 传入 map
    return fn(origin, map);
  }

  const constructor = origin.constructor;
  return new constructor(origin);
}

var strategy = {
  // ... 省略其他基础类型代码
  '[object Array]': function(origin, map) {
    let result = [];
    origin.forEach((item, index) => {
      // 传入 map
      result[index] = deepCloneDFS(item, map);
    })
    return result;
  },
  '[object Object]': function(origin, map) {
    let result = {};
    Object.keys(origin).forEach(key => {
      // 传入 map
      result[key] = deepCloneDFS(origin[key], map);
    })
    return result;
  }
}

运行结果

var obj = {
}
obj.test = [
  1,2 ,3, obj
];
var res = deepCloneDFS(obj);
console.log(res);

// Object {test: Array(4)}

策略扩充

我们知道 ES6 引入 Symbol,ES 10 引入 BitInt,这都是新的数据类型,需要小心 Symbol('a') === Symbol('b') // false

var strategy = {
  // ... 省略其他
  '[object Symbol]': function (origin) {
    return new Object(Symbol.prototype.valueOf.call(origin));
  },
  '[object BigInt]': function (origin) { return origin },
}

此外还有 Function、Map、Set 需要特殊处理一下

var strategy = {
  // ... 省略其他

  '[object Function]': function (origin) {
    // 箭头函数直接返回
    if (!origin.prototype) {
      return new Function(origin.toString());
    }
    // 普通函数需要正则处理处理
    const bodyReg = /(?<={)(.|\n)+(?=})/m;
    const paramReg = /(?<=\().+(?=\)\s+{)/;
    const funcString = func.toString();
    // 分别匹配 函数参数 和 函数体
    const param = paramReg.exec(funcString);
    const body = bodyReg.exec(funcString);
    if(!body) return null;
    if (param) {
      const paramArr = param[0].split(',');
      return new Function(...paramArr, body[0]);
    } else {
      return new Function(body[0]);
    }
  },

  // Map 得小心它的 key 和 value 都可能是个对象需要深拷贝
  '[object Map]': function (origin, map) {
    let result = new Map();
    origin.forEach((item, key) => {
      result.set(deepCloneDFS(key, map), deepCloneDFS(item, map));
    })
    return result;
  },

  '[object Set]': function (origin, map) {
    let result = new Set();
    origin.forEach((item) => {
      result.add(deepCloneDFS(item, map));
    });
    return result;
  },
}

运行结果如下

var res = {};
res.func = function (a, b, c) { return a + b + c };
res.map = new Map();
res.map.set('test', 1);
res.map.set(res.func, 1);
res.set = new Set()
res.set.add(res.func);

var clone = deepCloneDFS(res);
console.log(clone); // Object {func: , map: Map(2), set: Set(1)}
console.log(clone.map === res.map) // false
console.log(clone.set === res.set) // false

完整代码

function deepCloneDFS(origin, map = new Map()) {
  // 循环引用检测
  if (map.get(origin)) {
    return origin;
  }
  // 把对象作为键名
  map.set(origin, true)

  // 命中策略
  let constructorType = Object.prototype.toString.call(origin);
  let fn = strategy[constructorType];
  if (fn) {
    return fn(origin, map);
  }

  // 没有命中策略的,使用自身的构造函数重建一个
  const constructor = origin.constructor;
  return new constructor(origin);
}

// 策略
var strategy = {
  '[object Number]': function (origin) { return origin },
  '[object String]': function (origin) { return origin },
  '[object Boolean]': function (origin) { return origin },
  '[object Null]': function (origin) { return origin },
  '[object Undefined]': function (origin) { return origin },

  '[object Symbol]': function (origin) {
    return new Object(Symbol.prototype.valueOf.call(origin));
  },
  '[object BigInt]': function (origin) { return origin },

  '[object Function]': function (origin) {
    // 箭头函数直接返回
    if (!origin.prototype) {
      return new Function(origin.toString());
    }
    // 普通函数需要正则处理处理
    const bodyReg = /(?<={)(.|\n)+(?=})/m;
    const paramReg = /(?<=\().+(?=\)\s+{)/;
    const funcString = origin.toString();
    // 分别匹配 函数参数 和 函数体
    const param = paramReg.exec(funcString);
    const body = bodyReg.exec(funcString);
    if(!body) return null;
    if (param) {
      const paramArr = param[0].split(',');
      return new Function(...paramArr, body[0]);
    } else {
      return new Function(body[0]);
    }
  },

  // Map 得小心它的 key 和 value 都可能是个对象需要深拷贝
  '[object Map]': function (origin, map) {
    let result = new Map();
    origin.forEach((item, key) => {
      result.set(deepCloneDFS(key, map), deepCloneDFS(item, map));
    })
    return result;
  },

  '[object Set]': function (origin, map) {
    let result = new Set();
    origin.forEach((item) => {
      result.add(deepCloneDFS(item, map));
    });
    return result;
  },

  '[object Array]': function(origin, map) {
    let result = [];
    origin.forEach((item, index) => {
      result[index] = deepCloneDFS(item, map);
    });
    return result;
  },
  '[object Object]': function(origin, map) {
    let result = {};
    Object.keys(origin).forEach(key => {
      result[key] = deepCloneDFS(origin[key], map);
    });
    return result;
  }
}

总结

一段函数,兼容几乎全部类型,并且解决循环引用问题,并且精简了代码结构,采用了让代码更容易看懂的设计模式结构,就这样我们都达到了。

要说有什么奇怪的地方,就是函数那里,我们真的有必要拷贝制造出两段功能一样的函数么,看你的工作需要吧。

参考

lodash-cloneDeepWith

Everlose-JS 设计模式-工作常用的

如何写出一个惊艳面试官的深拷贝

github ConardLi-deepClone

posted @ 2020-05-30 17:33  Ever-Lose  阅读(310)  评论(0编辑  收藏  举报