引言

在上一篇文章中介绍了如何实现一个深拷贝,分别说明了对象、数组、循环引用、引用丢失、Symbol 和递归爆栈等情况下的深拷贝实践,今天我们来看看  Lodash 如何实现上述之外的函数、正则、Date、Buffer、Map、Set、原型链等情况下的深拷贝实践。本篇文章源码基于Lodash  4.17.11 版本。

整体流程

入口

入口文件是 cloneDeep.js,直接调用核心文件 baseClone.js 的方法。// 木易杨

const CLONE_DEEP_FLAG = 1

const CLONE_SYMBOLS_FLAG = 4

function cloneDeep(value) {

return baseClone(value, CLONE_DEEP_FLAG | CLONE_SYMBOLS_FLAG)

}

第一个参数是需要拷贝的对象,第二个是位掩码(Bitwise),关于位掩码的详细介绍请看下面拓展部分。

baseClone 方法

然后我们进入 ./.internal/baseClone.js 路径查看具体方法,主要实现逻辑都在这个方法里。

先介绍下该方法的参数 baseClone(value, bitmask, customizer, key, object, stack)value:需要拷贝的对象

bitmask:位掩码,其中 1 是深拷贝,2 拷贝原型链上的属性,4 是拷贝 Symbols 属性

customizer:定制的 clone 函数

key:传入 value 值的 key

object:传入 value 值的父对象

stack:Stack 栈,用来处理循环引用

我将分成以下几部分进行讲解,可以选择自己感兴趣的部分阅读。位掩码

定制 clone 函数

非对象

数组 & 正则

对象 & 函数

循环引用

Map & Set

Symbol & 原型链

baseClone 完整代码

这部分就是核心代码了,各功能分割如下,详细功能实现部分将对各个功能详细解读。// 木易杨

function baseClone(value, bitmask, customizer, key, object, stack) {

let result

// 标志位

const isDeep = bitmask & CLONE_DEEP_FLAG        // 深拷贝,true

const isFlat = bitmask & CLONE_FLAT_FLAG        // 拷贝原型链,false

const isFull = bitmask & CLONE_SYMBOLS_FLAG    // 拷贝 Symbol,true

// 自定义 clone 函数

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)

const tag = getTag(value)

if (isArr) {

// 数组

result = initCloneArray(value)

if (!isDeep) {

return copyArray(value, result)

}

} else {

// 对象

const isFunc = typeof value == 'function'

if (isBuffer(value)) {

return cloneBuffer(value, isDeep)

}

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)

// 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

}

// TypedArray

if (isTypedArray(value)) {

return result

}

// Symbol & 原型链

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]

}

assignValue(result, key, baseClone(subValue, bitmask, customizer, key, value, stack))

})

// 返回结果

return result

}

详细功能实现

位掩码

上面简单介绍了位掩码,参数定义如下。// 木易杨

// 主线代码

const CLONE_DEEP_FLAG = 1        // 1 即 0001,深拷贝标志位

const CLONE_FLAT_FLAG = 2        // 2 即 0010,拷贝原型链标志位,

const CLONE_SYMBOLS_FLAG = 4    // 4 即 0100,拷贝 Symbols 标志位

位掩码用于处理同时存在多个布尔选项的情况,其中掩码中的每个选项的值都等于 2 的幂。相比直接使用变量来说,优点是可以节省内存(1/32)(来自MDN)// 木易杨

// 主线代码

// cloneDeep.js 添加标志位,1 | 4 即 0001 | 0100 即 0101 即 5

CLONE_DEEP_FLAG | CLONE_SYMBOLS_FLAG

// baseClone.js 取出标志位

let result // 初始化返回结果,后续代码需要,和位掩码无关

const isDeep = bitmask & CLONE_DEEP_FLAG     // 5 & 1 即 1 即 true

const isFlat = bitmask & CLONE_FLAT_FLAG    // 5 & 2 即 0 即 false

const isFull = bitmask & CLONE_SYMBOLS_FLAG // 5 & 4 即 4 即 true

常用的基本操作如下a | b:添加标志位 a 和 b

mask & a:取出标志位 a

mask & ~a:清除标志位 a

mask ^ a:取出与 a 的不同部分// 木易杨

var FLAG_A = 1; // 0001

var FLAG_B = 4; // 0100

// 添加标志位 a 和 b => a | b

var mask = FLAG_A | FLAG_B => 0101 => 5

