深入浅出loadash深拷贝源码

·  阅读 145

本文会从JavaScript中经常出现的业务场景——对象拷贝出发,带大家了解浅拷贝、深拷贝的概念并实现;最后介绍从源码解读的角度查看lodash实现深浅拷贝的思路。

拷贝

拷贝不仅是业务常见的场景也是面试经典,因为Javascript中原始数据类型引用类型存储方式的不同,即原始数据类型保存在栈内存引用类型保存在堆内存,这个差异导致了两种数据类型赋值行为的差异,所以有了深拷贝浅拷贝的说法。

浅拷贝(shallow copy):只复制指向某个对象的指针,而不复制对象本身,新旧对象共享一块内存; 如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址。

深拷贝(deep copy):深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存,修改新对象,拷贝后两个对象互不影响。

接下来我们看看浅拷贝和深拷贝的实现:

浅拷贝

function cloneShallow(obj) {
  const newObj = {};
  for (let key in obj) {
    if (Object.prototype.hasOwnProperty.call(obj, key)) {
      newObj[key] = obj[key];
    }
  }
  return newObj;
}
复制代码

浅拷贝之实现 Object.assign

Object.assign(target, ...sources)

思路:

  1. 首先检查target是否为nullundefined,是的话报错。

  2. 使用 Object()target 转成对象,并将这个对象赋值给target

  3. 遍历每个sources,循环中执行如下操作:

    遍历当前对象的可枚举属性(for...in),如果是对象自有属性(Object.hasOwnProperty)则将其复制到target对应的key上。

// Attention 1
Object.defineProperty(Object, "new assign", {
value: function (target) {
  'use strict';
  if (target == null) { // Attention 2
    throw new TypeError('Cannot convert undefined or null to object');
  }

  // Attention 3
  var to = Object(target);
    
  for (var index = 1; index < arguments.length; index++) {
    var nextSource = arguments[index];

    if (nextSource != null) {  // Attention 2
      // Attention 4
      for (var nextKey in nextSource) {
        if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
          to[nextKey] = nextSource[nextKey];
        }
      }
    }
  }
  return to;
},
writable: true,
configurable: true
});
复制代码

核心要点:

1. 可枚举性

原生情况下挂载在 Object 上的属性是不可枚举的,但是直接在 Object 上挂载属性 a 之后是可枚举的,所以这里必须使用 Object.defineProperty,并设置 enumerable: false 以及 writable: true, configurable: true

2. 判断参数是否正确

target不能为null或者undefined

3. 为什么要使用 Object()target

因为可能会出现这种场景:

let obj = Object.assign('123',{a:'a'});
// String {'123', a: 'a'}
复制代码

4. 存在性

在不访问属性值的情况下判断对象中是否存在某个属性?

  • in操作符,会检查属性是否在对象及其 [[Prototype]] 原型链中
  • hasOwnProperty(..) 只会检查属性是否在对象中,不会检查 [[Prototype]] 原型链。

所以判断对象自有属性时我们肯定采用的是后者,但是我们是用了Object.prototype.hasOwnProperty.call(obj,..)而不是obj.hasOwnProperty(..),这是因为有些对象的原型链并没有Object,比如通过Object.create(null)来创建,这种情况下,使用 obj.hasOwnProperty(..) 就会失败。

深拷贝

我们知道浅拷贝只拷贝了一层,那么要实现深拷贝就只要对每一层的对象再拷贝一次即可,所以我们在浅拷贝的基础上改进:

function deepClone(obj) {
  const newObj = {};
  for (let key in obj) {
    if (Object.prototype.hasOwnProperty.call(obj, key)) {
      const value = obj[key];
      newObj[key] = typeof value === 'object' ? deepClone(value) : value;
    }
  }
  return newObj;
}
复制代码

但是很快你就会发现这有很多缺点,那就是不支持NullArrayRegExpDate,以及ES6的SetMapWeakSetWeakMap等等js内置对象,并且Symbol的key值也无法支持,还有就是循环引用递归爆栈的问题。

