// 木易杨
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 类型是 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 的数组
// 使用 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
}
引言
在上一篇文章中介绍了如何实现一个深拷贝,分别说明了对象、数组、循环引用、引用丢失、
Symbol
和递归爆栈等情况下的深拷贝实践,今天我们来看看Lodash
如何实现上述之外的函数、正则、Date、Buffer、Map、Set、原型链等情况下的深拷贝实践。本篇文章源码基于Lodash
4.17.11 版本。更多内容请查看 GitHub
整体流程
入口
入口文件是
cloneDeep.js
,直接调用核心文件baseClone.js
的方法。第一个参数是需要拷贝的对象,第二个是位掩码(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
函数baseClone 完整代码
这部分就是核心代码了,各功能分割如下,详细功能实现部分将对各个功能详细解读。
详细功能实现
位掩码
上面简单介绍了位掩码,参数定义如下。
位掩码用于处理同时存在多个布尔选项的情况,其中掩码中的每个选项的值都等于 2 的幂。相比直接使用变量来说,优点是可以节省内存(1/32)(来自MDN)
常用的基本操作如下
a | b
:添加标志位 a 和 bmask & a
:取出标志位 amask & ~a
:清除标志位 amask ^ a
:取出与 a 的不同部分定制
clone
函数上面代码比较清晰,存在定制
clone
函数时,如果存在 value 值的父对象,就传入value、key、object、stack
这些值,不存在父对象直接传入value
执行定制函数。函数返回值result
不为空则返回执行结果。这部分是为了定制
clone
函数暴露出来的方法。非对象
这里的处理和我在【进阶3-3】的处理一样,有一点不同在于对象的判断中加入了
function
,对于函数的拷贝详见下面函数部分。数组 & 正则
传入的对象是数组时,构造一个相同长度的数组
new array.constructor(length)
,这里相当于new Array(length)
,因为array.constructor === Array
。如果存在正则
RegExp#exec
返回的数组,拷贝属性index
和input
。判断逻辑是 1、数组长度大于 0,2、数组第一个元素是字符串类型,3、数组存在index
属性。其中正则表达式
regexObj.exec(str)
匹配成功时,返回一个数组,并更新正则表达式对象的属性。返回的数组将完全匹配成功的文本作为第一项,将正则括号里匹配成功的作为数组填充到后面。匹配失败时返回null
。如果不是深拷贝,传入
value
和result
,直接返回浅拷贝后的数组。这里的浅拷贝方式就是循环然后复制。对象 & 函数
通过上面代码可以发现,函数、
error
和weakmap
时返回空对象 {},并不会真正拷贝函数。value
类型是Object
对象和类数组时,调用initCloneObject
初始化对象,最终调用Object.create
生成新对象。其中
Object
的构造函数是一个函数对象。对于非常规类型对象,通过各自类型分别进行初始化。
拷贝正则类型
初始化
Symbol
类型循环引用
构造了一个栈用来解决循环引用的问题。
如果当前需要拷贝的值已存在于栈中,说明有环,直接返回即可。栈中没有该值时保存到栈中,传入
value
和result
。这里的result
是一个对象引用,后续对result
的修改也会反应到栈中。Map & Set
value
值是Map
类型时,遍历value
并递归其subValue
,遍历完成返回result
结果。value
值是Set
类型时,遍历value
并递归其subValue
,遍历完成返回result
结果。上面的区别在于添加元素的 API 不同,即
Map.set
和Set.add
。Symbol & 原型链
这里我们介绍下
Symbol
和 原型链属性的拷贝,通过标志位isFull
和isFlat
来控制是否拷贝。我们先来看下怎么获取自身、原型链、Symbol 这几种属性名组成的数组
keys
。上面通过
keysIn
和keys
获取常规可枚举属性,通过getSymbolsIn
和getSymbols
获取Symbol
可枚举属性。常规属性遍历原型链用的是
for.. in
,那么Symbol
是如何遍历原型链的呢,这里通过循环以及使用Object.getPrototypeOf
获取原型链上的Symbol
。我们回到主线代码,获取到
keys
组成的props
数组之后,遍历并递归。我们看下
arrayEach
的实现,主要实现了一个遍历,并在iteratee
返回为 false 时退出。我们看下
assignValue
的实现,在值不相等情况下,将 value 分配给object[key]
。参考
进阶系列目录
交流
进阶系列文章汇总如下,内有优质前端资料,觉得不错点个star。
我是木易杨,网易高级前端工程师,跟着我每周重点攻克一个前端面试重难点。接下来让我带你走进高级前端的世界,在进阶的路上,共勉!