# 屬性描述對象
## 概述
JavaScript 提供了一個內部數據結構,用來描述對象的屬性,控制它的行為,比如該屬性是否可寫、可遍歷等等。這個內部數據結構稱為“屬性描述對象”(attributes object)。每個屬性都有自己對應的屬性描述對象,保存該屬性的一些元信息。
下面是屬性描述對象的一個例子。
```javascript
{
value: 123,
writable: false,
enumerable: true,
configurable: false,
get: undefined,
set: undefined
}
```
屬性描述對象提供6個元屬性。
(1)`value`
`value`是該屬性的屬性值,默認為`undefined`。
(2)`writable`
`writable`是一個布爾值,表示屬性值(value)是否可改變(即是否可寫),默認為`true`。
(3)`enumerable`
`enumerable`是一個布爾值,表示該屬性是否可遍歷,默認為`true`。如果設為`false`,會使得某些操作(比如`for...in`循環、`Object.keys()`)跳過該屬性。
(4)`configurable`
`configurable`是一個布爾值,表示可配置性,默認為`true`。如果設為`false`,將阻止某些操作改寫該屬性,比如無法刪除該屬性,也不得改變該屬性的屬性描述對象(`value`屬性除外)。也就是說,`configurable`屬性控制了屬性描述對象的可寫性。
(5)`get`
`get`是一個函數,表示該屬性的取值函數(getter),默認為`undefined`。
(6)`set`
`set`是一個函數,表示該屬性的存值函數(setter),默認為`undefined`。
## Object.getOwnPropertyDescriptor()
`Object.getOwnPropertyDescriptor()`方法可以獲取屬性描述對象。它的第一個參數是目標對象,第二個參數是一個字符串,對應目標對象的某個屬性名。
```javascript
var obj = { p: 'a' };
Object.getOwnPropertyDescriptor(obj, 'p')
// Object { value: "a",
// writable: true,
// enumerable: true,
// configurable: true
// }
```
上面代碼中,`Object.getOwnPropertyDescriptor()`方法獲取`obj.p`的屬性描述對象。
注意,`Object.getOwnPropertyDescriptor()`方法只能用于對象自身的屬性,不能用于繼承的屬性。
```javascript
var obj = { p: 'a' };
Object.getOwnPropertyDescriptor(obj, 'toString')
// undefined
```
上面代碼中,`toString`是`obj`對象繼承的屬性,`Object.getOwnPropertyDescriptor()`無法獲取。
## Object.getOwnPropertyNames()
`Object.getOwnPropertyNames`方法返回一個數組,成員是參數對象自身的全部屬性的屬性名,不管該屬性是否可遍歷。
```javascript
var obj = Object.defineProperties({}, {
p1: { value: 1, enumerable: true },
p2: { value: 2, enumerable: false }
});
Object.getOwnPropertyNames(obj)
// ["p1", "p2"]
```
上面代碼中,`obj.p1`是可遍歷的,`obj.p2`是不可遍歷的。`Object.getOwnPropertyNames`會將它們都返回。
這跟`Object.keys`的行為不同,`Object.keys`只返回對象自身的可遍歷屬性的全部屬性名。
```javascript
Object.keys([]) // []
Object.getOwnPropertyNames([]) // [ 'length' ]
Object.keys(Object.prototype) // []
Object.getOwnPropertyNames(Object.prototype)
// ['hasOwnProperty',
// 'valueOf',
// 'constructor',
// 'toLocaleString',
// 'isPrototypeOf',
// 'propertyIsEnumerable',
// 'toString']
```
上面代碼中,數組自身的`length`屬性是不可遍歷的,`Object.keys`不會返回該屬性。第二個例子的`Object.prototype`也是一個對象,所有實例對象都會繼承它,它自身的屬性都是不可遍歷的。
## Object.defineProperty(),Object.defineProperties()
`Object.defineProperty()`方法允許通過屬性描述對象,定義或修改一個屬性,然后返回修改后的對象,它的用法如下。
```javascript
Object.defineProperty(object, propertyName, attributesObject)
```
`Object.defineProperty`方法接受三個參數,依次如下。
- object:屬性所在的對象
- propertyName:字符串,表示屬性名
- attributesObject:屬性描述對象
舉例來說,定義`obj.p`可以寫成下面這樣。
```javascript
var obj = Object.defineProperty({}, 'p', {
value: 123,
writable: false,
enumerable: true,
configurable: false
});
obj.p // 123
obj.p = 246;
obj.p // 123
```
上面代碼中,`Object.defineProperty()`方法定義了`obj.p`屬性。由于屬性描述對象的`writable`屬性為`false`,所以`obj.p`屬性不可寫。注意,這里的`Object.defineProperty`方法的第一個參數是`{}`(一個新建的空對象),`p`屬性直接定義在這個空對象上面,然后返回這個對象,這是`Object.defineProperty()`的常見用法。
如果屬性已經存在,`Object.defineProperty()`方法相當于更新該屬性的屬性描述對象。
如果一次性定義或修改多個屬性,可以使用`Object.defineProperties()`方法。
```javascript
var obj = Object.defineProperties({}, {
p1: { value: 123, enumerable: true },
p2: { value: 'abc', enumerable: true },
p3: { get: function () { return this.p1 + this.p2 },
enumerable:true,
configurable:true
}
});
obj.p1 // 123
obj.p2 // "abc"
obj.p3 // "123abc"
```
上面代碼中,`Object.defineProperties()`同時定義了`obj`對象的三個屬性。其中,`p3`屬性定義了取值函數`get`,即每次讀取該屬性,都會調用這個取值函數。
注意,一旦定義了取值函數`get`(或存值函數`set`),就不能將`writable`屬性設為`true`,或者同時定義`value`屬性,否則會報錯。
```javascript
var obj = {};
Object.defineProperty(obj, 'p', {
value: 123,
get: function() { return 456; }
});
// TypeError: Invalid property.
// A property cannot both have accessors and be writable or have a value
Object.defineProperty(obj, 'p', {
writable: true,
get: function() { return 456; }
});
// TypeError: Invalid property descriptor.
// Cannot both specify accessors and a value or writable attribute
```
上面代碼中,同時定義了`get`屬性和`value`屬性,以及將`writable`屬性設為`true`,就會報錯。
`Object.defineProperty()`和`Object.defineProperties()`參數里面的屬性描述對象,`writable`、`configurable`、`enumerable`這三個屬性的默認值都為`false`。
```javascript
var obj = {};
Object.defineProperty(obj, 'foo', {});
Object.getOwnPropertyDescriptor(obj, 'foo')
// {
// value: undefined,
// writable: false,
// enumerable: false,
// configurable: false
// }
```
上面代碼中,定義`obj.foo`時用了一個空的屬性描述對象,就可以看到各個元屬性的默認值。
## Object.prototype.propertyIsEnumerable()
實例對象的`propertyIsEnumerable()`方法返回一個布爾值,用來判斷某個屬性是否可遍歷。注意,這個方法只能用于判斷對象自身的屬性,對于繼承的屬性一律返回`false`。
```javascript
var obj = {};
obj.p = 123;
obj.propertyIsEnumerable('p') // true
obj.propertyIsEnumerable('toString') // false
```
上面代碼中,`obj.p`是可遍歷的,而`obj.toString`是繼承的屬性。
## 元屬性
屬性描述對象的各個屬性稱為“元屬性”,因為它們可以看作是控制屬性的屬性。
### value
`value`屬性是目標屬性的值。
```javascript
var obj = {};
obj.p = 123;
Object.getOwnPropertyDescriptor(obj, 'p').value
// 123
Object.defineProperty(obj, 'p', { value: 246 });
obj.p // 246
```
上面代碼是通過`value`屬性,讀取或改寫`obj.p`的例子。
### writable
`writable`屬性是一個布爾值,決定了目標屬性的值(value)是否可以被改變。
```javascript
var obj = {};
Object.defineProperty(obj, 'a', {
value: 37,
writable: false
});
obj.a // 37
obj.a = 25;
obj.a // 37
```
上面代碼中,`obj.a`的`writable`屬性是`false`。然后,改變`obj.a`的值,不會有任何效果。
注意,正常模式下,對`writable`為`false`的屬性賦值不會報錯,只會默默失敗。但是,嚴格模式下會報錯,即使對`a`屬性重新賦予一個同樣的值。
```javascript
'use strict';
var obj = {};
Object.defineProperty(obj, 'a', {
value: 37,
writable: false
});
obj.a = 37;
// Uncaught TypeError: Cannot assign to read only property 'a' of object
```
上面代碼是嚴格模式,對`obj.a`任何賦值行為都會報錯。
如果原型對象的某個屬性的`writable`為`false`,那么子對象將無法自定義這個屬性。
```javascript
var proto = Object.defineProperty({}, 'foo', {
value: 'a',
writable: false
});
var obj = Object.create(proto);
obj.foo = 'b';
obj.foo // 'a'
```
上面代碼中,`proto`是原型對象,它的`foo`屬性不可寫。`obj`對象繼承`proto`,也不可以再自定義這個屬性了。如果是嚴格模式,這樣做還會拋出一個錯誤。
但是,有一個規避方法,就是通過覆蓋屬性描述對象,繞過這個限制。原因是這種情況下,原型鏈會被完全忽視。
```javascript
var proto = Object.defineProperty({}, 'foo', {
value: 'a',
writable: false
});
var obj = Object.create(proto);
Object.defineProperty(obj, 'foo', {
value: 'b'
});
obj.foo // "b"
```
### enumerable
`enumerable`(可遍歷性)返回一個布爾值,表示目標屬性是否可遍歷。
JavaScript 的早期版本,`for...in`循環是基于`in`運算符的。我們知道,`in`運算符不管某個屬性是對象自身的還是繼承的,都會返回`true`。
```javascript
var obj = {};
'toString' in obj // true
```
上面代碼中,`toString`不是`obj`對象自身的屬性,但是`in`運算符也返回`true`,這導致了`toString`屬性也會被`for...in`循環遍歷。
這顯然不太合理,后來就引入了“可遍歷性”這個概念。只有可遍歷的屬性,才會被`for...in`循環遍歷,同時還規定`toString`這一類實例對象繼承的原生屬性,都是不可遍歷的,這樣就保證了`for...in`循環的可用性。
具體來說,如果一個屬性的`enumerable`為`false`,下面三個操作不會取到該屬性。
- `for..in`循環
- `Object.keys`方法
- `JSON.stringify`方法
因此,`enumerable`可以用來設置“秘密”屬性。
```javascript
var obj = {};
Object.defineProperty(obj, 'x', {
value: 123,
enumerable: false
});
obj.x // 123
for (var key in obj) {
console.log(key);
}
// undefined
Object.keys(obj) // []
JSON.stringify(obj) // "{}"
```
上面代碼中,`obj.x`屬性的`enumerable`為`false`,所以一般的遍歷操作都無法獲取該屬性,使得它有點像“秘密”屬性,但不是真正的私有屬性,還是可以直接獲取它的值。
注意,`for...in`循環包括繼承的屬性,`Object.keys`方法不包括繼承的屬性。如果需要獲取對象自身的所有屬性,不管是否可遍歷,可以使用`Object.getOwnPropertyNames`方法。
另外,`JSON.stringify`方法會排除`enumerable`為`false`的屬性,有時可以利用這一點。如果對象的 JSON 格式輸出要排除某些屬性,就可以把這些屬性的`enumerable`設為`false`。
### configurable
`configurable`(可配置性)返回一個布爾值,決定了是否可以修改屬性描述對象。也就是說,`configurable`為`false`時,`value`、`writable`、`enumerable`和`configurable`都不能被修改了。
```javascript
var obj = Object.defineProperty({}, 'p', {
value: 1,
writable: false,
enumerable: false,
configurable: false
});
Object.defineProperty(obj, 'p', {value: 2})
// TypeError: Cannot redefine property: p
Object.defineProperty(obj, 'p', {writable: true})
// TypeError: Cannot redefine property: p
Object.defineProperty(obj, 'p', {enumerable: true})
// TypeError: Cannot redefine property: p
Object.defineProperty(obj, 'p', {configurable: true})
// TypeError: Cannot redefine property: p
```
上面代碼中,`obj.p`的`configurable`為`false`。然后,改動`value`、`writable`、`enumerable`、`configurable`,結果都報錯。
注意,`writable`只有在`false`改為`true`會報錯,`true`改為`false`是允許的。
```javascript
var obj = Object.defineProperty({}, 'p', {
writable: true,
configurable: false
});
Object.defineProperty(obj, 'p', {writable: false})
// 修改成功
```
至于`value`,只要`writable`和`configurable`有一個為`true`,就允許改動。
```javascript
var o1 = Object.defineProperty({}, 'p', {
value: 1,
writable: true,
configurable: false
});
Object.defineProperty(o1, 'p', {value: 2})
// 修改成功
var o2 = Object.defineProperty({}, 'p', {
value: 1,
writable: false,
configurable: true
});
Object.defineProperty(o2, 'p', {value: 2})
// 修改成功
```
另外,`writable`為`false`時,直接目標屬性賦值,不報錯,但不會成功。
```javascript
var obj = Object.defineProperty({}, 'p', {
value: 1,
writable: false,
configurable: false
});
obj.p = 2;
obj.p // 1
```
上面代碼中,`obj.p`的`writable`為`false`,對`obj.p`直接賦值不會生效。如果是嚴格模式,還會報錯。
可配置性決定了目標屬性是否可以被刪除(delete)。
```javascript
var obj = Object.defineProperties({}, {
p1: { value: 1, configurable: true },
p2: { value: 2, configurable: false }
});
delete obj.p1 // true
delete obj.p2 // false
obj.p1 // undefined
obj.p2 // 2
```
上面代碼中,`obj.p1`的`configurable`是`true`,所以可以被刪除,`obj.p2`就無法刪除。
## 存取器
除了直接定義以外,屬性還可以用存取器(accessor)定義。其中,存值函數稱為`setter`,使用屬性描述對象的`set`屬性;取值函數稱為`getter`,使用屬性描述對象的`get`屬性。
一旦對目標屬性定義了存取器,那么存取的時候,都將執行對應的函數。利用這個功能,可以實現許多高級特性,比如定制屬性的讀取和賦值行為。
```javascript
var obj = Object.defineProperty({}, 'p', {
get: function () {
return 'getter';
},
set: function (value) {
console.log('setter: ' + value);
}
});
obj.p // "getter"
obj.p = 123 // "setter: 123"
```
上面代碼中,`obj.p`定義了`get`和`set`屬性。`obj.p`取值時,就會調用`get`;賦值時,就會調用`set`。
JavaScript 還提供了存取器的另一種寫法。
```javascript
// 寫法二
var obj = {
get p() {
return 'getter';
},
set p(value) {
console.log('setter: ' + value);
}
};
```
上面兩種寫法,雖然屬性`p`的讀取和賦值行為是一樣的,但是有一些細微的區別。第一種寫法,屬性`p`的`configurable`和`enumerable`都為`false`,從而導致屬性`p`是不可遍歷的;第二種寫法,屬性`p`的`configurable`和`enumerable`都為`true`,因此屬性`p`是可遍歷的。實際開發中,寫法二更常用。
注意,取值函數`get`不能接受參數,存值函數`set`只能接受一個參數(即屬性的值)。
存取器往往用于,屬性的值依賴對象內部數據的場合。
```javascript
var obj ={
$n : 5,
get next() { return this.$n++ },
set next(n) {
if (n >= this.$n) this.$n = n;
else throw new Error('新的值必須大于當前值');
}
};
obj.next // 5
obj.next = 10;
obj.next // 10
obj.next = 5;
// Uncaught Error: 新的值必須大于當前值
```
上面代碼中,`next`屬性的存值函數和取值函數,都依賴于內部屬性`$n`。
## 對象的拷貝
有時,我們需要將一個對象的所有屬性,拷貝到另一個對象,可以用下面的方法實現。
```javascript
var extend = function (to, from) {
for (var property in from) {
to[property] = from[property];
}
return to;
}
extend({}, {
a: 1
})
// {a: 1}
```
上面這個方法的問題在于,如果遇到存取器定義的屬性,會只拷貝值。
```javascript
extend({}, {
get a() { return 1 }
})
// {a: 1}
```
為了解決這個問題,我們可以通過`Object.defineProperty`方法來拷貝屬性。
```javascript
var extend = function (to, from) {
for (var property in from) {
if (!from.hasOwnProperty(property)) continue;
Object.defineProperty(
to,
property,
Object.getOwnPropertyDescriptor(from, property)
);
}
return to;
}
extend({}, { get a(){ return 1 } })
// { get a(){ return 1 } })
```
上面代碼中,`hasOwnProperty`那一行用來過濾掉繼承的屬性,否則可能會報錯,因為`Object.getOwnPropertyDescriptor`讀不到繼承屬性的屬性描述對象。
## 控制對象狀態
有時需要凍結對象的讀寫狀態,防止對象被改變。JavaScript 提供了三種凍結方法,最弱的一種是`Object.preventExtensions`,其次是`Object.seal`,最強的是`Object.freeze`。
### Object.preventExtensions()
`Object.preventExtensions`方法可以使得一個對象無法再添加新的屬性。
```javascript
var obj = new Object();
Object.preventExtensions(obj);
Object.defineProperty(obj, 'p', {
value: 'hello'
});
// TypeError: Cannot define property:p, object is not extensible.
obj.p = 1;
obj.p // undefined
```
上面代碼中,`obj`對象經過`Object.preventExtensions`以后,就無法添加新屬性了。
### Object.isExtensible()
`Object.isExtensible`方法用于檢查一個對象是否使用了`Object.preventExtensions`方法。也就是說,檢查是否可以為一個對象添加屬性。
```javascript
var obj = new Object();
Object.isExtensible(obj) // true
Object.preventExtensions(obj);
Object.isExtensible(obj) // false
```
上面代碼中,對`obj`對象使用`Object.preventExtensions`方法以后,再使用`Object.isExtensible`方法,返回`false`,表示已經不能添加新屬性了。
### Object.seal()
`Object.seal`方法使得一個對象既無法添加新屬性,也無法刪除舊屬性。
```javascript
var obj = { p: 'hello' };
Object.seal(obj);
delete obj.p;
obj.p // "hello"
obj.x = 'world';
obj.x // undefined
```
上面代碼中,`obj`對象執行`Object.seal`方法以后,就無法添加新屬性和刪除舊屬性了。
`Object.seal`實質是把屬性描述對象的`configurable`屬性設為`false`,因此屬性描述對象不再能改變了。
```javascript
var obj = {
p: 'a'
};
// seal方法之前
Object.getOwnPropertyDescriptor(obj, 'p')
// Object {
// value: "a",
// writable: true,
// enumerable: true,
// configurable: true
// }
Object.seal(obj);
// seal方法之后
Object.getOwnPropertyDescriptor(obj, 'p')
// Object {
// value: "a",
// writable: true,
// enumerable: true,
// configurable: false
// }
Object.defineProperty(obj, 'p', {
enumerable: false
})
// TypeError: Cannot redefine property: p
```
上面代碼中,使用`Object.seal`方法之后,屬性描述對象的`configurable`屬性就變成了`false`,然后改變`enumerable`屬性就會報錯。
`Object.seal`只是禁止新增或刪除屬性,并不影響修改某個屬性的值。
```javascript
var obj = { p: 'a' };
Object.seal(obj);
obj.p = 'b';
obj.p // 'b'
```
上面代碼中,`Object.seal`方法對`p`屬性的`value`無效,是因為此時`p`屬性的可寫性由`writable`決定。
### Object.isSealed()
`Object.isSealed`方法用于檢查一個對象是否使用了`Object.seal`方法。
```javascript
var obj = { p: 'a' };
Object.seal(obj);
Object.isSealed(obj) // true
```
這時,`Object.isExtensible`方法也返回`false`。
```javascript
var obj = { p: 'a' };
Object.seal(obj);
Object.isExtensible(obj) // false
```
### Object.freeze()
`Object.freeze`方法可以使得一個對象無法添加新屬性、無法刪除舊屬性、也無法改變屬性的值,使得這個對象實際上變成了常量。
```javascript
var obj = {
p: 'hello'
};
Object.freeze(obj);
obj.p = 'world';
obj.p // "hello"
obj.t = 'hello';
obj.t // undefined
delete obj.p // false
obj.p // "hello"
```
上面代碼中,對`obj`對象進行`Object.freeze()`以后,修改屬性、新增屬性、刪除屬性都無效了。這些操作并不報錯,只是默默地失敗。如果在嚴格模式下,則會報錯。
### Object.isFrozen()
`Object.isFrozen`方法用于檢查一個對象是否使用了`Object.freeze`方法。
```javascript
var obj = {
p: 'hello'
};
Object.freeze(obj);
Object.isFrozen(obj) // true
```
使用`Object.freeze`方法以后,`Object.isSealed`將會返回`true`,`Object.isExtensible`返回`false`。
```javascript
var obj = {
p: 'hello'
};
Object.freeze(obj);
Object.isSealed(obj) // true
Object.isExtensible(obj) // false
```
`Object.isFrozen`的一個用途是,確認某個對象沒有被凍結后,再對它的屬性賦值。
```javascript
var obj = {
p: 'hello'
};
Object.freeze(obj);
if (!Object.isFrozen(obj)) {
obj.p = 'world';
}
```
上面代碼中,確認`obj`沒有被凍結后,再對它的屬性賦值,就不會報錯了。
### 局限性
上面的三個方法鎖定對象的可寫性有一個漏洞:可以通過改變原型對象,來為對象增加屬性。
```javascript
var obj = new Object();
Object.preventExtensions(obj);
var proto = Object.getPrototypeOf(obj);
proto.t = 'hello';
obj.t
// hello
```
上面代碼中,對象`obj`本身不能新增屬性,但是可以在它的原型對象上新增屬性,就依然能夠在`obj`上讀到。
一種解決方案是,把`obj`的原型也凍結住。
```javascript
var obj = new Object();
Object.preventExtensions(obj);
var proto = Object.getPrototypeOf(obj);
Object.preventExtensions(proto);
proto.t = 'hello';
obj.t // undefined
```
另外一個局限是,如果屬性值是對象,上面這些方法只能凍結屬性指向的對象,而不能凍結對象本身的內容。
```javascript
var obj = {
foo: 1,
bar: ['a', 'b']
};
Object.freeze(obj);
obj.bar.push('c');
obj.bar // ["a", "b", "c"]
```
上面代碼中,`obj.bar`屬性指向一個數組,`obj`對象被凍結以后,這個指向無法改變,即無法指向其他值,但是所指向的數組是可以改變的。
- 前言
- 入門篇
- 導論
- 歷史
- 基本語法
- 數據類型
- 概述
- null,undefined 和布爾值
- 數值
- 字符串
- 對象
- 函數
- 數組
- 運算符
- 算術運算符
- 比較運算符
- 布爾運算符
- 二進制位運算符
- 其他運算符,運算順序
- 語法專題
- 數據類型的轉換
- 錯誤處理機制
- 編程風格
- console 對象與控制臺
- 標準庫
- Object 對象
- 屬性描述對象
- Array 對象
- 包裝對象
- Boolean 對象
- Number 對象
- String 對象
- Math 對象
- Date 對象
- RegExp 對象
- JSON 對象
- 面向對象編程
- 實例對象與 new 命令
- this 關鍵字
- 對象的繼承
- Object 對象的相關方法
- 嚴格模式
- 異步操作
- 概述
- 定時器
- Promise 對象
- DOM
- 概述
- Node 接口
- NodeList 接口,HTMLCollection 接口
- ParentNode 接口,ChildNode 接口
- Document 節點
- Element 節點
- 屬性的操作
- Text 節點和 DocumentFragment 節點
- CSS 操作
- Mutation Observer API
- 事件
- EventTarget 接口
- 事件模型
- Event 對象
- 鼠標事件
- 鍵盤事件
- 進度事件
- 表單事件
- 觸摸事件
- 拖拉事件
- 其他常見事件
- GlobalEventHandlers 接口
- 瀏覽器模型
- 瀏覽器模型概述
- window 對象
- Navigator 對象,Screen 對象
- Cookie
- XMLHttpRequest 對象
- 同源限制
- CORS 通信
- Storage 接口
- History 對象
- Location 對象,URL 對象,URLSearchParams 對象
- ArrayBuffer 對象,Blob 對象
- File 對象,FileList 對象,FileReader 對象
- 表單,FormData 對象
- IndexedDB API
- Web Worker
- 附錄:網頁元素接口
- a
- img
- form
- input
- button
- option
- video,audio