递归爆栈

因为递归调用的方式会造成堆栈一直叠加,当拷贝的源对象层级深到一定程度的时候就可能发生堆栈溢出,所以我们需要改写成循环的方式。

仔细观察对象,会发现其实这就是一棵树:

var a = {
    a1: 1,
    a2: {
        b1: 1,
        b2: {
            c1: 1
        }
    }
}
复制代码
    a
  /   \
 a1   a2        
 |    / \         
 1   b1 b2     
     |   |        
     1  c1
         |
         1       
复制代码

所以我们可以用一个栈存储当前需要深拷贝的对象的树结构,当栈空的时候就遍历完了,栈里面存储下一个需要拷贝的节点。

function deepClone(obj) {
  const target = {};
  const root = {
    // 要拷贝的源对象
    source: obj,
    // 要拷贝的目标对象
    target
  }
  const stack = [root];
  while (stack.length > 0) {
    const { source, target: innerTarget } = stack.pop();
    for (let inerKey in source) {
      if (Object.prototype.hasOwnProperty.call(source, inerKey)) {
        const value = source[inerKey];
        if (typeof value === 'object') {
          // 初始化新的对象,并将其放入父对象对应的key
          const newTarget = {};
          innerTarget[inerKey] = newTarget;
          stack.push({
            source: value,
            target: newTarget,
          });
        } else {
          innerTarget[inerKey] = value;
        }
      }
    }
  }
  return target;
}
复制代码

循环引用

解决循环引用最好的方法就是记录每一个source拷贝后对应的target

function deepClone(obj) {
  const target = {};
  const valueList = [];
  valueList.push({
    source: obj,
    target: target
  });
  const root = {
    // 要拷贝的源对象
    source: obj,
    // 要拷贝的目标对象
    target
  }
  const stack = [root];
  while (stack.length > 0) {
    const { source, target: innerTarget } = stack.pop();
    for (let inerKey in source) {
      if (Object.prototype.hasOwnProperty.call(source, inerKey)) {
        const value = source[inerKey];
        if (typeof value === 'object') {
          // 解决循环引用
          const searchValue = find(valueList, value);
          if (searchValue) {
            innerTarget[inerKey] = searchValue;
            break;
          }
          // 初始化新的对象,并将其放入父对象对应的key
          const newTarget = {};
          innerTarget[inerKey] = newTarget;
          valueList.push({
            source: value,
            target: newTarget
          });
          stack.push({
            source: value,
            target: newTarget,
          });
        } else {
          innerTarget[inerKey] = value;
        }
      }
    }
  }
  return target;
}

function find(list, value) {
  for (const obj of list) {
    if (obj.source === value) {
      return obj.target;
    }
  }
  return null;
}
复制代码

当然,这里存储拷贝对应关系的数据结构是一个对象,其实不高效,可以考虑使用 WeakMap ,而在 lodash中则是实现了一个栈。

对象兼容

接下来我们逐个解决对象的兼容:

Null

我们知道js有个bug,typeof null === 'object',所以我们只要在判断对象的时候多一个null的判断,不符合 isObject 条件的都直接返回值:

function isObject(obj) {
    return typeof obj === 'object' && obj !== null;
}
复制代码
Array

在初始化新的对象targetnewTarget时,我们判断一下是否为数组,是的话初始化一个空数组:

function createTarget(obj) {
    return Array.isArray(obj) ? [] : {};
}
复制代码

其实数组还有一种情况,RegExp.prototype.exec() 方法返回一个数组,包含额外的属性 indexinput

RegExp
if (source instanceof RegExp) {
    target = new RegExp(source.source, source.flags);
}
复制代码
Date
if (source instanceof Date) {
    target = new Date(source);
} 
复制代码

剩下一些ES6的对象我们先不看,最后我们会查看lodash是如何解决这些对象的。我们先处理Symbol

Symbol