// 取出标志位 a => mask & a

mask & FLAG_A => 0001 => 1

mask & FLAG_B => 0100 => 4

// 清除标记位 a => mask & ~a

mask & ~FLAG_A => 0100 => 4

// 取出与 a 的不同部分 => mask ^ a

mask ^ FLAG_A => 0100 => 4

mask ^ FLAG_B => 0001 => 1

FLAG_A ^ FLAG_B => 0101 => 5

定制 `clone` 函数// 木易杨

// 主线代码

if (customizer) {

result = object ? customizer(value, key, object, stack) : customizer(value)

}

if (result !== undefined) {

return result

}

上面代码比较清晰,存在定制 clone 函数时,如果存在 value 值的父对象,就传入 value、key、object、stack 这些值,不存在父对象直接传入 value 执行定制函数。函数返回值result 不为空则返回执行结果。

这部分是为了定制 clone 函数暴露出来的方法。

非对象// 木易杨

// 主线代码

//判断要拷贝的值是否是对象,非对象直接返回本来的值

if (!isObject(value)) {

return value;

}

// ../isObject.js

function isObject(value) {

const type = typeof value;

return value != null && (type == 'object' || type ='function');

}

这里的处理和我在【进阶3-3】的处理一样,有一点不同在于对象的判断中加入了 function,对于函数的拷贝详见下面函数部分。

数组 & 正则// 木易杨

// 主线代码

const isArr = Array.isArray(value)

const hasOwnProperty = Object.prototype.hasOwnProperty

if (isArr) {

// 数组

result = initCloneArray(value)

if (!isDeep) {

return copyArray(value, result)

}

} else {

... // 非数组,后面解析

}

// 初始化一个数组

function initCloneArray(array) {

const { length } = array

// 构造相同长度的新数组

const result = new array.constructor(length)

// 正则 `RegExp#exec` 返回的数组

if (length && typeof array[0] == 'string' && hasOwnProperty.call(array, 'index')) {

result.index = array.index

result.input = array.input

}

return result

}

// ... 未完待续,最后部分有数组遍历赋值

传入的对象是数组时,构造一个相同长度的数组 new array.constructor(length),这里相当于 new Array(length),因为 array.constructor === Array。// 木易杨

var a = [];

a.constructor === Array; // true

var a = new Array;

a.constructor === Array // true

如果存在正则 RegExp#exec 返回的数组,拷贝属性 index 和 input。判断逻辑是 1、数组长度大于 0,2、数组第一个元素是字符串类型,3、数组存在 index 属性。// 木易杨

if (length && typeof array[0] == 'string' && hasOwnProperty.call(array, 'index')) {

result.index = array.index

result.input = array.input

}

其中正则表达式 regexObj.exec(str) 匹配成功时,返回一个数组,并更新正则表达式对象的属性。返回的数组将完全匹配成功的文本作为第一项,将正则括号里匹配成功的作为数组填充到后面。匹配失败时返回 null。// 木易杨

var re = /quick\s(brown).+?(jumps)/ig;

var result = re.exec('The Quick Brown Fox Jumps Over The Lazy Dog');

console.log(result);

// [

//    0: "Quick Brown Fox Jumps"  // 匹配的全部字符串

//    1: "Brown"                  // 括号中的分组捕获

//    2: "Jumps"

//    groups: undefined

//    index: 4                    // 匹配到的字符位于原始字符串的基于0的索引值

//    input: "The Quick Brown Fox Jumps Over The Lazy Dog" // 原始字符串

//    length: 3

// ]

如果不是深拷贝,传入value 和 result,直接返回浅拷贝后的数组。这里的浅拷贝方式就是循环然后复制。// 木易杨

if (!isDeep) {

return copyArray(value, result)

}

// 浅拷贝数组

function copyArray(source, array) {

let index = -1

const length = source.length

array || (array = new Array(length))

while (++index

array[index] = source[index]

}

return array

}

对象 & 函数// 木易杨

// 主线代码

const isArr = Array.isArray(value)

const tag = getTag(value)

