likes
comments
collection
share

深拷贝实现细节远不止递归(上篇)

作者站长头像
站长
· 阅读数 47

上篇《不用递归也能实现深拷贝》重点在实现算法上,这篇通过 loadsh cloneDeep 的源码总结实现深拷贝更多细节,如果说上篇是总结实现深拷贝的原理话,那么这篇就是总结如何考虑各种细节写出一个高质量的深拷贝工具库。

loadsh cloneDeep 通过以下几个细节来实现

  1. 目标对象的各种类型的复制处理
  2. 处理 function
  3. 兼容 node 环境
  4. 处理循环引用
  5. 过滤原型属性
  6. 递归

处理目标对象的各种类型

这是深拷贝实现的难点,经常会忽略一些类型的处理。JSON.parse(JSON.stringify(data)) 也是对一些类型处理不当而被开发者吐槽。loadsh 对类型处理考虑的非常细致:

类型检测

工具库做类型判断一般都会用比较严谨的 Object.prototype.toString.call :

Object.prototype.toString.call(data);

Object.prototype.toString.call 并不是都会返回对象的真实类型:

Object.prototype.toString() 返回 "[object Type]",这里的 Type 是对象的类型。如果对象有 Symbol.toStringTag 属性,其值是一个字符串,则它的值将被用作 Type。许多内置的对象,包括 Map 和 Symbol,都有 Symbol.toStringTag。一些早于 ES6 的对象没有 Symbol.toStringTag,但仍然有一个特殊的标签 ——MDN

Symbol.toStringTag属性被修改后,Object.prototype.toString.call 返回的值并不是真实的类型。

const myDate = new Date();
Object.prototype.toString.call(myDate); // [object Date]

myDate[Symbol.toStringTag] = "myDate";
Object.prototype.toString.call(myDate); // [object myDate]

Date.prototype[Symbol.toStringTag] = "prototype polluted";
Object.prototype.toString.call(new Date()); // [object prototype polluted]

因此在调用 Object.prototype.toString() 要进行 Symbol.toStringTag 判断和处理

if (Symbol.toStringTag && Symbol.toStringTag in data) {
         data[Symbol.toStringTag]=undefined
}
Object.prototype.toString.call(data);

data 如果是值类型,Symbol.toStringTag in data 会报错,需要将 data 转为对象

if (Symbol.toStringTag && Symbol.toStringTag in Object(data)) {
         data[Symbol.toStringTag]=undefined
}
Object.prototype.toString.call(data);

lodash 中类型检测 baseGetTag 代码:

var symToStringTag = Symbol ? Symbol.toStringTag : undefined;
function baseGetTag(value) {
  if (value == null) {
    return value === undefined ? undefinedTag : nullTag;
  }
  return (symToStringTag && symToStringTag in Object(value))
    ? getRawTag(value)
    : objectToString(value);
}

getRawTag:

var objectProto = Object.prototype;
var nativeObjectToString = objectProto.toString;
var hasOwnProperty = objectProto.hasOwnProperty;
function getRawTag(value) {
  var isOwn = hasOwnProperty.call(value, symToStringTag),
    tag = value[symToStringTag];

  try {
    value[symToStringTag] = undefined;
    var unmasked = true;
  } catch (e) { }

  var result = nativeObjectToString.call(value);
  if (unmasked) {
    if (isOwn) {
      value[symToStringTag] = tag;
    } else {
      delete value[symToStringTag];
    }
  }
  return result;
}

objectToString:

function objectToString(value) {
  return nativeObjectToString.call(value);
}

Array

深拷贝在处理数组上和对象类似,只是一个初始化数组,一个初始化对象。

const result = Array.isArray(source) ? [] : {}

大部分情况下没问题,但在一些框架库中有可能定义一个继承 Array 的类:

class MyArray extends Array {
  custom = function () {
    console.log('custom');
  }
}
const arr = new MyArray(1, 2, 3);
console.log(Array.isArray(arr)) // true

代码中 Array.isArray(arr) 返回的是 true,如果深拷贝的对象是 MyArray 对象,上面初始化的方式返回的是 Array 对象。严格讲深拷贝应该返回的也是 MyArray 对象。

loadsh 中通过执行 arr 的构造函数创建对象来解决这个问题。

const cloneArr = new arr.constructor(arr.length)
class MyArray extends Array {
  custom = function () {
    console.log('custom');
  }
}
const arr = new MyArray(1, 2, 3);
console.log(Array.isArray(arr)) // true
const cloneArr = new arr.constructor(arr.length)
console.log(cloneArr instanceof MyArray) // true

loadsh cloneDeep 处理数组的部分

const isArray = Array.isArray
var isArr = isArray(value);
if (isArr) {
    result = initCloneArray(value);
    if (!isDeep) {
        return copyArray(value, result);
    }
}

代码中 initCloneArray 来初始化数组,如果浅拷贝执行 copyArray

/**
 * Initializes an array clone.
 *  
 * @private
 * @param {Array} array The array to clone.
 * @returns {Array} Returns the initialized clone.
 */
