天天看点

DS.Lab筆記 - ECMA-262-5 - 属性与属性描述器属性的种类

属性的种类

作者在一句话里总结说有三种。但是在这句话后面的一个段落里只罗列了两个:一个是命名属性,在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"
           

第四个坑:

继续阅读