if (isArr) {

... // 数组情况,详见上面解析

} else {

// 函数

const isFunc = typeof value == 'function'

// 如果是 Buffer 对象,拷贝并返回

if (isBuffer(value)) {

return cloneBuffer(value, isDeep)

}

// Object 对象、类数组、或者是函数但没有父对象

if (tag == objectTag || tag == argsTag || (isFunc && !object)) {

// 拷贝原型链或者 value 是函数时,返回 {},不然初始化对象

result = (isFlat || isFunc) ? {} : initCloneObject(value)

if (!isDeep) {

return isFlat

? copySymbolsIn(value, copyObject(value, keysIn(value), result))

: copySymbols(value, Object.assign(result, value))

}

} else {

// 在 cloneableTags 中,只有 error 和 weakmap 返回 false

// 函数或者 error 或者 weakmap 时,

if (isFunc || !cloneableTags[tag]) {

// 存在父对象返回value,不然返回空对象 {}

return object ? value : {}

}

// 初始化非常规类型

result = initCloneByTag(value, tag, isDeep)

}

}

通过上面代码可以发现,函数、error 和 weakmap 时返回空对象 {},并不会真正拷贝函数。

value 类型是 Object 对象和类数组时,调用 initCloneObject 初始化对象,最终调用Object.create 生成新对象。// 木易杨

function initCloneObject(object) {

// 构造函数并且自己不在自己的原型链上

return (typeof object.constructor == 'function' && !isPrototype(object))

? Object.create(Object.getPrototypeOf(object))

: {}

}

// 本质上实现了一个instanceof,用来测试自己是否在自己的原型链上

function isPrototype(value) {

const Ctor = value && value.constructor

// 寻找对应原型

const proto = (typeof Ctor == 'function' && Ctor.prototype) || Object.prototype

return value === proto

}

其中 Object 的构造函数是一个函数对象。// 木易杨

var obj = new Object();

typeof obj.constructor;

// 'function'

var obj2 = {};

typeof obj2.constructor;

// 'function'

对于非常规类型对象,通过各自类型分别进行初始化。// 木易杨

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: // Map 类型

return new Ctor

case numberTag: // 数字和字符串类型

case stringTag:

return new Ctor(object)

case regexpTag: // 正则

return cloneRegExp(object)

case setTag: // Set 类型

return new Ctor

case symbolTag: // Symbol 类型

return cloneSymbol(object)

}

}

拷贝正则类型// 木易杨

// \w 用于匹配字母,数字或下划线字符,相当于[A-Za-z0-9_]

const reFlags = /\w*$/

function cloneRegExp(regexp) {

// 返回当前匹配的文本

const result = new regexp.constructor(regexp.source, reFlags.exec(regexp))

// 下一次匹配的起始索引

result.lastIndex = regexp.lastIndex

return result

}

初始化 Symbol 类型// 木易杨

const symbolValueOf = Symbol.prototype.valueOf

function cloneSymbol(symbol) {

return 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 的修改也会反应到栈中。

Map & Set

value 值是 Map 类型时,遍历 value 并递归其 subValue,遍历完成返回 result 结果。// 木易杨

// 主线代码

if (tag == mapTag) {

value.forEach((subValue, key) => {

result.set(key, baseClone(subValue, bitmask, customizer, key, value, stack))

})

return result

}

value 值是 Set 类型时,遍历 value 并递归其 subValue,遍历完成返回 result 结果。// 木易杨

// 主线代码

if (tag == setTag) {

value.forEach((subValue) => {

result.add(baseClone(subValue, bitmask, customizer, subValue, value, stack))

})

return result

}

上面的区别在于添加元素的 API 不同,即  Map.set 和 Set.add。

Symbol & 原型链

这里我们介绍下 Symbol 和 原型链属性的拷贝,通过标志位 isFull 和 isFlat 来控制是否拷贝。// 木易杨

// 主线代码

// 类型化数组对象

if (isTypedArray(value)) {

return result

}

const keysFunc = isFull // 拷贝 Symbol 标志位

? (isFlat           // 拷贝原型链属性标志位

? getAllKeysIn     // 包含自身和原型链上可枚举属性名以及 Symbol

: getAllKeys)    // 仅包含自身可枚举属性名以及 Symbol

: (isFlat

? keysIn         // 包含自身和原型链上可枚举属性名的数组

: keys)            // 仅包含自身可枚举属性名的数组

const props = isArr ? undefined : keysFunc(value)

arrayEach(props || value, (subValue, key) => {

if (props) {

key = subValue

subValue = value[key]

}

// 递归拷贝(易受调用堆栈限制)

assignValue(result, key, baseClone(subValue, bitmask, customizer, key, value, stack))

})

return result

我们先来看下怎么获取自身、原型链、Symbol 这几种属性名组成的数组 keys。// 木易杨

// 创建一个包含自身和原型链上可枚举属性名以及 Symbol 的数组

// 使用 for...in 遍历

function getAllKeysIn(object) {

const result = keysIn(object)

if (!Array.isArray(object)) {

result.push(...getSymbolsIn(object))

}

return result

}

// 创建一个仅包含自身可枚举属性名以及 Symbol 的数组

// 非 ArrayLike 数组使用 Object.keys

function getAllKeys(object) {

const result = keys(object)

if (!Array.isArray(object)) {

result.push(...getSymbols(object))

}

return result

}

上面通过 keysIn 和 keys 获取常规可枚举属性,通过 getSymbolsIn 和 getSymbols 获取Symbol 可枚举属性。// 木易杨

// 创建一个包含自身和原型链上可枚举属性名的数组

// 使用 for...in 遍历

function keysIn(object) {

const result = []

for (const key in object) {

result.push(key)

}

return result

}

// 创建一个仅包含自身可枚举属性名的数组

// 非 ArrayLike 数组使用 Object.keys

function keys(object) {

return isArrayLike(object)

? arrayLikeKeys(object)

: Object.keys(Object(object))

}

// 测试代码

function Foo() {

this.a = 1

this.b = 2

}

Foo.prototype.c = 3

keysIn(new Foo)

// ['a', 'b', 'c'] (迭代顺序无法保证)

keys(new Foo)

// ['a', 'b'] (迭代顺序无法保证)

常规属性遍历原型链用的是 for.. in,那么 Symbol 是如何遍历原型链的呢,这里通过循环以及使用 Object.getPrototypeOf 获取原型链上的 Symbol。// 木易杨

// 创建一个包含自身和原型链上可枚举 Symbol 的数组

// 通过循环和使用 Object.getPrototypeOf 获取原型链上的 Symbol

function getSymbolsIn (object) {

const result = []

while (object) { // 循环

result.push(...getSymbols(object))

object = Object.getPrototypeOf(Object(object))

}

return result

}

// 创建一个仅包含自身可枚举 Symbol 的数组

// 通过 Object.getOwnPropertySymbols 获取 Symbol 属性

const nativeGetSymbols = Object.getOwnPropertySymbols

const propertyIsEnumerable = Object.prototype.propertyIsEnumerable

function getSymbols (object) {

if (object == null) { // 判空

return []

}

object = Object(object)

return nativeGetSymbols(object)

.filter((symbol) => propertyIsEnumerable.call(object, symbol))

}

我们回到主线代码,获取到 keys 组成的 props 数组之后,遍历并递归。// 木易杨

// 主线代码

const props = isArr ? undefined : keysFunc(value)

arrayEach(props || value, (subValue, key) => {

// props 时替换 key 和 subValue,因为 props 里面的 subValue 只是 value 的 key

if (props) {

key = subValue

subValue = value[key]

}

// 递归拷贝(易受调用堆栈限制)

assignValue(result, key, baseClone(subValue, bitmask, customizer, key, value, stack))

})

// 返回结果,主线结束

return result

我们看下 arrayEach 的实现,主要实现了一个遍历,并在 iteratee 返回为 false 时退出。// 木易杨

// 迭代数组

// iteratee 是每次迭代调用的函数

function arrayEach(array, iteratee) {

let index = -1

const length = array.length

while (++index

if (iteratee(array[index], index, array) === false) {

break

}

}

return array

}

我们看下 assignValue 的实现,在值不相等情况下,将 value 分配给 object[key]。// 木易杨

const hasOwnProperty = Object.prototype.hasOwnProperty

// 如果现有值不相等,则将 value 分配给 object[key]。

function assignValue(object, key, value) {

const objValue = object[key]

// 不相等

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

}

}

// 比较两个值是否相等

// (value !== value && other !== other) 是为了判断 NaN

function eq(value, other) {

return value === other || (value !== value && other !== other)

}

let 连续复制_【进阶4-4期】Lodash是如何实现深拷贝的相关推荐

  1. let 连续复制_要在Word中使用“格式刷”对同一个格式进行多次复制时,应先用鼠标()。...

    要在Word中使用"格式刷"对同一个格式进行多次复制时,应先用鼠标(). 答:左键双击"格式刷"按钮 闽菜由福州,闽南和 三路不同风味的地方菜组合而成 答:闽西 ...

  2. 【精解】Exchange Server 2007群集连续复制

    [精解]Exchange Server 2007群集连续复制 David物流公司去年新购置了Exchange2007服务器,但在使用中发现,由于服务器偶尔出现故障,导致公司暂时不能收到客户邮件问题严重 ...

  3. (需求实战_进阶_07)SSM集成RabbitMQ 订阅模式 关键代码讲解、开发、测试

    接上一篇:(企业内部需求实战_进阶_06)SSM集成RabbitMQ 订阅模式 关键代码讲解.开发.测试 https://gblfy.blog.csdn.net/article/details/104 ...

  4. (需求实战_进阶_03)SSM集成RabbitMQ 路由模式关键代码讲解、开发、测试

    接上一篇:(企业内部需求实战_进阶_02)SSM集成RabbitMQ 关键代码讲解.开发.测试 https://gblfy.blog.csdn.net/article/details/10421403 ...

  5. (需求实战_进阶_02)SSM集成RabbitMQ 关键代码讲解、开发、测试

    接上一篇:(企业内部需求实战_进阶_01)SSM集成RabbitMQ 关键代码讲解.开发.测试 https://gblfy.blog.csdn.net/article/details/10419730 ...

  6. Exchange系列—群集连续复制配置

    群集连续复制配置 配置网络连接顺序 在所有群集服务器网络连接中配置网络连接顺序,右击"网上邻居",点击属性进入网络连接窗口,在菜单栏中的高级项中选中高级设置,在高级设置中调整连接顺 ...

  7. 电脑连续复制粘贴,随意复制,随意粘贴,需要打开剪贴板功能。

    电脑连续复制粘贴,随意粘贴,需要打开剪贴板功能. 以win10为例,连续复制粘贴步骤如下所示: 1.在电脑桌面点击windows图标. 2.点击齿轮状图标,进入设置. 3.在windows设置中,点击 ...

  8. matlab 积分进阶教程,最适合小白的matlab教程系列_进阶系列二之微积分

    微积分篇 更多文章参考 符号变量 极限问题 求导数 求导数----偏导数 积分--符号解 积分数值解 多重积分 级数求和 泰勒级数展开 常微分方程 更多文章参考 最适合小白的matlab教程系列_基础 ...

  9. 最适合小白的matlab教程系列_进阶系列二之微积分

    微积分篇 更多文章参考 符号变量 极限问题 求导数 求导数----偏导数 积分--符号解 积分数值解 多重积分 级数求和 泰勒级数展开 常微分方程 更多文章参考 最适合小白的matlab教程系列_基础 ...

最新文章

  1. maven 强制更新_Android App内部更新Library的方法
  2. 初涉程序员之路的感悟
  3. Unity C# Job System介绍(二) 安全性系统和NativeContainer
  4. 很棒的远程执行工具psexec的用法
  5. 在flex4中使用asdoc生成api文档
  6. AS3和Flex常用知识100条
  7. IIS DNS服务器搭建
  8. 华为手机助手 linux,HiSuite-华为手机助手上线!从此再也不担心手机数据导不进Mac电脑啦!...
  9. 影响科学圈的那些计算机代码
  10. 国科大 - 模式识别与机器学习(黄庆明等)- 期末复习 - 试卷
  11. Matlab imfilter函数
  12. 计算机网络 --- 网络编程
  13. java去除空格的函数_JAVA中去掉空格--trim函数
  14. vscode 左侧文件夹颜色含义
  15. 大数据产品价值主张_大数据对商业模式创新的影响
  16. 360企业安全校招内推(可免笔试)
  17. nexus3私服搭建
  18. 哈佛经理人自我激励自我超越艺术
  19. Java小白入门200例65之Java sort()数组排序
  20. 树莓派原生系统安装ROS(含网络代理,ROS编译安装以及ROS小车制作过程)

热门文章

  1. VS中的工程文件组织
  2. Word 2003 标题样式设置
  3. 黑帽SEO技巧揭秘:PBN是什么?还可以用来建设外链吗?
  4. 实现 OT 算法的开源项目
  5. 兄弟Brother DCP-T428W 驱动
  6. PhotoRobot之Sohu相册
  7. 现在很多网友崇拜科学家,反对宣扬戏子,这是什么心态?
  8. JavaScript工作面试常用知识点总结大全
  9. 【TIANCHI】天池大数据竞赛(学习赛)--- 淘宝用户购物行为数据可视化分析
  10. 项目管理sod_SOD健康给世界快乐体育公益项目在京启动