至于Symbol的键值,我们需要Reflect.ownKeys()

静态方法 Reflect.ownKeys() 返回一个由目标对象自身的属性键组成的数组。它的返回值等同于 Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target))

for..in不同的是Reflect.ownKeys()会返回包括Symbol在内的所有自身属性组成的键值:

// for (let inerKey in source) {
Reflect.ownKeys(source).forEach(inerKey => {
...
});
复制代码

Lodash是如何实现深拷贝的

loadash是JavaScript一个饱负盛名的工具库,包含了很多实用的工具方法,其中深拷贝的实现是很值得我们学习的。

我们基于lodash v4.17.21的版本进行代码解读,从以下几个方面:

完整代码

打开cloneDeep.js

function cloneDeep(value) {
  return baseClone(value, CLONE_DEEP_FLAG | CLONE_SYMBOLS_FLAG)
}
复制代码

可以看到是调用了baseClone这个方法:

/* @private
 * @param {*} [value] 要拷贝的值.
 * @param {number} [bitmask] 二进制位掩码
 * @param {Function} [customizer] 自定义克隆方式的函数
 * @param {string} [key] `value`的 key 值.
 * @param {Object} [object] `value`的父对象.
 * @param {Object} [stack] 存储拷贝前后对象的对应关系,用来解决递归引用.
 * @returns {*} 返回拷贝的新对象.
 */
function baseClone(value, bitmask, customizer, key, object, stack) {
  let result
  const isDeep = bitmask & CLONE_DEEP_FLAG // 是否深拷贝
  const isFlat = bitmask & CLONE_FLAT_FLAG // 是否拷贝原型链上的属性
  const isFull = bitmask & CLONE_SYMBOLS_FLAG // 是否拷贝 Symbol
  // 如果有自定义克隆的函数,则直接调用,如果其执行结果不是undefined就直接返回
  if (customizer) {
    result = object ? customizer(value, key, object, stack) : customizer(value)
  }
  if (result !== undefined) {
    return result
  }
  // 如果是 对象 和 `null` 直接返回
  if (!isObject(value)) {
    return value
  }
  // 判断数组
  const isArr = Array.isArray(value)
  // tag是 `Object.prototype.toString` 的返回结果
  const tag = getTag(value)
  // 处理数组
  if (isArr) {
    result = initCloneArray(value)
    if (!isDeep) {
      return copyArray(value, result)
    }
  } else {
    // 判断函数
    const isFunc = typeof value === 'function'
    // 处理 `Buffer`
    if (isBuffer(value)) {
      return cloneBuffer(value, isDeep)
    }
    // 如果是对象、Arguments、或者是函数并且没有传入父对象的参数
    if (tag == objectTag || tag == argsTag || (isFunc && !object)) {
      result = (isFlat || isFunc) ? {} : initCloneObject(value)
      if (!isDeep) {
        return isFlat
          ? copySymbolsIn(value, copyObject(value, keysIn(value), result))
          : copySymbols(value, Object.assign(result, value))
      }
    } else {
      if (isFunc || !cloneableTags[tag]) {
        return object ? value : {}
      }
      result = initCloneByTag(value, tag, isDeep)
    }
  }
  // 检查循环引用并返回其对应的克隆
  stack || (stack = new Stack)
  const stacked = stack.get(value)
  if (stacked) {
    return stacked
  }
  stack.set(value, result)
  // 针对不同的tag做不同的处理,比如 Map Set 等
  if (tag == mapTag) {
    value.forEach((subValue, key) => {
      result.set(key, baseClone(subValue, bitmask, customizer, key, value, stack))
    })
    return result
  }

  if (tag == setTag) {
    value.forEach((subValue) => {
      result.add(baseClone(subValue, bitmask, customizer, subValue, value, stack))
    })
    return result
  }
  // 判断类型化数组,比如 Int8Array,Uint8Array
  if (isTypedArray(value)) {
    return result
  }
  // 根据是否赋值原型链对象 以及 是否拷贝 Symbol 赋值 keysFunc
  // 这个函数用来获取value应该被拷贝的key值
  const keysFunc = isFull
    ? (isFlat ? getAllKeysIn : getAllKeys)
    : (isFlat ? keysIn : keys)

  const props = isArr ? undefined : keysFunc(value)
  // arrayEach 是一个有中断循环功能的 forEach
  arrayEach(props || value, (subValue, key) => {
    if (props) {
      key = subValue
      subValue = value[key]
    }
    // 递归填充克隆(有堆栈溢出的风险)
    assignValue(result, key, baseClone(subValue, bitmask, customizer, key, value, stack))
  })
  return result
}
复制代码

二进制技巧

cloneDeep当中,bitmask的值是 14 进行按位或运算的结果。

按位或运算

对每一对比特位执行或(OR)操作。只有 a 或者 b 中至少有一位是 1 时, a OR b 才为 1。或操作的真值表:

aba | b
000
011
101
111

对应的二进制运算是这样子的:

const CLONE_DEEP_FLAG = 1
const CLONE_SYMBOLS_FLAG = 4
const CLONE_FLAT_FLAG = 2

function cloneDeep(value) {
  return baseClone(value, CLONE_DEEP_FLAG | CLONE_SYMBOLS_FLAG)
}

// 运算

  0000 0001 // 1 
| 0000 0010 // 4
----------------
   0000 0011 // 5
   
复制代码

baseclone中进行与运算:

对每一对比特位执行与(AND)操作。只有 a 和 b 都是 1 时,a AND b 才是 1。与操作的真值表如下:

aba & b
000
010
100
111
const isDeep = bitmask & CLONE_DEEP_FLAG // 是否深拷贝
const isFlat = bitmask & CLONE_FLAT_FLAG // 是否拷贝原型链上的属性
const isFull = bitmask & CLONE_SYMBOLS_FLAG // 是否拷贝 Symbol

// bitmask & CLONE_DEEP_FLAG:

  0000 0011 // 5
& 0000 0001 // 1
----------------
  0000 0001 //1
 
// bitmask & CLONE_FLAT_FLAG

  0000 0011 // 5
& 0000 0010 // 2
----------------
  0000 0010 // 2
  
// bitmask & CLONE_SYMBOLS_FLAG

  0000 0011 // 5
& 0000 0100 // 4
----------------
  0000 0000 // 0
  
// 所以
const isDeep = true // 是否深拷贝
const isFlat = true // 是否拷贝原型链上的属性
const isFull = false // 是否拷贝 Symbol

复制代码

到这里我们就明白了lodash的deepClone是进行深拷贝,并且拷贝原型链上的属性但是不会拷贝Symbol的。

关于二进制的应用,其实在react当中也有,比如effectFlag

虽然业务中不常使用位操作,但在特定场景下位操作时很方便、高效的方式。比如baseClone这里是用来表示3个布尔值,但是我们只用了1个参数即可表示,那么在有需要多个布尔值的数据结构下,用一个二进制来表示这些布尔值是最高效的,我们只需要把对应的每个布尔值用不同的2的整数幂表示即可。

数组和正则

// 判断数组
const isArr = Array.isArray(value)
const tag = getTag(value)
// 处理数组
if (isArr) {
    result = initCloneArray(value)
    if (!isDeep) {
      return copyArray(value, result)
    }
}
复制代码

判断是数组就执行initCloneArray,如果是不是深拷贝执行copyArray的返回结果。

为什么不直接 new Array 或者用字面量的形式创建数组呢?我们来看源码:

initCloneArray:

// 初始化一个数组
function initCloneArray(array) {
  // 构造一个相同程度的数组
  const { length } = array
  const result = new array.constructor(length)

  // Add properties assigned by `RegExp#exec`.
  // hasOwnProperty 即 Object.prototype.hasOwnProperty 
  if (length && typeof array[0] === 'string' && hasOwnProperty.call(array, 'index')) {
    result.index = array.index
    result.input = array.input
  }
  return result
}
复制代码