function initCloneArray(array) {
    var length = array.length,
        result = new array.constructor(length);

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

这里代码处理的很细致,除了来处理继承 Array 类的拷贝以外还考虑到了正则执行返回的数组类型的拷贝。

/**
 * Copies the values of `source` to `array`.
 *
 * @private
 * @param {Array} source The array to copy values from.
 * @param {Array} [array=[]] The array to copy values to.
 * @returns {Array} Returns `array`.
 */
function copyArray(source, array) {
    var index = -1,
        length = source.length;

    array || (array = Array(length));
    while (++index < length) {
        array[index] = source[index];
    }
    return array;
}

Symbol

对象的 Symbol 属性 for…inObject.keys 都遍历不了。需要通过 Object.getOwnPropertySymbols 方法来获取 Symbol 属性。

for (let key in data) {
  if (toString.call(data[key]) === '[object Object]' || toString.call(data[key]) === '[object Array]') {
    target[key] = clone(data[key])
  } else {
    target[key] = data[key]
  }
}

加上 Symbol 的处理

var keys = Object.keys(data)
keys = keys.concat(Object.getOwnPropertySymbols(data))
keys.forEach(key=>{
  if (toString.call(data[key]) === '[object Object]' || toString.call(data[key]) === '[object Array]') {
    target[key] = clone(data[key])
  } else {
    target[key] = data[key]
  }
})

loadsh cloneDeep 处理 Symbol 的部分

/**
 * A specialized version of `_.filter` for arrays without support for
 * iteratee shorthands.
 *
 * @private
 * @param {Array} [array] The array to iterate over.
 * @param {Function} predicate The function invoked per iteration.
 * @returns {Array} Returns the new filtered array.
 */
function arrayFilter(array, predicate) {
  var index = -1,
      length = array == null ? 0 : array.length,
      resIndex = 0,
      result = [];

  while (++index < length) {
    var value = array[index];
    if (predicate(value, index, array)) {
      result[resIndex++] = value;
    }
  }
  return result;
}

function stubArray() {
  return [];
}

var propertyIsEnumerable = objectProto.propertyIsEnumerable;
var nativeGetSymbols = Object.getOwnPropertySymbols;

/**
 * Creates an array of the own enumerable symbols of `object`.
 *
 * @private
 * @param {Object} object The object to query.
 * @returns {Array} Returns the array of symbols.
 */
var getSymbols = !nativeGetSymbols ? stubArray : function(object) {
  if (object == null) {
    return [];
  }
  object = Object(object);
  return arrayFilter(nativeGetSymbols(object), function(symbol) {
    return propertyIsEnumerable.call(object, symbol);
  });
};

代码中处理的更为严谨,通过 Object.getOwnPropertySymbols 获取对象 Symbol 属性后过通过 propertyIsEnumerable 过滤掉不可以枚举的属性。

代码中还封装 arrayFilter 函数来实现类似 Array.prototype.filter ,这种方式有以下两个优点:

  • 大量空位的数组或者类数组对象,在使用 JavaScript 原生的 Array.prototype.filter 方法时可能会出现问题
  • arrayFilter 方法支持更多的数据类型,包括数组、类数组对象、对象等,而 JavaScript 原生方法只支持数组

Set

判断 Set 类型就相对简单点

/**
 * Initializes an object clone based on its `toStringTag`.
 *
 * **Note:** This function only supports cloning values with tags of
 * `Boolean`, `Date`, `Error`, `Map`, `Number`, `RegExp`, `Set`, or `String`.
 *
 * @private
 * @param {Object} object The object to clone.
 * @param {string} tag The `toStringTag` of the object to clone.
 * @param {boolean} [isDeep] Specify a deep clone.
 * @returns {Object} Returns the initialized clone.
 */
function initCloneByTag(object, tag, isDeep) {
  var Ctor = object.constructor;
  switch (tag) {
    case arrayBufferTag:
      return cloneArrayBuffer(object);

    case boolTag:
    case dateTag:
      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:
      return new Ctor;

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

    case regexpTag:
      return cloneRegExp(object);

    case setTag:
      return new Ctor;

    case symbolTag:
      return cloneSymbol(object);
  }
}
var setTag = '[object Set]'
function baseIsSet(value) {
  return isObjectLike(value) && getTag(value) == setTag;
}
var nodeIsSet = nodeUtil && nodeUtil.isSet;
var isSet = nodeIsSet ? baseUnary(nodeIsSet) : baseIsSet;//判断 node 环境下还是浏览器环境下

var tag = getTag(value),
result = initCloneByTag(value, tag, isDeep);
if (isSet(value)) {
  value.forEach(function(subValue) {
    result.add(baseClone(subValue, bitmask, customizer, subValue, value, stack));
 });

Map

var setTag = '[object Map]'
function baseIsSet(value) {
  return isObjectLike(value) && getTag(value) == mapTag;
}
var nodeIsSet = nodeUtil && nodeUtil.isMap;
var isMap = nodeIsMap ? baseUnary(nodeIsMap) : baseIsMap;//判断 node 环境下还是浏览器环境下

var tag = getTag(value),
result = initCloneByTag(value, tag, isDeep);
if (isMap(value)) {
  value.forEach(function(subValue, key) {
    result.set(key, baseClone(subValue, bitmask, customizer, key, value, stack));
  });
}

其他对象通过 initCloneByTag 初始化复制对象。

扩展

上篇文章有评论为什么不使用 structedClone 来实现深拷贝, structedClone本身的一些问题暂不考虑使用:

  • 兼容性:虽然现在主流浏览器都支持,但是 chrome 在 98 以上才开始支持,还是会有兼容性的风险
  • 对象包含Function会直接报错,而JSON.parse(JSON.stringify(obj))会忽略Function, 因此在使用structedClone之前还要遍历对象检测是否包含Function,报错的体验远比忽略差很多。
转载自:https://juejin.cn/post/7244083820214566970
评论
请登录