Fork me on GitHub

从对象的遍历到浅拷贝的思考

我们已经来到了现代ECMAScript的时代,以前的正确的方法,现在看来似乎有点考虑不全。比如在ES6之前要实现对象的浅拷贝,比较简单,因为当时对象的属性只有String类型,ES6之后对象的属性有String和Symbol类型
由于文章上下文关系,本文将按照’属性描述符’->’对象的属性遍历方法介绍’->’现代ECMAScript对象的浅拷贝’进行介绍,现代ECMAScript对象的深拷贝比较复杂,有时间在分析。

属性描述符

对象里目前存在的属性描述符有两种主要形式:数据描述符和存取描述符。数据描述符是一个具有值的属性,该值可能是可写的,也可能不是可写的。存取描述符是由getter-setter函数对描述的属性。描述符必须是这两种形式之一,不能同时是两者。

数据描述符

configurable

当且仅当该属性的 configurable 为 true 时,该属性描述符才能够被改变,同时该属性也能从对应的对象上被删除。默认为 false。

enumerable

当且仅当该属性的enumerable为true时,该属性才能够出现在对象的枚举属性中。默认为 false。
数据描述符同时具有以下可选键值:

value

该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。默认为 undefined。

writable

当且仅当该属性的writable为true时,value才能被赋值运算符改变。默认为 false。

存取描述符

get

一个给属性提供 getter 的方法,如果没有 getter 则为 undefined。该方法返回值被用作属性值。默认为 undefined。

set

一个给属性提供 setter 的方法,如果没有 setter 则为 undefined。该方法将接受唯一参数,并将该参数的新值分配给该属性。默认为 undefined。

属性描述符的读写操作

我们可以通过Object.getOwnPropertyDescriptor(o,name)、Object.getOwnPropertyDescriptors(obj)来查看属性描述符,
通过Object.defineProperty(o,name,desc)、Object.defineProperties(o,descriptors)、Object.create(proto,descriptors)等方法来更改属性描述符。

对象的属性遍历方法介绍

上面,我们了解了属性描述符,其中枚举这个属性描述符,在不同方法对对象属性的遍历过程产生的作用差异很大,下面我开始介绍如何遍历对象的属性。
本小结的测试代码,我就不贴出来了,都很简单,我这里直接给出结论,感兴趣的朋友可以自己尝试。

for…in

定义:for…in语句以任意顺序遍历一个对象的可枚举属性。对于每个不同的属性,语句都会被执行。
语法:

1
2
3
for (item in object) {...}
// itme--在每次迭代时,将不同的属性名分配给变量。
// object--被迭代枚举其属性的对象。

PS:这里我们需要记住该方法遍历属性的特点:
1.可以遍历对象自身和原型链上可枚举的属性
2.任意顺序,说明遍历的属性先后顺序不定(不同运行环境顺序不同)。不建议对数组使用for…in来遍历主要原因就是这个,另一方面为性能考虑for…in还会遍历到原型链上的可枚举属性。

Object.keys()

定义:Object.keys() 方法会返回一个由该对象的自身的可枚举属性组成的数组。

语法:

1
2
Object.keys(obj)
// obj--要返回其枚举自身属性的对象。

PS:这里我们需要记住该方法遍历属性的特点:
1.属性是自身的
2.属性是可枚举的
3.任意顺序,枚举属性的顺序和for…in/Object.getOwnPropertyNames(obj)一致
4.返回一个所有元素为字符串(不包括Symbol)的数组

Object.getOwnPropertyNames()

定义:Object.getOwnPropertyNames()方法返回一个由指定对象的所有自身属性的属性名(包括不可枚举属性但不包括Symbol值作为名称的属性)组成的数组。
语法:

1
2
Object.getOwnPropertyNames(obj)
// obj--一个对象,其自身的可枚举和不可枚举属性的名称被返回。

PS:这里我们需要记住该方法遍历属性的特点:
1.属性是自身的
2.属性了枚举和不可枚举都可以遍历
3.任意顺序,枚举属性的顺序和for…in/Object.keys(obj)一致

Object.getOwnPropertySymbols()

定义:Object.getOwnPropertySymbols() 方法返回一个给定对象自身的所有 Symbol 属性的数组。
语法:

1
2
Object.getOwnPropertySymbols(obj)
// obj--要返回 Symbol 属性的对象。

PS:
1.属性是自身的
2.属性为Symbol类型

Reflect.ownKeys()

定义:Reflect.ownKeys() 返回一个由目标对象自身的属性键组成的数组。
语法:

1
2
Reflect.ownKeys(target)
// target--获取自身属性键的目标对象。

PS:
1.属性是自身的
2.属性可以是字符串或Symbol
3.属性是可枚举或不可枚举
这里看,似乎能遍历出自身的所有属性,还差原型连上的属性。

总结下上面的方法

为了好归纳,我这里将对象的属性分为以下类别:1.自身可枚举的属性,2.自身不可枚举的属性,3.Symbol类型的属性,4.原型链上的可枚举属性,5.原型链上的不可枚举属性,6.原型链上的Symbol属性2018-03-16更新

方法 可遍历的属性类别
for…in 1,4
Object.keys() 1
Object.getOwnPropertyNames() 1,2
Object.getOwnPropertySymbols() 3
Reflect.ownKeys() 1,2,3

从上表分析,还没有一个方法能完美解决,我们只能组合使用了。
PS:除了for…in其余方法均返回数组。

现代ECMAScript对象的浅拷贝

本小节会我们会实现各种浅拷贝,并分析各自的劣势,最终我们将实现一种比较完美的方法(暂不考虑兼容性)。

现看一个以前实现的方法

1
2
3
4
5
6
7
8
9
10
11
12
function shallowCopy (obj) {
if (typeof obj !== 'object') {
return
}
var newObj = obj instanceof Array ? [] : {}
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = obj[key]
}
}
return newObj
}

这中方法对于ES6之前的确可行,毕竟我也用过这样的方法,有了Symbol之后这个就不再正确了。

JSON.parse(JSON.stringify())

该方式在遇到不安全的JSON值会自动将其忽略,在数组中则会返回null(以保证单元位置不变)。
不安全的 JSON 值: undefined 、 function 、 symbol (ES6+)和包含循环引用(对象之间相互引用,形成一个无限循环)的 对象 都不符合 JSON 结构标准,支持 JSON 的语言无法处理它们

Object.assign 和 展开运算符(…)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let obj = {
name: 'yzf',
age: 100,
sex: 'male',
[Symbol()]: 'symbol1',
}
let obj2 = Object.assign({}, obj)
// obj2
{
name: 'yzf',
age: 100,
sex: 'male',
[Symbol()]: 'symbol1',
}

这个函数的定义:Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。
特点:自身的可枚举的包括Symbol类型的,不包括不可枚举的属性和原型链上的属性,不完美。

Object.create() + Object.getPrototypeOf() + Object.getOwnPropertyDescriptors()

首先需要介绍一下相关的方法,ES7的Object.getOwnPropertyDescriptors()。

Object.getOwnPropertyDescriptors()

定义:Object.getOwnPropertyDescriptors() 方法用来获取一个对象的所有自身属性的描述符。
语法:

1
2
Object.getOwnPropertyDescriptors(obj)
// 需要获取自身属性的对象。

PS:
1.属性是自身的
2.属性可以是字符串或Symbol
3.属性是可枚举或不可枚举
4.包括了集体属性的描述符(value)
到这里似乎我们已经找到了,比较完美的解决方案了,我们来组合一下这3个方法。

1
2
3
4
5
6
// obj为需要浅拷贝的对象
let obj1 = Object.create(
Object.getPrototypeOf(obj),
Object.getOwnPropertyDescriptors(obj)
)
// obj1就是我们浅拷贝的得到的对象。

PS:obj1对象通过Object.create()方法指定了自身的原型链(从原型链继承了相关属性),然后在通过Object.getOwnPropertyDescriptors()方法把自身的(包括可枚举的、不可枚举的、Symbol类型的)全部添加obj1上,这样是实现了我们的真正意义上的浅拷贝。

扩展

数组的浅拷贝ary.slice()、 ary.concat()、[…ary]、JSON.parse(JSON.stringify(ary))
期望加入一个技术氛围nice的团队-成都

参考文档:
Object.create
Object.assign
Object.getOwnPropertySymbols
ES6时代,你真的会克隆对象吗?

-------------本文结束感谢您的阅读,如果本文对你有帮助就记得给个star-------------
Donate comment here