😄本文首发于: lodash-source-learning/
工具库 lodash 在开发过程当中为咱们封装了丰富便捷的js函数,实现一些经常使用的功能,在使用过程当中就会对lodash的内部实现原理感到好奇。javascript
本次文章的主要内容分析阅读了lodash中深拷贝 _.cloneDeep()
的实现。html
浅拷贝:对于引用类型的数据来讲,赋值运算只是更改了引用的指针,可是指针指向的地址仍是同一个,因此对应的变更会影响双方。java
深拷贝:递归拷贝一个对象中的字对象,完成后两个对象不互相影响。node
包括但不限于:git
const CLONE_DEEP_FLAG = 1 const CLONE_SYMBOLS_FLAG = 4 function deepClone(value) { return baseClone(value, CLONE_DEEP_FLAG | CLONE_SYMBOLS_FLAG) }
cloneDeep
的主体函数baseClone
:github
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 }
以上入口代码看起来很简洁:定义两个位掩码常量,经过位运算控制参数类型,达到控制参数权限的基本实现:api
1 | 4 & 1 => 1 1 | 4 & 2 => 0 1 | 4 & 4 => 4
由上面的位元算可得知,在当前深拷贝模式下,isDeep
和 isFull
为true
,这两个变量在下面的代码中起到很大的判断做用。数组
关于javascript中位运算能够参考MDN:Bitwise_Operators。缓存
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) }
以上实现经过调用Object
的原型toString()
方法,区别不一样value
对应的具体类型:数据结构
var toString = Object.prototype.toString; toString.call(new Date); // [object Date] toString.call(new String); // [object String] toString.call(Math); // [object Math] //JavaScript版本1.8.5 及以上 toString.call(undefined); // [object Undefined] toString.call(null); // [object Null] toString.call(argument); // [object Arguments]
if (isArr) { // 数组深拷贝的初始化,返回了一个新数组的雏形 result = initCloneArray(value) }
function initCloneArray(array) { const { length } = array const result = new array.constructor(length) if (length && typeof array[0] === 'string' && hasOwnProperty.call(array, 'index')) { result.index = array.index result.input = array.input } return result } export default initCloneArray
看到这里会有疑问,为何数组类型的拷贝,须要判断typeof array[0] === 'string' && hasOwnProperty.call(array, 'index')
?index
和input
是什么状况?
熟悉js正则匹配的会知道,这里考虑了一种特殊的数组状况,那就是regexObj.exec(str)
,用来处理匹配正则时,执行exec()
的返回结果状况,若是匹配成功,exec()
方法返回一个数组(包含额外的属性 index
和 input
)
const matches = /(hello \S+)/.exec('hello world, javascript'); console.log(matches); 输出=> [ 0: "hello world," 1: "hello world," index: 0 input: "hello world, javascript" groups: undefined length: 2 ]
if (isBuffer(value)) { return cloneBuffer(value, isDeep) }
const Buffer = moduleExports ? root.Buffer : undefined, 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 }
以上对buffer对象相关的一些引用作处理。Buffer.allocUnsafe() 在node中返回指定大小的新未初始化Buffer实例。
具体能够参考:Buffer.allocUnsafe。
五、Object的拷贝
Object的拷贝开始,会使用Object.create()
构造出一个空对象,用以实现原对象的原型继承。
// 用来检测value是否为原型对象 function isPrototype(value) { const Ctor = value && value.constructor const proto = (typeof Ctor === 'function' && Ctor.prototype) || objectProto return value === proto } function initCloneObject(object) { return (typeof object.constructor === 'function' && !isPrototype(object)) ? Object.create(Object.getPrototypeOf(object)) : {} }
stack || (stack = new Stack) const stacked = stack.get(value) if (stacked) { return stacked } // 这里的result是上面一系列代码生成的初始化对象,能够暂时把它理解为一个包含原型继承关系的空对象 stack.set(value, result)
上面代码创建了Stack
,这是个数据管理接口,将子对象的值做为key-value
一对一的形式缓存起来,其内部详细的缓存行为大概细分为HashCache
、MapCache
和ListCache
,为何使用三种对象缓存策略?
HashCache
本质上是用对象的存储方式,但是会有个限制,js
中的对象存储,本质上是键值对的集合(Hash 结构),只能限制使用字符串/Symbol
看成键,这给它的使用带来了很大的限制。而Map
提供了一种更完善的 Hash
结构实现,它的key
能够是各类类型,因此在key
为Object/Array
等类型的场景下,lodash内部使用了MapCache
。
class Stack{ ... const LARGE_ARRAY_SIZE = 200 // Stack的set方法 set(key, value) { let data = this.__data__ if (data instanceof ListCache) { const pairs = data.__data__ if (pairs.length < LARGE_ARRAY_SIZE - 1) { pairs.push([key, value]) this.size = ++data.size return this } data = this.__data__ = new MapCache(pairs) } data.set(key, value) this.size = data.size return this } ... }
由Stack
的入口逻辑能够看到,当缓存内部__data__
的长度超出LARGE_ARRAY_SIZE
限额时,构造了MapCache
的实例,并采用了MapCache
的内部set
方法,不然使用ListCache
。
ListCache
实际上是一个二维数组类型的数据结构
class ListCache { ... // ListCache中set方法,实现了二维数组式存储 set(key, value) { const data = this.__data__ const index = assocIndexOf(data, key) if (index < 0) { ++this.size data.push([key, value]) } else { data[index][1] = value } return this } ... }
下面是MapCache
的存储主要实现:
// 初始化数据结构 this.__data__ = { 'hash': new Hash, 'map': new Map, 'string': new Hash } set(key, value) { const data = getMapData(this, key) const size = data.size data.set(key, value) this.size += data.size == size ? 0 : 1 return this } // 根据key的类型来判断该数据的存储方式,Hash或者Map function getMapData({ __data__ }, key) { const data = __data__ return isKeyable(key) ? data[typeof key === 'string' ? 'string' : 'hash'] : data.map } // 检查 value 是否适合用做惟一对象键 function isKeyable(value) { const type = typeof value return (type === 'string' || type === 'number' || type === 'symbol' || type === 'boolean') ? (value !== '__proto__') : (value === null) }
有下面的代码能够看出,Hash
实际上是用对象形式作缓存
const HASH_UNDEFINED = '__lodash_hash_undefined__' this.__data__ = Object.create(null) set(key, value) { const data = this.__data__ this.size += this.has(key) ? 0 : 1 data[key] = value === undefined ? HASH_UNDEFINED : value return this }
const loopObject = { a: 1 } loopObject.b = loopObject
🌰中,loopObject
中的b
就是一个循环引用的属性。
因为这个特殊状况的存在,在使用JSON.parse(JSON.stringify(loopObject))
时会出现内存溢出的问题。
使用缓存的另外一个好处是,可以处理对象中循环引用的状况。在遍历到循环引用对象时,缓存策略会从ceche
中利用对应的key
找出对应的value
,若是对应的引用已经拷贝了,就不须要在再次执行拷贝了,避免了溢出的问题。
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 } // 其余的可迭代对象,好比Array/Object arrayEach(props || value, (subValue, key) => { if (props) { key = subValue subValue = value[key] } // 递归进行数据的克隆 assignValue(result, key, baseClone(subValue, bitmask, customizer, key, value, stack)) })
字对象的递归拷贝主要递归使用了baseClone()
,并对不一样类型的对象做区分。
以上是对lodash
的深拷贝作了一个大概流程的分析,并无具体到每个函数实现,特别是Stack
中几种缓存结构的深刻解析,之后会持续更新HashCache、MapCache和ListCache相关内容。
🦉🦉🦉
---------------NO WAN---------------