You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
constcopy=input=>{// 其它不变for(letkeyininput){// ...}// 处理以 Symbol 值作为属性键的属性(本次新增)constsymbolArr=Object.getOwnPropertySymbols(input)if(symbolArr.length){for(leti=0,len=symbolArr.length;i<len;i++){if(input.propertyIsEnumerable(symbolArr[i])){constvalue=input[symbolArr[i]]output[symbolArr[i]]=copy(value)}}}// ...}
下面我们对 source 对象做拷贝操作:
constsource={}constsym1=Symbol('1')constsym2=Symbol('2')Object.defineProperties(source,{[sym1]: {value: 'This is symbol value.',enumerable: true},[sym2]: {value: 'This is a non-enumerable property.',enumerable: false}})
打印结果,也符合预期结果:
3.4 针对 Date 对象的处理
其实,处理 Date 对象,跟上面提到的包装对象的处理是差不多的。暂时先放到 isWrapperObject() 和 handleWrapperObject() 中处理。
constdeepCopy=source=>{// 其他不变...// 判断是否为包装对象(本次更新)constisWrapperObject=obj=>{consttheClass=getClass(obj)consttype=/^\[object (.*)\]$/.exec(theClass)[1]return['Boolean','Number','String','Symbol','BigInt','Date'].includes(type)}// 处理包装对象consthandleWrapperObject=obj=>{consttype=getClass(obj)switch(type){// 其他 case 不变// ...case'[object Date]':
returnnewDate(obj.valueOf())// new Date(+obj)default:
returnundefined}}// 其他不变...}
constre1=/foo*/gconststr='table football, foosball'letarrwhile((arr=re1.exec(str))!==null){console.log(`Found ${arr[0]}. Next starts at ${re1.lastIndex}.`)}// 以上语句会输出,以下结果:// "Found foo. Next starts at 9."// "Found foo. Next starts at 19."// 当我们修改 re1 的 lastIndex 属性时,输出以下结果:re1.lastIndex=9while((arr=re1.exec(str))!==null){console.log(`Found ${arr[0]}. Next starts at ${re1.lastIndex}.`)}// "Found foo. Next starts at 19."// 以上这些相信你们都都懂。
constre1=/foo*/gconststr='table football, foosball'letarr// 修改 lastIndex 属性re1.lastIndex=9// 基于 re1 拷贝一个正则表达式constre2=newRegExp(re1.source,re1.flags)console.log('re1:')while((arr=re1.exec(str))!==null){console.log(`Found ${arr[0]}. Next starts at ${re1.lastIndex}.`)}console.log('re2:')while((arr=re2.exec(str))!==null){console.log(`Found ${arr[0]}. Next starts at ${re2.lastIndex}.`)}// re1:// expected output: "Found foo. Next starts at 19."// re2:// expected output: "Found foo. Next starts at 9."// expected output: "Found foo. Next starts at 19."
/** Used to match `RegExp` flags from their coerced string values. */varreFlags=/\w*$/;/** * Creates a clone of `regexp`. * * @private * @param {Object} regexp The regexp to clone. * @returns {Object} Returns the cloned regexp. */functioncloneRegExp(regexp){varresult=newregexp.constructor(regexp.source,reFlags.exec(regexp));result.lastIndex=regexp.lastIndex;returnresult;}
A shallow clone won't do that as it's just _.assign({}, object) and a deep clone is loosely based on the structured cloning algorithm and doesn't attempt to clone inheritance or lack thereof.
Reflect.ownKeys:返回一个数组,包含自身所有的属性(包括 Symbol 属性,不可枚举属性以及可枚举属性)
由于我们仅拷贝可枚举的字符串属性和可枚举的 Symbol 属性,因此我们将 Reflect.ownKeys() 和 Object.prototype.propertyIsEnumerable() 结合使用即可。
所以,我们将以下这部分:
for(letkeyininput){if(input.hasOwnProperty(key)){constvalue=input[key]output[key]=copy(value)}}// 处理以 Symbol 值作为属性键的属性constsymbolArr=Object.getOwnPropertySymbols(input)if(symbolArr.length){for(leti=0,len=symbolArr.length;i<len;i++){if(input.propertyIsEnumerable(symbolArr[i])){constvalue=input[symbolArr[i]]output[symbolArr[i]]=copy(value)}}}
优化成:
// 仅遍历对象自身可枚举的属性(包括字符串属性和 Symbol 属性)Reflect.ownKeys(input).forEach(key=>{if(input.propertyIsEnumerable(key)){output[key]=copy(input[key])}})
一、JSON.stringify() 的缺陷
利用内置的 JSON 静态方法,可以实现简易的深拷贝:
它可以满足大部分应用场景,毕竟很少去拷贝函数之类的。
简单总结:
布尔值、数值、字符串对应的包装对象,在序列化过程会自动转换成其原始值。
undefined
、任意函数、Symbol
值,在序列化过程有两种不同的情况。null
。任意函数
、undefined
被单独转换时,会返回undefined
。所有以
Symbol
为属性键的属性都会被忽略,即便在第二个参数replacer
中指定了该属性。Date
调用了其内置的toJSON()
方法转换成字符串,因此会被当初字符串处理。NaN
和Infinity
的数值及null
都会当做null
。这些对象
Map
、Set
、WeakMap
、WeakSet
仅会序列化可枚举的属性。被转换值如果含有
toJSON()
方法,该方法定义什么值将被序列化。对包含循环引用的对象进行序列化,会抛出错误。
从命名来看,我认为它们只是方便我们操作符合 JSON 格式的 JavaScript 对象或符合 JSON 格式的字符串。
它只是恰好能满足一些简单的深拷贝需求而已。
二、边界条件
其实实现一个较为完整的深拷贝,要处理很多边界条件。比如:
目前,最完善的深拷贝方法应该是 Lodash 的
_.cloneDeep()
方法。实际项目中,如需处理。JSON.stringify()
无法解决的 Case,我会推荐使用它本文旨在学习,以上边界条件都会尽可能兼顾到。这样,无论日后实现特殊的深拷贝,还是面试,都可以从容应对。
三、实现
使用递归思路实现。先写一个简易版本:
以上简易版本还存在很多情况要特殊处理,接下来针对
JSON.stringify()
的缺陷,一点一点去完善它。3.1 针对布尔值、数值、字符串的包装对象的处理
由于 for...in 无法遍历不可枚举的属性。例如,包装对象的
[[PrimitiveValue]]
内部属性,因此需要我们特殊处理一下。以上结果,显然不是预期结果。包装对象的
[[PrimitiveValue]]
属性可通过valueOf()
方法获取。我们在控制台打印一下结果,可以看到是符合预期结果的。
3.2 针对函数的处理
直接返回就好了,一般不用处理。在实际应用场景需要拷贝函数太少了...
3.3 针对以 Symbol 值作为属性键的处理
由于以上
for...in
方法无法遍历Symbol
的属性键,因此:这里,我们需要用到两个方法:
Object.getOwnPropertySymbols()
它返回一个对象自身的所有 Symbol 属性的数组,包括不可枚举的属性。
Object.prototype.propertyIsEnumerable()
它返回一个布尔值,表示指定的属性是否可枚举。
下面我们对
source
对象做拷贝操作:打印结果,也符合预期结果:
3.4 针对 Date 对象的处理
其实,处理
Date
对象,跟上面提到的包装对象的处理是差不多的。暂时先放到isWrapperObject()
和handleWrapperObject()
中处理。3.5 针对 Map、Set 对象的处理
同样的,暂时先放到
isWrapperObject()
和handleWrapperObject()
中处理。利用 Map、Set 对象的 Iterator 特性和自身的方法,可以快速解决。
打印下结果:
3.6 针对循环引用的问题
以下是一个循环引用(circular reference)的对象:
上面提到
JSON.stringify()
无法处理循环引用的问题,我们在控制台打印一下:从结果可以看到,当对循环引用的对象进行序列化处理时,会抛出类型错误:
Uncaught TypeError: Converting circular structure to JSON
。接着,使用自行实现的
deepCopy()
方法,看下结果是什么:我们看到,在拷贝循环引用的
foo
对象时,发生栈溢出了。那我们去实现一下:
先看看打印结果,不会像之前一样溢出了。
需要注意的是,这里不使用 Map 而是 WeakMap 的原因:
首先,Map 的键属于强引用,而 WeakMap 的键则属于弱引用。且 WeakMap 的键必须是对象,WeakMap 的值则是任意的。
由于它们的键与值的引用关系,决定了 Map 不能确保其引用的对象不会被垃圾回收器回收的引用。假设我们使用的 Map,那么图中的
foo
对象和我们深拷贝内部的const map = new Map()
创建的map
对象一直都是强引用关系,那么在程序结束之前,foo
不会被回收,其占用的内存空间一直不会被释放。相比之下,原生的 WeakMap 持有的是每个键对象的“弱引用”,这意味着在没有其他引用存在时垃圾回收能正确进行。原生 WeakMap 的结构是特殊且有效的,其用于映射的 key 只有在其没有被回收时才是有效的。
基本上,如果你要往对象上添加数据,又不想干扰垃圾回收机制,就可以使用 WeakMap。
我们熟知的 Lodash 库的深拷贝方法,自实现了一个类似 WeakMap 特性的构造函数去处理循环引用的。(详看)
这里提供另一个思路,也是可以的。
请在实现深拷贝之后测试以下示例:
3.7 针对正则表达式的处理
正则表达式里面,有两个非常重要的属性:
返回当前正则表达式对象的模式文本的字符串。注意,这是 ES6 新增的属性。
返回当前正则表达式对象标志。
有了以上两个属性,我们就可以使用
new RegExp(pattern, flags)
构造函数去创建一个正则表达式了。但需要注意的是,正则表达式有一个
lastIndex
属性,该属性可读可写,其值为整型,用来指定下一次匹配的起始索引。在设置了global
或sticky
标志位的情况下(如/foo/g
、/foo/y
),JavaScriptRegExp
对象是有状态的。他们会将上次成功匹配后的位置记录在lastIndex
属性中。因此,上述拷贝正则表达式的方式是有缺陷的。看示例:
所以,你可以发现以下示例,打印结果是不一致的,原因就是使用
RegExp
构造函数去创建一个正则表达式时,lastIndex
会默认设为0
。因此:
打印结果也是符合预期的:
由于
RegExp.prototype.flags
是 ES6 新增属性,我们可以看下 ES5 是如何实现的(源自 Lodash):但还是那句话,都 2021 年了,兼容 ES5 的问题就放心交给 Babel 吧。
3.8 处理原型
主要是修改以下这一步骤:
主要利用
Object.create()
来创建output
对象,改成这样:来看下打印结果,可以看到
source
的原型对象已经拷贝过来了:再来看下
Object.create(null)
的情况,也是预期结果。我们可以看到 Lodash 的
_.cloneDeep(Object.create(null))
深拷贝方法并没有处理这种情况。当然了,要拷贝这种数据结构在实际应用场景,真的少之又少...四、优化
综上所述,完整但未优化的深拷贝方法如下:
接下来就是优化工作了...
4.1 优化一
我们上面使用到了
for...in
和Object.getOwnPropertySymbols()
方法去遍历对象的属性(包括字符串属性和 Symbol 属性),还涉及了可枚举属性和不可枚举属性。所以,我们将以下这部分:
优化成:
4.2 优化二
优化
getClass()
、isWrapperObject()
、handleWrapperObject()
、handleRegExp()
及其相关的类型判断方法。由于
handleWrapperObject()
原意是处理包装对象,但是随着后面要处理的特殊对象越来越多,为了减少文章篇幅,暂时都写在里面了,稍微有点乱。因此下面我们来整合一下,部分处理函数可能会修改函数名。
五、最后
其实,上面提到的一些边界 Case、或者其他一些特殊对象(如
ArrayBuffer
等),这里并没有处理,但我认为该完结了,因为这些在实际应用场景真的太少了。代码已丢到 GitHub 👉 toFrankie/Some-JavaScript-File。
还是那句话:
这篇文章主要面向学习、面试(手动狗头),或许也可以帮助你熟悉一些对象的特性。如有不足,欢迎指出,万分感谢 👋 ~
终于终于终于......要写完了,吐了三斤血...
最终版本如下:
六、References
The text was updated successfully, but these errors were encountered: