简介
lodash
版本 5.0
。
主要分析如何深克隆,代码为简化后的核心代码。基本类型,自定义clone,错误处理不考虑。
区分对象类型
通过 Object.prototype.toString.call
来获取详细类型 tag
。
数组会通过 Array.isArray
。
const toString = Object.prototype.toString
function getTag(value) {
if (value == null) {
return value === undefined ? '[object Undefined]' : '[object Null]'
}
return toString.call(value)
}
复制代码
克隆引用类型
在拷贝对象时,有2种方法:
- 构造函数
new targetObject.constructor()
- 优点:
- 构造函数生成对象,保证属性完整。
- 在拷贝内置对象,例如
Array
,Map
等,有无法创建的JS内部属性。只能通过构造函数生成。
- 在拷贝内置对象,例如
- 原型链正确。
- 构造函数生成对象,保证属性完整。
- 缺点:
- 执行一次函数,性能开销。
- 优点:
- 原型
Object.create(Object.getPrototypeOf(targetObject))
- 优点:
- 性能开销小。
- 原型链正确。
- 缺点:
- 无法拷贝JS内部属性。
- 优点:
克隆数组
数组的 length
是特殊的JS内部属性,必须用构造函数创建。
特殊情况:regex.exec()
的返回值数组。
注意:为什么不直接用 new Array(length)
?
- 因为有可能是一个继承了
Array
的class
创建的数组。- 例如:
Vue2
中响应式原理,所创建的数组。class MyArray extends Array {}
,代理一些数组方法。
- 例如:
function initCloneArray(array) {
const { length } = array
const result = new array.constructor(length)
// regex.exec()的返回值数组, 需要特殊处理index input
if (length && typeof array[0] === 'string' && hasOwnProperty.call(array, 'index')) {
result.index = array.index
result.input = array.input
}
return result
}
复制代码
克隆普通对象 和 Arguments
要注意:constructor
和 对象原型 都可被修改,要进行判断。
function initCloneObject(object) {
//constructor是函数,原型不是它本身,则原型创建
return (typeof object.constructor === 'function' && !isPrototype(object))
? Object.create(Object.getPrototypeOf(object))
: {} // 特殊情况,直接创建对象 原型链丢失
}
复制代码
还有一个特殊情况:chrome
已经支持 class
私有属性,方法。通过原型克隆,私有属性无法创建,调用时报错。
// 在支持原生私有属性的浏览器中调用。 babel转换后的无此问题。但是那不算原生私有属性。
class Test {
#a=1
print(){
console.log(this.#a)
}
}
let o1 = new Test()
o1.print()// 1
let o2 = initCloneObject(o1)
o2.print()//报错没有私有属性。 Uncaught TypeError: Cannot read private member #a from an object whose class did not declare it
复制代码
通过构造函数创建,则无此问题。
let o3 = new o1.constructor()
o3.print() // 1
复制代码
lodash
用原型创建,是考虑性能问题(减少构造函数执行)。
此特殊情况 lodash
还没处理,目前还是 BUG
状态。
克隆RegExp对象
取出正则,正则表达式标志。
function cloneRegExp(regexp) {
const result = new regexp.constructor(regexp.source, reFlags.exec(regexp))
// g标志时有用,开始下一个匹配的起始索引值
result.lastIndex = regexp.lastIndex
return result
}
复制代码
克隆Symbol对象
Symbol.prototype.valueOf
返回原始值。
在调用 Object
,对于基本类型的值,会构造其保证类型的对象。
const symbolValueOf = Symbol.prototype.valueOf
function cloneSymbol(symbol) {
return Object(symbolValueOf.call(symbol))
}
复制代码
其余引用类型
不一一分析了,看代码,原理都是构造函数。
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 setTag:
return new Ctor
}
}
function cloneArrayBuffer(arrayBuffer) {
const result = new arrayBuffer.constructor(arrayBuffer.byteLength)
new Uint8Array(result).set(new Uint8Array(arrayBuffer))
return result
}
function cloneDataView(dataView, isDeep) {
const buffer = cloneArrayBuffer(dataView.buffer)
return new dataView.constructor(buffer, dataView.byteOffset, dataView.byteLength)
}
function cloneTypedArray(typedArray, isDeep) {
const buffer = cloneArrayBuffer(typedArray.buffer)
return new typedArray.constructor(buffer, typedArray.byteOffset, typedArray.length)
}
复制代码
递归克隆
递归克隆,数组和对象
数组是遍历数组项。
对象是遍历属性。
获取属性
获取对象属性。**注意:**类数组对象,symbol
作为属性key
function getAllKeys(object) {
// 取keys
const result = keys(object)
if (!Array.isArray(object)) {
// 处理特殊性的symbol 属性
result.push(...getSymbols(object))
}
return result
}
//对象则用Object.keys
//类数组对象,eg: arguments 则for in 取key
//都返回keys数组
function keys(object) {
return isArrayLike(object)
? arrayLikeKeys(object)
: Object.keys(Object(object))
}
// 属性是否可枚举
const propertyIsEnumerable = Object.prototype.propertyIsEnumerable
// 对象自身的所有 Symbol 属性的数组。
const nativeGetSymbols = Object.getOwnPropertySymbols
// 获取symbol作为key的属性
function getSymbols(object) {
if (object == null) {
return []
}
// 包装基本类型,
object = Object(object)
return nativeGetSymbols(object).filter((symbol) => propertyIsEnumerable.call(object, symbol))
}
复制代码
遍历
// value是被克隆对象
// 数组遍历自身。 对象遍历属性数组。
const props = isArr ? undefined : getAllKeys(value)
//
arrayEach(props || value, (subValue, key) => {
// subValue是array[index],key是index
if (props) {
// 对于对象 key是array[index]
key = subValue
// value 是 value[lkey]
subValue = value[key]
}
// 将递归遍历的值 赋值到 key中。 处理特殊情况,eg: NAN也要赋值为NAN
// 基本等于 object[key] = baseClone(subValue, bitmask, customizer, key, value, stack)
assignValue(result, key, baseClone(subValue, bitmask, customizer, key, value, stack))
})
复制代码
递归克隆,Map和Set
同理遍历。但是用 set
/ add
来赋值。
if (tag == mapTag) {
value.forEach((subValue, key) => {
result.set(key, baseClone(subValue, bitmask, customizer, key, value, stack))
})
}
if (tag == setTag) {
value.forEach((subValue) => {
result.add(baseClone(subValue, bitmask, customizer, subValue, value, stack))
})
}
复制代码
处理循环引用
原理:缓存每个引用,然后调用克隆函数时去缓存判断是否已经克隆。有则将克隆的引用返回。无则继续克隆。
lodash
有2种模式来缓存引用。
缓存引用
怎么才能存储引用呢? Array
或 Map
。
数组缓存引用 ListCache
每个数组项为 数组(长度为2)[旧对象引用,克隆对象引用]
。
具体大概如右: [[旧对象引用,克隆对象引用], [旧对象引用,克隆对象引用], [旧对象引用,克隆对象引用],...]
然后判断缓存时,遍历数组即可。
这里实现一个最简单的demo
class ListCache {
__data__ = []
size = 0
get(key) {
const data = this.__data__
const index = data.findIndex(([cacheKey])=>cacheKey===key)
return index < 0 ? undefined : data[index][1]
}
has(key) {
const data = this.__data__
return data.findIndex(([cacheKey])=>cacheKey===key) > -1
}
set(key, value) {
const data = this.__data__
const index = data.findIndex(([cacheKey])=>cacheKey===key)
if (index < 0) {
++this.size
data.push([key, value])
} else {
data[index][1] = value
}
return this
}
}
复制代码
loadsh
中小于200的缓存,用 ListCatch
超过则用 MapCatch
MapCatch
使用原生 Map
对象。
克隆函数
loadsh
是不克隆函数的,将返回 {}
。
因为克隆函数没有实际意义,公用同一个函数也没问题。
而且涉及到 科里化,this,闭包等问题,无法克隆相等价值的函数。
但是面试会问,还是要克隆一个。
克隆函数
new Function('return ' + fn.toString())()
// 或者
eval(`(${fn.toString()})`)
复制代码
总结
- 区分对象tag
- 克隆对象通过,构造函数 或者 原型链
Array
或Map
解决循环引用- 递归
- 获取对象属性
- 克隆函数