扒一扒Lodash 中的深拷贝,学问可真不少 | 【javascript基础系列】| 源码解读
前言
本文是在学习
Loash
源码的时候做的笔记和总结 深拷贝,这个知识点主要涉及到JS
的类型,类型判断 等 这些知识点 我们平时可能很多时候用 JSON.parse(JSON.stringify(xx)) 来实现简单的深拷贝,但是会存在很大的问题 看上去知识点很简单,但是真的简单吗?其实细节很多,一点都不简单
JS 中深拷贝的痛点在哪里
- JS中的数据类型比较多,需要一个个判断,所以,如何有效的区分每个细分的数据类型
- 如何深拷贝每种数据类型
- 面对循环引用问题,如何解决
源码中文备注
// clone - baseClone(value, CLONE_SYMBOLS_FLAG) 浅拷贝
// cloneWith - baseClone(value, CLONE_SYMBOLS_FLAG, customizer) 浅拷贝 customizer 接受一个函数自定义拷贝的函数(如果这个函数返回undefined,那么按照正常的逻辑来走)
// cloneDeep - baseClone(value, CLONE_DEEP_FLAG | CLONE_SYMBOLS_FLAG) 深拷贝 ,会递归的拷贝
// cloneDeepWith - baseClone(value, CLONE_DEEP_FLAG | CLONE_SYMBOLS_FLAG, customizer) 深拷贝,也可以接受 自定义的拷贝函数 customizer
/**
* The base implementation of `clone` and `cloneDeep` which tracks
* traversed objects.
*
* @private
* @param {*} value The value to clone. 需要拷贝的值
* @param {number} bitmask The bitmask flags. 配置值
* 1 - Deep clone 1.深拷贝
* 2 - Flatten inherited properties
* 4 - Clone symbols
* @param {Function} [customizer] The function to customize cloning. 自定义拷贝函数
* @param {string} [key] The key of `value`. 需要拷贝的值 对应的 key
* @param {Object} [object] The parent object of `value`. 需要拷贝的值
* @param {Object} [stack] Tracks traversed objects and their clone counterparts. 将每个需要拷贝的值,存进缓存,主要是为了解决 循环引用问题
* @returns {*} Returns the cloned value.
*/
function baseClone(value, bitmask, customizer, key, object, stack) {
let result
// bitmask 通过这一个配置的值,决定 下面 isDeep isFlat isFull 这三个配置值
// & 位操作符号,通过一个值与不同的值进行 位操作,能决定出多个个不同的值!
// & 位操作符的优点是:节省内存,但是缺点也很明显:不直观。一般不建议用,直接 设置多个参数值即可
const isDeep = bitmask & CLONE_DEEP_FLAG // 是否为深拷贝(false 即浅拷贝)
const isFlat = bitmask & CLONE_FLAT_FLAG // 是否展平 : 决定是否将对象的原型上的属性也拷贝
const isFull = bitmask & CLONE_SYMBOLS_FLAG //是否是全部的key:决定是否将 symbol的的 key 也深拷贝
// 深拷贝具体对应的配置是 isDeep = true, isFlat = false , isFull = true
// 如果有自定义 拷贝函数
if (customizer) {
result = object ? customizer(value, key, object, stack) : customizer(value)
}
// 如果自定义的 拷贝方法 可以返回值,那么直接返回即可
if (result !== undefined) {
return result
}
// 如果是 基本类型,那么直接返回 值即可
if (!isObject(value)) {
return value
}
const isArr = Array.isArray(value)
// 此处的tag 是 Object.prototype.toString.call(value) 的结果 [object Array] 等
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)) {
// buffer的拷贝
return cloneBuffer(value, isDeep)
}
// 如果是 Object 或则 Arguments 或则 function(顶层函数,也就是第一层的函数)
if (tag == objectTag || tag == argsTag || (isFunc && !object)) {
// 初始化 Object
// 注意:如果 isFlat(key展平) 或则是 函数(顶层函数),那么只是 初始化为 {}
result = (isFlat || isFunc) ? {} : initCloneObject(value)
if (!isDeep) {
// 浅拷贝 isFlat : 是否展平 - 是否获取到该对象的原型链上的属性的key
// 如果是深拷贝:根本不会进入此代码块
return isFlat
? copySymbolsIn(value, copyObject(value, keysIn(value), result))
: copySymbols(value, Object.assign(result, value))
}
} else {
// 如果是 function(此处为 非顶层的 function,也就从第二层开始往后的所有的 function),那么就直接返回函数,也就是 函数除了第一层的函数,其余的都是 浅拷贝
// 如果是 Loash 库 不支持的 类型,那么直接返回其值
if (isFunc || !cloneableTags[tag]) {
return object ? value : {}
}
// 初始化其他类型
result = initCloneByTag(value, tag, isDeep)
}
}
// 循环引用的问题
// Check for circular references and return its corresponding clone.
// 解决循环引用问题,用缓存把每个值都存下来,如果缓存有,肯定是循环引用,直接从缓存中取值即可
stack || (stack = new Stack)
const stacked = stack.get(value)
if (stacked) {
return stacked
}
stack.set(value, result)
// Map 类型的拷贝
if (tag == mapTag) {
value.forEach((subValue, key) => {
result.set(key, baseClone(subValue, bitmask, customizer, key, value, stack))
})
return result
}
// Set类型的拷贝
if (tag == setTag) {
value.forEach((subValue) => {
result.add(baseClone(subValue, bitmask, customizer, subValue, value, stack))
})
return result
}
// TODO 类数组类型的处理
// 如果是 类型化数组,那么直接返回
// https://zhuanlan.zhihu.com/p/258353637 详细的解析
if (isTypedArray(value)) {
return result
}
// 获取所有的 key,以便下面循环 拷贝用
// 深拷贝,此处 const keysFunc = getAllKeys
const keysFunc = isFull
? (isFlat ? getAllKeysIn : getAllKeys)
: (isFlat ? keysIn : keys)
const props = isArr ? undefined : keysFunc(value)
// 循环深拷贝每个属性,里面会递归
arrayEach(props || value, (subValue, key) => {
if (props) {
key = subValue
subValue = value[key]
}
// Recursively populate clone (susceptible to call stack limits).
// 递归拷贝(可能会收到 stack 的限制,也就是我们之前在上面写到的 stack 的限制为200)
assignValue(result, key, baseClone(subValue, bitmask, customizer, key, value, stack))
})
return result
}
Lodash 中的深拷贝是如何实现的
Lodash 深拷贝 流程梳理
确定配置参数
- bitmask:配置标志,主要是用于 位操作的,根据传入的值,可以得到下面的几个配置的具体参数
- isDeep:是否要深拷贝,一般调用
cloneDeep
方法,此处为true
- isFlat:是否要将
Object
里面的属性拉平,深拷贝此处为false
- isFull: 是否全部属性都要遍历,主要是
symbol
类型的属性,深拷贝此处为true
- isDeep:是否要深拷贝,一般调用
- customizer: 自定义的拷贝方法,
_.cloneDeepWith(value, [customizer])
主要用在这个方法里面,也就是自己可以自定义对某些特殊的地方自己定义拷贝函数;此处分析我们不考虑这个 - key:
value
对应的key
,主要是递归调用的时候,递归里面的每个项 - object:
value
的父数据,value
是object
的key
的值,一样也是 递归的时候用到的 - stack:这个对象,主要用于判断当前对象是不是循环引用
自定义拷贝函数
如果customizer
有值,那么直接调用customizer
函数,得到结果,如果有结果,那么直接返回结果即可,如果没有结果,相当于 customizer
没有作用,继续执行下面的代码
注意:customizer
在进行递归调用的过程中一直是起作用的
// 如果有自定义 拷贝函数
if (customizer) {
result = object ? customizer(value, key, object, stack) : customizer(value)
}
// 如果自定义的 拷贝方法 可以返回值,那么直接返回即可
if (result !== undefined) {
return result
}
如果是基本类型,直接返回
这个比较简单,如果不是引用类型,那么直接返回
function isObject(value) {
const type = typeof value
return value != null && (type === 'object' || type === 'function')
}
// 如果不是引用类型,也就是 基本类型,那么直接返回 值即可
if (!isObject(value)) {
return value
}
根据不同的数据类型,初始化一个对象,后面用于深拷贝
- 如果是
Array
,初始化一个空的Array
,以便后面使用 - 如果是
Buffer
类型,那么直接复制即可
const allocUnsafe = Buffer ? Buffer.allocUnsafe : undefined
function cloneBuffer(buffer, isDeep) {
if (isDeep) {
return buffer.slice()
}
const length = buffer.length
const result = allocUnsafe ? allocUnsafe(length) : new buffer.constructor(length)
buffer.copy(result)
return result
}
- 如果是
Object
,Arguments
,function
(顶层,而不是递归里面的function
) - 否则就是初始化 对应的 类型
// 如果是 Object 或则 Arguments 或则 function(顶层函数,也就是第一层的函数)
if (tag == objectTag || tag == argsTag || (isFunc && !object)) {
// 初始化 Object
// 注意:如果 isFlat(key展平) 或则是 函数(顶层函数),那么只是 初始化为 {}
result = (isFlat || isFunc) ? {} : initCloneObject(value)
if (!isDeep) {
// 浅拷贝 isFlat : 是否展平 - 是否获取到该对象的原型链上的属性的key
// 如果是深拷贝:根本不会进入此代码块
return isFlat
? copySymbolsIn(value, copyObject(value, keysIn(value), result))
: copySymbols(value, Object.assign(result, value))
}
} else {
// 如果是 function(此处为 非顶层的 function,也就从第二层开始往后的所有的 function),那么就直接返回函数,也就是 函数除了第一层的函数,其余的都是 浅拷贝
// 如果是 Loash 库 不支持的 类型,那么直接返回其值
if (isFunc || !cloneableTags[tag]) {
return object ? value : {}
}
// 初始化其他类型
result = initCloneByTag(value, tag, isDeep)
}
注意:下面代码是根据深拷贝,做了一定的代码删除,因为源码里面此处是公共代码,有其他的逻辑(比如浅拷贝)
//简化后的代码
// 2. 如果是 数组 - 初始化数组
if (isArr) {
// 只是初始化 数组 - 为啥要初始化?
result = initCloneArray(value)
}else{
// 3. 如果是 Buffer
if (isBuffer(value)) {
// buffer的拷贝
return cloneBuffer(value, isDeep)
}
if (tag == objectTag || tag == argsTag || (isFunc && !object)) {
// 初始化 Object
result = (isFlat || isFunc) ? {} : initCloneObject(value)
}else{
if (isFunc || !cloneableTags[tag]) {
return object ? value : {}
}
result = initCloneByTag(value, tag, isDeep)
}
}
循环引用的问题
直接用一个Map,如果是循环引用,直接就返回了
// 循环引用问题
stack || (stack = new Stack)
const stacked = stack.get(value)
if (stacked) {
return stacked
}
stack.set(value, result)
map类型深拷贝
if (tag == setTag) {
value.forEach((subValue) => {
result.add(baseClone(subValue, bitmask, customizer, subValue, value, stack))
})
return result
}
数组类型和其他可迭代类型的处理
const props = isArr ? undefined : keysFunc(value)
// 循环深拷贝每个属性,里面会递归
arrayEach(props || value, (subValue, key) => {
if (props) {
key = subValue
subValue = value[key]
}
// Recursively populate clone (susceptible to call stack limits).
// 递归拷贝(可能会收到 stack 的限制,也就是我们之前在上面写到的 stack 的限制为200)
assignValue(result, key, baseClone(subValue, bitmask, customizer, key, value, stack))
})
特别注意点
关于函数的复制
- 第一层的函数是会被深拷贝的
- 再往下层递归的话,就没有深拷贝了,而是直接复制的函数的引用,也就是说 原来的函数改了,那么此处的函数也改了
// 如果是 function(此处为 非顶层的 function,也就从第二层开始往后的所有的 function),那么就直接返回函数,也就是 函数除了第一层的函数,其余的都是 浅拷贝
// 如果是 Loash 库 不支持的 类型,那么直接返回其值
if (isFunc || !cloneableTags[tag]) {
return object ? value : {}
}
- 案例测试如下
let obj_1 = {
p1_fun:function(){
console.log('p1 ====')
},
p2:{
name:'p2',
p2_fun:function(){
console.log('p2 =========')
}
}
}
const obj_2 = _.cloneDeep(obj_1)
obj_1.p1_fun = function(){
console.log('p1_fun --- 改了')
}
obj_1.p2.p2_fun = function(){
console.log('p2_fun --- 改了')
}
// 此处的func 是深拷贝,不会随着 obj1的改变而改变
console.log('obj_2.p1=====',obj_2.p1_fun())
// 此处的func 是引用,随着 obj1 里面的改变而改变
console.log('obj_2.p2.p2_fun=====',obj_1.p2.p2_fun())
对于一些不在范围内的类型(自定义的类型)
如果是一些不支持的类型,那么直接就返回了
知识点拆解(重点)
类型的判断
类型判断的方法有很多,比如typeof
,instanceof
等,但是都无法适用于全部的场景,而
Object.protoType.toString.call
可以算是比较终极的解法
注意:
- 源码中基本类型直接用
typeof
来判断的(其实也可以直接全部用toString
) - 一定要用
Object
上的原型方法,而不是 对象实例的toString
方法,因为每个实例对象都有可能自己实现了自己的实例方法,导致不准确 - 返回的值是
[Object Date]
这样的字符串
关于数据类型判断,详细的可以看这篇文章 你真的会判断javascript中的数据类型吗? | 【javascript基础系列】
关于原生js
中的类型属性有多少个,我们来看看Loash
里面支持多少类型的深拷贝!
真实情况只会比这个更多(下面的不包含基本类型)
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]'
初始化 Object
先看下对应的源码
// 初始化Object,为后面拷贝做准备
function initCloneObject(object) {
return (typeof object.constructor === 'function' && !isPrototype(object))
? Object.create(Object.getPrototypeOf(object))
: {}
}
// 判断是否是 JS 内部的原型
function isPrototype(value) {
const Ctor = value && value.constructor
const proto = (typeof Ctor === 'function' && Ctor.prototype) || objectProto
// Ctor.prototype 其实就是 拿的 value的原型 [value.constructor.prototype === valle.__proto__]
// 只是 __proto__ 这种写法 不是标准所支持的
return value === proto
}
解析
- 如果此处的值为
JS
里面原生的对象类型,比如Object
,那么初始化比较简单就是{}
- 如果此处的值不是
JS
里面的原生的对象类型,比如Vue
,那么就用此值的构造函数来初始化 - 判断当前值是否为
Object
这样的类型,也就是 源码中isPrototype
方法 - 获取一个值的构造函数
Object.getPrototypeOf(object)
- 通过一个动态的构造函数 初始化一个值
Object.create(构造函数)
,连在一起就可以这样写Object.create(Object.getPrototypeOf(object))
初始化 Array
先看对应源码
function initCloneArray(array) {
const { length } = array
const result = new array.constructor(length)
// Add properties assigned by `RegExp#exec`.
// 比较特殊的一个数组情况,也就是 通过正则产生的,会存在 index 和 input的两个属性
if (length && typeof array[0] === 'string' && hasOwnProperty.call(array, 'index')) {
result.index = array.index
result.input = array.input
}
return result
}
解析
初始化 array 能理解,但是 后面的是 index,input 是什么鬼?
通过正则产生的数组里面就可能存在这几个参数,所以必须考虑这个情况,下面的例子就完整的演示了这种情况
var str = 'For more information, see Chapter 3.4.5.1';
var re = /see (chapter \d+(.\d)*)/i;
var found = str.match(re);
console.log(found);
// logs [ 'see Chapter 3.4.5.1',
// 'Chapter 3.4.5.1',
// '.1',
// index: 22,
// input: 'For more information, see Chapter 3.4.5.1' ]
// 'see Chapter 3.4.5.1' 是整个匹配。
// 'Chapter 3.4.5.1' 被'(chapter \d+(.\d)*)'捕获。
// '.1' 是被'(.\d)'捕获的最后一个值。
// 'index' 属性 (22) 是整个匹配从零开始的索引。
// 'input' 属性是被解析的原始字符串。
其他类型初始化
function initCloneByTag(object, tag, isDeep) {
const 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)
}
}
循环引用如何解决
先看源码
// 循环引用的问题
// Check for circular references and return its corresponding clone.
stack || (stack = new Stack)
const stacked = stack.get(value)
if (stacked) {
return stacked
}
stack.set(value, result)
解析
其实也很简单,就是用一个 Set
做一个字典映射,只要是 引用类型,就直接用此处的值 作为 key
存进 缓存中
每次遇到引用的值,都直接去缓存中查看下,有的话,直接赋值即可,如果缓存中没有,那么就存进缓存
这样就解决了循环引用的问题
有个小问题:
就是缓存的存储数据的个数有大小限制 200 个
获取所有的Key
Map
的拷贝
if (tag == mapTag) {
value.forEach((subValue, key) => {
result.set(key, baseClone(subValue, bitmask, customizer, key, value, stack))
})
return result
}
Set
的拷贝
if (tag == setTag) {
value.forEach((subValue) => {
result.add(baseClone(subValue, bitmask, customizer, subValue, value, stack))
})
return result
}
总结
Loash
是一个经典,并且经过社区检验的过的一个第单方库,通过对其 深拷贝 函数的源码解读,我们学到了很多
也不经感叹:就一个小小的深拷贝,涉及的知识点和细节真多
这是一个源码阅读系列文章,不断更新,希望对大家有帮助
有问题可以留言一起探讨
转载自:https://juejin.cn/post/7152479583605325838