深入浅出loadash深拷贝源码
本文会从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)
思路:
首先检查target是否为
null
或undefined
,是的话报错。使用
Object()
将target
转成对象,并将这个对象赋值给target
。遍历每个
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;
}
但是很快你就会发现这有很多缺点,那就是不支持Null、Array、RegExp、Date,以及ES6的Set、Map、WeakSet、WeakMap等等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
在初始化新的对象target
和newTarget
时,我们判断一下是否为数组,是的话初始化一个空数组:
function createTarget(obj) {
return Array.isArray(obj) ? [] : {};
}
其实数组还有一种情况,RegExp.prototype.exec()
方法返回一个数组,包含额外的属性 index
和 input
。
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的值是 1
和 4
进行按位或运算的结果。
按位或运算
:
对每一对比特位执行或(OR)操作。只有 a 或者 b 中至少有一位是 1 时, a OR b 才为 1。或操作的真值表:
a | b | a | b |
---|---|---|
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 1 |
对应的二进制运算是这样子的:
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。与操作的真值表如下:
a | b | a & b |
---|---|---|
0 | 0 | 0 |
0 | 1 | 0 |
1 | 0 | 0 |
1 | 1 | 1 |
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()
。
我也是到这里才发现原来这个方法返回的数组是有 index
和 input
属性的。
不得不说,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)
}
}
判断当前要拷贝的对象是否满足为Object
、Arguments
、或者是Function
并且没有传入父对象即value
这个参数:
- 如果是:
- 如果是要拷贝到原型链或者是一个函数,那么初始化result为空对象
{}
,否则执行initCloneObject
- 如果不是深拷贝:
- 如果是要拷贝原型链则返回
copySymbolsIn
调用的结果 - 否则
copySymbols
调用的结果
- 如果是要拷贝原型链则返回
- 如果不是深拷贝:
- 如果是要拷贝到原型链或者是一个函数,那么初始化result为空对象
- 否则:
- 如果当前拷贝对象是函数或者
cloneableTags[tag]
的值为假- 有传入父对象即value这个参数,返回当前要拷贝的对象本身,否则返回空对象
{}
- 有传入父对象即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个函数keysIn
和copyObject
,我们先来看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
是一个对象,且只有errorTag
和 weakMapTag
对应的value是false
,其他的都是true
。
也就是说如果value
是Error
或 WeakMap
,并且有传入父对象参数的话,会直接返回引用,否则是创建空对象。
而如果是函数的话,在没有传参父对象的情况下会直接返回空对象{}
,否则返回自身的引用。
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)
如果当前需要拷贝的值已存在于栈中,说明有环,直接返回即可。栈中没有该值时保存到栈中,传入 value
和 result
。这里的 result
是一个对象引用,后续对 result
的修改也会反应到栈中。
对 Stack
感兴趣的同学可以自行查看lodash如何实现一个栈的。
深拷贝的局限
以下内容摘自 https://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.js 和 Immer.js 可以查看 https://juejin.cn/post/6844904111402385422 进行更多了解。
参考: