深拷贝实现细节远不止递归(上篇)
上篇《不用递归也能实现深拷贝》重点在实现算法上,这篇通过 loadsh cloneDeep
的源码总结实现深拷贝更多细节,如果说上篇是总结实现深拷贝的原理话,那么这篇就是总结如何考虑各种细节写出一个高质量的深拷贝工具库。
loadsh cloneDeep
通过以下几个细节来实现
- 目标对象的各种类型的复制处理
- 处理
function
- 兼容
node
环境 - 处理循环引用
- 过滤原型属性
- 递归
处理目标对象的各种类型
这是深拷贝实现的难点,经常会忽略一些类型的处理。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…in
和 Object.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