属性描述符

本文最后更新于:41 分钟前

属性描述符

ES5开始,所有的属性都具备了属性描述符

defineProperty

defineProperty添加一个新属性或者修改一个已有属性(如果他的configurable=true)并对特性进行设置

1
2
3
4
5
6
7
8
9
10
var myObj={}

Object.defineProperty(myObj,'a',{
configurable: true, //可配置
enumerable: true, //可枚举
value: 2,
writable: true //可写
})

myObj.a //2

getOwnPropertyDescriptor

getOwnPropertyDescriptor可以得到自己的属性的描述符

1
2
3
4
5
6
7
8
9
var myObj={a:2}

Object.getOwnPropertyDescriptor(myObj,'a')
// {
// configurable: true //可配置
// enumerable: true //可枚举
// value: 2
// writable: true //可写
// }

writable

writable决定是否可以修改属性的值

1
2
3
4
5
6
7
8
9
10
11
var myObj={}

Object.defineProperty(myObj,'a',{
configurable: true, //可配置
enumerable: true, //可枚举
value: 2,
writable: false //不可写
})

myObj.a = 3
myObj.a //2

writable: false设置了false不可修改,所以修改失败了,如果在严格模式下,这种方式会出错

1
2
3
4
5
6
7
8
9
10
11
12
'use strict'
var myObj={}

Object.defineProperty(myObj,'a',{
configurable: true, //可配置
enumerable: true, //可枚举
value: 2,
writable: false //不可写
})

myObj.a = 3
myObj.a //Uncaught TypeError: Cannot assign to read only property 'a' of object '#<Object>'

configurable

configurable只要是属性是可以配置的,就可以使用defineProperty方法来修改属性描述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var myObj={
a:2
}
myObj.a = 3
myObj.a //3

Object.defineProperty(myObj,'a',{
configurable: false, //不可配置
enumerable: true, //可枚举
value: 4,
writable: true //可写
})
myObj.a //4
myObj.a = 5
myObj.a //5

Object.defineProperty(myObj,'a',{
configurable: true,
enumerable: true, //可枚举
value: 4,
writable: true //可写
})
//TypeError: Cannot redefine property: aat Function.defineProperty

configurable:false是不可逆的,单项操作,无法撤销

有个小例外,即便是configurable:false,我们还是可以把writable的状态由true改为false,但是无法从false改为true

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var myObj={
a:2
}
myObj.a = 3

delete myObj.a
myObj.a //undefined

Object.defineProperty(myObj,'a',{
configurable: false, //不可配置
enumerable: true, //可枚举
value: 4,
writable: true //可写
})

myObj.a //4

delete myObj.a
myObj.a //4

如你所见,最后一个delete语句(静默)失败了,因为属性是不可配置的。
在本例中,delete只用来直接删除对象的(可删除)属性。如果对象的某个属性是某个对象/函数的最后一个引用者,对这个属性执行delete操作之后,这个未引用的对象/函数就可以被垃圾回收。但是,不要把delete看作一个释放内存的工具(就像C/C++中那样),它就是一个删除对象属性的操作,仅此而已。

enumerable

这个描述符控制的是属性是否会出现在对象的属性枚举中,比如说for..in循环。
如果把enumerable设置成false,这个属性就不会出现在枚举中,虽然仍然可以正常访问它。
但是如果你不希望某些特殊属性出现在枚举中,那就把它设置成enumerable:false

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var myObj={}

Object.defineProperty(myObj,'a',{
enumerable: true, //可枚举
value: 4,
})
myObj.propertyIsEnumerable('a') //true

Object.defineProperty(myObj,'b',{
enumerable: false, //不可枚举
value: 1,
})

myObj.propertyIsEnumerable('b') //false

Object.keys(myObj) // ['a']

Object.getOwnPropertyNames(myObj) //['a', 'b']

propertyIsEnumerable()会检查给定的属性名是否直接存在于对象中(而不是在原型链上)并且满足enumerable:true
Object.keys()会返回一个数组,包含所有可枚举属性
Object.getOwnPropertyNames()会返回一个数组,包含所有属性,无论是否可枚举

常量对象

结合writable:falseconfigurable:false就可以创建一个真正的常量属性(不可修改、重定义或者删除):

1
2
3
4
5
6
7
8
9
10
11
12
var myObj={}

Object.defineProperty(myObj,'a',{
configurable: false, //可配置
value: 2,
writable: false //不可写
})

Object.defineProperty(myObj,'a',{
value: 4,
})
//TypeError: Cannot redefine property: a at Function.defineProperty (<anonymous>)

禁止扩展

如果你想禁止一个对象添加新属性并且保留已有属性,可以使用Object.preventExtensions(..):

存在性

对象属性访问返回值可能是undefined,但是这个值有可能是属性中存储的undefined,也可能是因为属性不存在所以返回undefined。那么如何区分这两种情况呢?
我们可以在不访问属性值的情况下判断对象中是否存在这个属性

1
2
3
4
5
6
7
8
9
10
11
12
var myObj={
a:2,
c:undefined
}

console.log('a' in myObj) //true
console.log('b' in myObj) //false
console.log('c' in myObj) //true

myObj.hasOwnProperty('a') //true
myObj.hasOwnProperty('b') //false
myObj.hasOwnProperty('c') //true

in操作符会检查属性是否在对象及其[[Prototype]]原型链中。
相比之下,hasOwnProperty(...)只会检查属性是否在对象中,不会检查[[Prototype]]链。

看起来操作符可以检查容器内是否有某个值,但是它实际上检查的是某个属性名是否存在。
对于数组来说这个区别非常重要,4 in [2,4,6] 的结果并不是你期待的true,因为[2,4,6]这个数组中包含的属性名是0、1、2,没有4。

1
console.log(4 in [2,4,6]) //false

枚举

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var myObj={}

Object.defineProperty(myObj,'a',{
enumerable: true, //让a可枚举
value: 4,
})

Object.defineProperty(myObj,'b',{
enumerable: false, //让b不可枚举
value: 1,
})

myObj.propertyIsEnumerable('b') //false

myObj.b //1
console.log('b' in myObj) //true
myObj.hasOwnProperty('b') //true

for(let k in myObj){
console.log(k,myObj[k])
}
// a 4

可以看到,myObj.b确实存在并且有访问值,但是却不会出现在for ..in循环中(尽管可以通过in操作符来判断是否存在)。原因是“可枚举”就相当于“可以出现在对象属性的遍历中”。

在数组上应用for..in循环有时会产生出人意料的结果,因为这种枚举不仅会包含所有数值索引,还会包含所有可枚举属性。最好只在对象上应用for ..in循环,如果要遍历数组就使用传统的for循环来遍历数值索引。

还可以通过另一种方式来区分是否可枚举

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var myObj={}

Object.defineProperty(myObj,'a',{
enumerable: true, //让a可枚举
value: 4,
})

Object.defineProperty(myObj,'b',{
enumerable: false, //让b不可枚举
value: 1,
})

myObj.propertyIsEnumerable('a') //true
myObj.propertyIsEnumerable('b') //false

Object.keys(myObj) // ['a']
Object.getOwnPropertyNames(myObj) //['a', 'b']


propertyISEnunerable(..)会检查给定的属性名是否直接存在于对象中(而不是在原型链上)并且满足enunerable:true
Object.keys(..)会返回一个数组,包含所有可枚举属性,object.getOwnPropertyNames( ..)会返回一个数组,包含所有属性,无论它们是否可枚举。
inhasOwnProperty(..)的区别在于是否查找[[Prototype]]链,
Object.keys(..)Object.getOwnPropertyNames(..)都只会查找对象直接包含的属性。

遍历

for ..in循环可以用来遍历对象的可枚举属性列表(包括[[Prototype]]链)。但是如何遍历属性的值呢?
对于数值索引的数组来说,可以使用标准的for循环来遍历值:

1
2
3
4
5
6
let myArray=['1','2','3']

for(let i=0;i<myArray.length;i++){
console.log(myArray[i])
}
// 1 2 3

这实际上并不是在遍历值,而是遍历下标来指向值,如myArray[i]
ES5中增加了一些数组的辅助迭代器,包括forEach(. .)every(..)some(..)。每种辅助迭代器都可以接受一个回调函数并把它应用到数组的每个元素上,唯一的区别就是它们对于回调函数返回值的处理方式不同。

forEach(..)会遍历数组中的所有值并忽略回调函数的返回值。
every(..)会一直运行直到回调函数返回false(或者“假”值),
some(..)会一直运行直到回调函数返回true(或者“真”值)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const arr = [ 1, 2, 3, 4, 5, 6 ]; 

console.log('some', arr.some( function( item, index, array ){
console.log( 'item=' + item + ',index='+index+',array='+array );
return item > 3;
}));

console.log('every', arr.every( function( item, index, array ){
console.log( 'item=' + item + ',index='+index+',array='+array );
return item > 3;
}));

// item=1,index=0,array=1,2,3,4,5,6
// item=2,index=1,array=1,2,3,4,5,6
// item=3,index=2,array=1,2,3,4,5,6
// item=4,index=3,array=1,2,3,4,5,6
// some true

// item=1,index=0,array=1,2,3,4,5,6
// every false

every(..)some(..)中特殊的返回值和普通for循环中的break 语句类似,它们会提前终止遍历。
使用for..in遍历对象是无法直接获取属性值的,因为它实际上遍历的是对象中的所有可枚举属性,你需要手动获取属性值。

那么如何直接遍历值而不是数组下标(或者对象属性)呢?
ES6增加了一种用来遍历数组的for..of循环语法(如果对象本身定义了迭代器的话也可以遍历对象):

1
2
3
4
5
let mayArray=[1,2,3]
for(let i of mayArray){
console.log(i)
}
// 1 2 3

for..of循环首先会向被访问对象请求一个迭代器对象,然后通过调用迭代器对象的next()方法来遍历所有返回值。
数组有内置的@@iterator,因此 for..of可以直接应用在数组上。我们使用内置的@@iterator来手动遍历数组,看看它是怎么工作的:

1
2
3
4
5
6
7
8
9
10
11
12
let mayArray=[1,2,3]
let it = mayArray[Symbol.iterator]()
it.next()
it.next()
it.next()
it.next()

// {value: 1, done: false}
// {value: 2, done: false}
// {value: 3, done: false}
// {value: undefined, done: true}

如你所见,调用迭代器的next()方法会返回形式为{ value: .. , done: .. }的值,value是当前的遍历值,done是一个布尔值,表示是否还有可以遍历的值。
注意,和值“3”一起返回的是done:false,乍一看好像很奇怪,你必须再调用一次next()才能得到done:true,从而确定完成遍历。