用array对象的构造函数去构造一个相同程度的数组,并且还判断了正则匹配结果返回的数组,即RegExp.prototype.exec()

我也是到这里才发现原来这个方法返回的数组是有 indexinput 属性的。

不得不说,lodash十分严谨。

copyArray则是将刚刚创建好的空数组,进行浅拷贝:

function copyArray(source, array) {
  let index = -1
  const length = source.length

  array || (array = new Array(length))
  while (++index < length) {
    array[index] = source[index]
  }
  return array
}
复制代码

tag分类

获取tag的方式是通过Object.prototype.toString的方式。

const tag = getTag(value)
// ...

const toString = Object.prototype.toString

function getTag(value) {
  if (value == null) {
    return value === undefined ? '[object Undefined]' : '[object Null]'
  }
  return toString.call(value)
}
// ...

// 所有的tag
/** `Object#toString` result references. */
const argsTag = '[object Arguments]'
const arrayTag = '[object Array]'
const boolTag = '[object Boolean]'
const dateTag = '[object Date]'
const errorTag = '[object Error]'
const mapTag = '[object Map]'
const numberTag = '[object Number]'
const objectTag = '[object Object]'
const regexpTag = '[object RegExp]'
const setTag = '[object Set]'
const stringTag = '[object String]'
const symbolTag = '[object Symbol]'
const weakMapTag = '[object WeakMap]'

const arrayBufferTag = '[object ArrayBuffer]'
const dataViewTag = '[object DataView]'
const float32Tag = '[object Float32Array]'
const float64Tag = '[object Float64Array]'
const int8Tag = '[object Int8Array]'
const int16Tag = '[object Int16Array]'
const int32Tag = '[object Int32Array]'
const uint8Tag = '[object Uint8Array]'
const uint8ClampedTag = '[object Uint8ClampedArray]'
const uint16Tag = '[object Uint16Array]'
const uint32Tag = '[object Uint32Array]'
复制代码

这些列出来的tag,几乎是所有的JavaScript 标准内置对象(为什么说几乎,因为还有一些内置对象比如WebAssembly.Global等没有列出,具体可以参考MDN),接下来会根据这些tag创建不同的拷贝对象。

核心拷贝过程

if (isArr) {
    // ...
  } else {
    // 判断函数
    const isFunc = typeof value === 'function'
    // 处理 `Buffer`
    if (isBuffer(value)) {
      return cloneBuffer(value, isDeep)
    }
    // 如果是Object、Arguments、或者是Function并且没有传入父对象的参数
    if (tag == objectTag || tag == argsTag || (isFunc && !object)) {
      result = (isFlat || isFunc) ? {} : initCloneObject(value)
      if (!isDeep) {
        return isFlat
          ? copySymbolsIn(value, copyObject(value, keysIn(value), result))
          : copySymbols(value, Object.assign(result, value))
      }
    } else {
      if (isFunc || !cloneableTags[tag]) {
        return object ? value : {}
      }
      result = initCloneByTag(value, tag, isDeep)
    }
  }
复制代码

判断当前要拷贝的对象是否满足为ObjectArguments、或者是Function并且没有传入父对象即value这个参数:

  • 如果是:
    • 如果是要拷贝到原型链或者是一个函数,那么初始化result为空对象{},否则执行initCloneObject
      • 如果不是深拷贝:
        • 如果是要拷贝原型链则返回copySymbolsIn调用的结果
        • 否则copySymbols调用的结果
  • 否则:
    • 如果当前拷贝对象是函数或者cloneableTags[tag]的值为假
      • 有传入父对象即value这个参数,返回当前要拷贝的对象本身,否则返回空对象{}
    • initCloneByTag的执行结果赋值给 result

initCloneObject:

