属性的种类
作者在一句话里总结说有三种。但是在这句话后面的一个段落里只罗列了两个:一个是命名属性,在JS代码中可以访问,还有一种是内部属性,JS代码中不能访问到。第三种是他在第一段落中提到的,在ES3中的传统的属性,在这个版本的标准里,属性的值与其命名是直接关联的,其内部的实现并没有被详细解释,虽然有些实现在属性值背后添加了访问器的概念,setter和getter方法。但是在ES5里这种情况被明确描述了,命名属性明确地有访问器,作者称之为named accessor property。。
属性的特性(attribute)
[[Value]]
[[Writable]]对应ES3里的{ReadOnly}。
[[Enumerable]]对应ES3里的{DontEnum}。
[[Configurable]]对应ES3里的{DontDelete}。
命名数据属性
这类属性有四个特性,它们的默认值是:
var defaultDataPropertyAttributes = {
[[Value]]: undefined,
[[Writable]]: false,
[[Enumerable]]: false,
[[Configurable]]: false
};
所以,也就是说你创建一个新属性的时候,你要是不提供这几个特性的值,那么这个属性就成了一个不可以修改,不能够枚举不能够再修改特性值的属性了,那也就等于一个常量:
// define a global constant
Object.defineProperty(this, "MAX_SIZE", {
value: 100
});
console.log(MAX_SIZE); // 100
MAX_SIZE = 200; // error in strict mode, [[Writable]] = false,
delete MAX_SIZE; // error in strict mode, [[Configurable]] = false
console.log(MAX_SIZE); // still 100
在《Effective JavaScript》里,有几个知识点都是跟可枚举特性有关的,问题在于在原型链上任何一个代理点新创建的属性都会被for...in循环遍历。所以在ES5里,这个问题可以通过enumerable解决:
Object.defineProperty(Array.prototype, "sum", {
value: function arraySum() {
// sum implementation
},
enumerable: false
});
// now with using the same example this "sum"
// is no longer enumerable
for (var k in a) {
console.log(k); // 0, 1, 2
}
这样一来,在ES3里面的一个单纯的赋值操作
// simple assignment (if we create a new property)
foo.bar = 10;
在ES5里就成了
// the same as
Object.defineProperty(foo, "bar", {
value: 10,
writable: true,
enumerable: true,
configurable: true
});
注意:
- defineProperty()不仅用来创建属性,也负责修改;
- defineProperty()返回被修改的对象本身;
- Object.keys()返回一个对象上的所有可枚举属性;
- Object.getOwnPropertyNames()返回一个对象上的所有属性,包括可枚举和不可枚举的。
命名访问器属性
学术化的定义是:命名访问器属性将两个访问器与一个属性名关联起来,分别是设定器和取值器,两者用来间接地修改和读取该属性的值。
其实感觉很接近C#里的property。
这类属性多了两个特性:[[Get]]和[[Set]]。
以前ES3系列的文章里有讲到过,ES3里每个对象都有些内部属性,其中[[Get]]和[[Put]]是负责读写属性值的。在ES5里,它们不是一回事,而实际上却又有紧密的关系,对象上的内部属性[[Get]]会呼叫访问器属性的[[Get]]特性,[[Put]]会呼叫[[Set]]。
10和20这段讲的意思在下面的示例代码里能更清楚地显现出来,如果在getter做一些手脚,那么将读不到这个属性自己的值。
这类属性的特性的默认值是:
var defaultAccessorPropertyAttributes = {
[[Get]]: undefined,
[[Set]]: undefined,
[[Enumerable]]: false,
[[Configurable]]: false
};
如果没有定义set,属性就成为只读。
用defineProperty()来定义属性的语法是:
var foo = {};
Object.defineProperty(foo, "bar", {
get: function getBar() {
return 20;
},
set: function setBar(value) {
// setting implementation
}
});
foo.bar = 10; // calls foo.bar.[[Set]](10)
// independently always 20
console.log(foo.bar); // calls foo.bar.[[Get]]()
或者用声明式也可以:
var foo = {
get bar () {
return 20;
},
set bar (value) {
console.log(value);
}
};
foo.bar = 100;
console.log(foo.bar); // 20
第一个坑:修改一个[[Configurable]]为false的对象
好,下面作者指出一个貌似是bug的东西,倒不能算bug,但或许是个坑。在前面讲过,一旦一个属性的[[Configurable]]是false,以后就不能再修改它的任何特性了,只能修改它的值而已,所以再次给getter赋值会导致一个异常,这个很合情合理:
// configurable false by default
var foo = Object.defineProperty({}, "bar", {
get: function () {
return "bar";
}
});
// trying to reconfigure the "bar"
// property => exception is thrown
try {
Object.defineProperty(foo, "bar", {
get: function () {
return "baz"
}
});
} catch (e) {
if (e instanceof TypeError) {
console.log(foo.bar); // still "bar"
}
}
但是,如果修改时的新值与当前值相同,这个异常不会被抛出。
function getBar() {
return "bar";
}
var foo = Object.defineProperty({}, "bar", {
get: getBar
});
// no exception even if configurable is false,
// but practically such "re"-configuration is useless
Object.defineProperty(foo, "bar", {
get: getBar
});
第二个坑:[[Configurable]]为false的前提下,[[Writable]]可以由true到false,但不可由false到true
看下下面的代码就明白了:
var foo = Object.defineProperty({}, "bar", {
value: "bar",
writable: true,
configurable: false
});
Object.defineProperty(foo, "bar", {
value: "baz"
});
console.log(foo.bar); // "baz"
// change writable
Object.defineProperty(foo, "bar", {
value: "qux",
writable: false // changed from true to false, OK
});
console.log(foo.bar); // "qux"
// try to change writable again - back to true
Object.defineProperty(foo, "bar", {
value: "qux",
writable: true // ERROR
});
第三个坑:[[Configurable]]为true的前提下,数据属性和访问器属性之间可以相互转换
如果[[Configurable]]为false当然就不可能了,作者得出结论说可见[[Writable]]有多么不重要。
// writable false by default
var foo = Object.defineProperty({}, "bar", {
value: "bar",
configurable: true
});
Object.defineProperty(foo, "bar", {
get: function () {
return "baz";
}
});
console.log(foo.bar); // OK, "baz"
第四个坑: