• 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 = /(?<={)(.|
    )+(?=})/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 = /(?<={)(.|
    )+(?=})/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

  • 相关阅读:
    SharePoint Portal Server与SharePoint Services之间的关系
    配置Microsoft Visual SourceSafe 2005的局域网/Internet访
    Maven创建Web项目(idea)
    Maven入门(idea)
    Windows下搭建Vue脚手架CLI
    关于wince中的全屏显示
    ubuntu右键添加打开终端的快捷菜单[转]
    将CString转换成string ...
    090606日记
    Java中的InputStream和OutputStream
  • 原文地址:https://www.cnblogs.com/everlose/p/12993838.html
Copyright © 2020-2023  润新知