function initCloneObject(object) {
  // 构造函数是一个函数,并且不是原型对象
  return (typeof object.constructor === 'function' && !isPrototype(object))
    ? Object.create(Object.getPrototypeOf(object))
    : {}
}
// ...
const objectProto = Object.prototype
// 检查 `value` 是否可能是原型对象。
function isPrototype(value) {
  const Ctor = value && value.constructor
  const proto = (typeof Ctor === 'function' && Ctor.prototype) || objectProto

  return value === proto
}
复制代码

一般来说,只要不是原型对象,都会走到 Object.create 生成新对象,但是如果是一个原型对象,则是直接创建一个空对象{},其实这里我挺不理解的,为什么是原型对象的情况,不去拷贝原型对象上的属性,而是直接创建一个空对象呢? 希望有了解的大佬指点一下。

copySymbolsIn:

copySymbolsIn(value, copyObject(value, keysIn(value), result))调用了2个函数keysIncopyObject,我们先来看keysIn

keysIn

/**
 * Creates an array of the own and inherited enumerable property names of `object`.
 *
 *
 * @static
 * @memberOf _
 * @since 3.0.0
 * @category Object
 * @param {Object} object The object to query.
 * @returns {Array} Returns the array of property names.
 * @example
 *
 * function Foo() {
 *   this.a = 1;
 *   this.b = 2;
 * }
 *
 * Foo.prototype.c = 3;
 *
 * _.keysIn(new Foo);
 * // => ['a', 'b', 'c'] (iteration order is not guaranteed)
 */
function keysIn(object) {
  const result = []
  for (const key in object) {
    result.push(key)
  }
  return result
}
复制代码

代码结合注释和例子,不难发现这其实就是for..in的方式去获取对象及其原型链上可遍历的属性。

copyObject中最终会调用assignValue,而assignValue当中又调用了baseAssignValue

/**
 * Copies properties of `source` to `object`.
 *
 * @private
 * @param {Object} source The object to copy properties from.
 * @param {Array} props The property identifiers to copy.
 * @param {Object} [object={}] The object to copy properties to.
 * @param {Function} [customizer] The function to customize copied values.
 * @returns {Object} Returns `object`.
 */
function copyObject(source, props, object, customizer) {
  const isNew = !object
  object || (object = {})

  for (const key of props) {
    let newValue = customizer
      ? customizer(object[key], source[key], key, object, source)
      : undefined

    if (newValue === undefined) {
      newValue = source[key]
    }
    if (isNew) {
      baseAssignValue(object, key, newValue)
    } else {
      assignValue(object, key, newValue)
    }
  }
  return object
}
// ...
// 如果现有值不相等值未定义而且键 key 不在对象中,则将 value 分配给 object[key]
function assignValue(object, key, value) {
  const objValue = object[key]
  // object上没有有这个key并且值不相等的情况直接分配
  // 这里eq的判断也很有意思,感兴趣的朋友可以自己去看看
  if (!(hasOwnProperty.call(object, key) && eq(objValue, value))) {
    // 值可用
    if (value !== 0 || (1 / value) === (1 / objValue)) {
      baseAssignValue(object, key, value)
    }
    // 值未定义而且键 key 不在对象中
  } else if (value === undefined && !(key in object)) {
    baseAssignValue(object, key, value)
  }
}
// ...
// 赋值基本实现,没有值检查。
function baseAssignValue(object, key, value) {
  if (key == '__proto__') {
    Object.defineProperty(object, key, {
      'configurable': true,
      'enumerable': true,
      'value': value,
      'writable': true
    })
  } else {
    object[key] = value
  }
}

复制代码

所以copyObject做的事情就是将key值数组从源对象上遍历取值,然后浅拷贝给新对象。

现在让我们回到copySymbolsIn:

copySymbolsIn(value, copyObject(value, keysIn(value), result))
复制代码

这里先是调用了copyObject获取了value自身以及原型链的可遍历属性数组,然后浅拷贝给result,并返回result,所以此时copySymbolsIn传递的参数是value以及已经拷贝了不包含Symbol键值的result。那么很显然,从方法名可以看出这个函数就是要拷贝Symbol键值对:

function copySymbolsIn(source, object) {
  return copyObject(source, getSymbolsIn(source), object)
}
复制代码

果然,在copySymbolsIn里又再次调用了copyObject,这次的key值数组是getSymbolsIn执行的结果,我们来看:

function getSymbolsIn(object) {
  const result = []
  // 递归获取原型链上的Symbol键值
  while (object) {
    result.push(...getSymbols(object))
    object = Object.getPrototypeOf(Object(object))
  }
  return result
}
//...
const propertyIsEnumerable = Object.prototype.propertyIsEnumerable
const nativeGetSymbols = Object.getOwnPropertySymbols

// 获取 object 上可枚举的 Symbol 属性,返回属性数组
function getSymbols(object) {
  if (object == null) {
    return []
  }
  object = Object(object)
  return nativeGetSymbols(object).filter((symbol) => propertyIsEnumerable.call(object, symbol))
}
复制代码

我们再往前回到:

if (!isDeep) {
    return isFlat
      ? copySymbolsIn(value, copyObject(value, keysIn(value), result))
      : copySymbols(value, Object.assign(result, value))
    }
复制代码

我们刚刚查看了copySymbolsIn,知道了拷贝原型链上的属性的执行过程,现在我们看不拷贝原型链的执行:

copySymbols(value, Object.assign(result, value))
// ...
function copySymbols(source, object) {
  return copyObject(source, getSymbols(source), object)
}
复制代码

执行过程是相似的,先是调用Object.assign执行浅拷贝,然后再拷贝Symbol键值。

到这里可以发现,如果是浅拷贝的话,默认是拷贝Symbol键值且不能改变的。

那么接下来我们来看深拷贝的流程,我们再回到代码:

// 如果是Object、Arguments、或者是Function并且没有传入父对象的参数
if (tag == objectTag || tag == argsTag || (isFunc && !object)) {
  result = (isFlat || isFunc) ? {} : initCloneObject(value)
  if (!isDeep) {
    // ...
  }
} else {
  if (isFunc || !cloneableTags[tag]) {
    return object ? value : {}
  }
  result = initCloneByTag(value, tag, isDeep)
}
复制代码

我们来看cloneableTags[tag]是个什么:

const cloneableTags = {}
cloneableTags[argsTag] = cloneableTags[arrayTag] =
cloneableTags[arrayBufferTag] = cloneableTags[dataViewTag] =
cloneableTags[boolTag] = cloneableTags[dateTag] =
cloneableTags[float32Tag] = cloneableTags[float64Tag] =
cloneableTags[int8Tag] = cloneableTags[int16Tag] =
cloneableTags[int32Tag] = cloneableTags[mapTag] =
cloneableTags[numberTag] = cloneableTags[objectTag] =
cloneableTags[regexpTag] = cloneableTags[setTag] =
cloneableTags[stringTag] = cloneableTags[symbolTag] =
cloneableTags[uint8Tag] = cloneableTags[uint8ClampedTag] =
cloneableTags[uint16Tag] = cloneableTags[uint32Tag] = true
cloneableTags[errorTag] = cloneableTags[weakMapTag] = false
复制代码

是一个对象,且只有errorTagweakMapTag 对应的value是false,其他的都是true

也就是说如果valueErrorWeakMap,并且有传入父对象参数的话,会直接返回引用,否则是创建空对象。

而如果是函数的话,在没有传参父对象的情况下会直接返回空对象{},否则返回自身的引用。

initCloneByTag:

这是根据不同的对象类型来初始化result,我们看看具体怎么做的:

function initCloneByTag(object, tag, isDeep) {
  const Ctor = object.constructor
  switch (tag) {
    case arrayBufferTag:
      return cloneArrayBuffer(object)

    case boolTag:
    case dateTag:
      // 一元正号运算符(+)位于其操作数前面,计算其操作数的数值
      // 如果操作数不是一个数值,会尝试将其转换成一个数值。
      // 尽管一元负号也能转换非数值类型
      // 但是一元正号是转换其他对象到数值的最快方法,也是最推荐的做法,因为它不会对数值执行任何多余操作
      // + true;  1
      // + false;  0
      // + new Date(); 相当于 new Date().getTime()
      return new Ctor(+object)

    case dataViewTag:
      return cloneDataView(object, isDeep)

    case float32Tag: case float64Tag:
    case int8Tag: case int16Tag: case int32Tag:
    case uint8Tag: case uint8ClampedTag: case uint16Tag: case uint32Tag:
      return cloneTypedArray(object, isDeep)

    case mapTag:
      // new Map
      return new Ctor

    case numberTag:
    case stringTag:
      return new Ctor(object)

    case regexpTag:
      return cloneRegExp(object) // 稍后说明

    case setTag:
      // new Set
      return new Ctor

    case symbolTag:
      return cloneSymbol(object) // 稍后说明
  }
}

复制代码

我们主要来看克隆正则对象和Symbol:

// ./cloneRegExp.js
const reFlags = /\w*$/;
//w匹配任意一个包括下划线的任何单词字符等价于[A-Za-z0-9_]
function cloneRegExp(regexp) {
    const result = new regexp.constructor(regexp.source, reFlags.exec());//返回当前匹配的文本;
    result.lastIndex = regexp.lastIndex; //表示下一次匹配的开始位置
    return result;
}

// ./cloneSymbol.js
const symbolValueOf = Symbol.prototype.valueOf;
function cloneSymbol(symbol) {
  return symbolValueOf ? Object(symbolValueOf.call(symbol)) : {};
}
复制代码

解决循环引用

构造了一个栈用来解决循环引用的问题。

stack || (stack = new Stack)
const stacked = stack.get(value)
// 已存在
if (stacked) {
    return stacked
}
stack.set(value, result)
复制代码

如果当前需要拷贝的值已存在于栈中,说明有环,直接返回即可。栈中没有该值时保存到栈中,传入 valueresult。这里的 result 是一个对象引用,后续对 result 的修改也会反应到栈中。

Stack 感兴趣的同学可以自行查看lodash如何实现一个栈的。

深拷贝的局限

以下内容摘自 zhuanlan.zhihu.com/p/160315811

如果需要对一个复杂对象进行频繁操作,每次都完全深拷贝一次的话性能岂不是太差了,因为大部分场景下都只是更新了这个对象的某几个字段,而其他的字段都不变,对这些不变的字段的拷贝明显是多余的。那么问题来了,浅拷贝不更新,深拷贝性能差,怎么办?

这里推荐3个可以实现”部分“深拷贝的库:

  • Immutable.js

    Immutable.js 会把对象所有的 key 进行 hash 映射,将得到的 hash 值转化为二进制,从后向前每 5 位进行分割后再转化为 Trie 树。Trie 树利用这些 hash 值的公共前缀来减少查询时间,最大限度地减少无谓 key 的比较。

  • seamless-immutable

    如果数据量不大但想用这种类似 updateIn 便利的语法的话可以用 seamless-immutable。这个库就没有上面的 Trie 树这些幺蛾子了,就是为其扩展了 updateIn、merge 等 9 个方法的普通简单对象,利用 Object.freeze 冻结对象本身改动, 每次修改返回副本。感觉像是阉割版,性能不及 Immutable.js,但在部分场景下也是适用的。

  • Immer.js

    通过用来数据劫持的 Proxy 实现:对原始数据中每个访问到的节点都创建一个 Proxy,修改节点时修改副本而不操作原数据,最后返回到对象由未修改的部分和已修改的副本组成。(这不就是 Vue3 数据响应式原理嘛)

关于 Immutable.jsImmer.js 可以查看 juejin.cn/post/684490… 进行更多了解。

参考:

分类:
前端
标签: