对象字面量扩展

ES6给不起眼儿的{ .. }对象字面量增加了几个重要的便利扩展。

简约属性

你一定很熟悉用这种形式的对象字面量声明:

  1. var x = 2, y = 3,
  2. o = {
  3. x: x,
  4. y: y
  5. };

如果到处说x: x总是让你感到繁冗,那么有个好消息。如果你需要定义一个名称和词法标识符一致的属性,你可以将它从x: x缩写为x。考虑如下代码:

  1. var x = 2, y = 3,
  2. o = {
  3. x,
  4. y
  5. };

简约方法

本着与我们刚刚检视的简约属性相同的精神,添附在对象字面量属性上的函数也有一种便利简约形式。

以前的方式:

  1. var o = {
  2. x: function(){
  3. // ..
  4. },
  5. y: function(){
  6. // ..
  7. }
  8. }

而在ES6中:

  1. var o = {
  2. x() {
  3. // ..
  4. },
  5. y() {
  6. // ..
  7. }
  8. }

警告: 虽然x() { .. }看起来只是x: function(){ .. }的缩写,但是简约方法有一种特殊行为,是它们对应的老方式所不具有的;确切地说,是允许super(参见本章稍后的“对象super”)的使用。

Generator(见第四章)也有一种简约方法形式:

  1. var o = {
  2. *foo() { .. }
  3. };

简约匿名

虽然这种便利缩写十分诱人,但是这其中有一个微妙的坑要小心。为了展示这一点,让我们检视一下如下的前ES6代码,你可能会试着使用简约方法来重构它:

  1. function runSomething(o) {
  2. var x = Math.random(),
  3. y = Math.random();
  4. return o.something( x, y );
  5. }
  6. runSomething( {
  7. something: function something(x,y) {
  8. if (x > y) {
  9. // 使用相互对调的`x`和`y`来递归地调用
  10. return something( y, x );
  11. }
  12. return y - x;
  13. }
  14. } );

这段蠢代码只是生成两个随机数,然后用大的减去小的。但这里重要的不是它做的是什么,而是它是如何被定义的。让我把焦点放在对象字面量和函数定义上,就像我们在这里看到的:

  1. runSomething( {
  2. something: function something(x,y) {
  3. // ..
  4. }
  5. } );

为什么我们同时说something:function something?这不是冗余吗?实际上,不是,它们俩被用于不同的目的。属性something让我们能够调用o.something(..),有点儿像它的公有名称。但是第二个something是一个词法名称,使这个函数可以为了递归而从内部引用它自己。

你能看出来为什么return something(y,x)这一行需要名称something来引用这个函数吗?因为这里没有对象的词法名称,要是有的话我们就可以说return o.something(y,x)或者其他类似的东西。

当一个对象字面量的确拥有一个标识符名称时,这其实是一个很常见的做法,比如:

  1. var controller = {
  2. makeRequest: function(..){
  3. // ..
  4. controller.makeRequest(..);
  5. }
  6. };

这是个好主意吗?也许是,也许不是。你在假设名称controller将总是指向目标对象。但它也很可能不是 —— 函数makeRequest(..)不能控制外部的代码,因此不能强制你的假设一定成立。这可能会回过头来咬到你。

另一些人喜欢使用this定义这样的东西:

  1. var controller = {
  2. makeRequest: function(..){
  3. // ..
  4. this.makeRequest(..);
  5. }
  6. };

这看起来不错,而且如果你总是用controller.makeRequest(..)来调用方法的话它就应该能工作。但现在你有一个this绑定的坑,如果你做这样的事情的话:

  1. btn.addEventListener( "click", controller.makeRequest, false );

当然,你可以通过传递controller.makeRequest.bind(controller)作为绑定到事件上的处理器引用来解决这个问题。但是这很讨厌 —— 它不是很吸引人。

或者要是你的内部this.makeRequest(..)调用需要从一个嵌套的函数内发起呢?你会有另一个this绑定灾难,人们经常使用var self = this这种用黑科技解决,就像:

  1. var controller = {
  2. makeRequest: function(..){
  3. var self = this;
  4. btn.addEventListener( "click", function(){
  5. // ..
  6. self.makeRequest(..);
  7. }, false );
  8. }
  9. };

更讨厌。

注意: 更多关于this绑定规则和陷阱的信息,参见本系列的 this与对象原型 的第一到二章。

好了,这些与简约方法有什么关系?回想一下我们的something(..)方法定义:

  1. runSomething( {
  2. something: function something(x,y) {
  3. // ..
  4. }
  5. } );

在这里的第二个something提供了一个超级便利的词法标识符,它总是指向函数自己,给了我们一个可用于递归,事件绑定/解除等等的完美引用 —— 不用乱搞this或者使用不可靠的对象引用。

太好了!

那么,现在我们试着将函数引用重构为这种ES6解约方法的形式:

  1. runSomething( {
  2. something(x,y) {
  3. if (x > y) {
  4. return something( y, x );
  5. }
  6. return y - x;
  7. }
  8. } );

第一眼看上去不错,除了这个代码将会坏掉。return something(..)调用经不会找到something标识符,所以你会得到一个ReferenceError。噢,但为什么?

上面的ES6代码段将会被翻译为:

  1. runSomething( {
  2. something: function(x,y){
  3. if (x > y) {
  4. return something( y, x );
  5. }
  6. return y - x;
  7. }
  8. } );

仔细看。你看出问题了吗?简约方法定义暗指something: function(x,y)。看到我们依靠的第二个something是如何被省略的了吗?换句话说,简约方法暗指匿名函数表达式。

对,讨厌。

注意: 你可能认为在这里=>箭头函数是一个好的解决方案。但是它们也同样不够,因为它们也是匿名函数表达式。我们将在本章稍后的“箭头函数”中讲解它们。

一个部分地补偿了这一点的消息是,我们的简约函数something(x,y)将不会是完全匿名的。参见第七章的“函数名”来了解ES6函数名称的推断规则。这不会在递归中帮到我们,但是它至少在调试时有用处。

那么我们怎样总结简约方法?它们简短又甜蜜,而且很方便。但是你应当仅在你永远不需要将它们用于递归或事件绑定/解除时使用它们。否则,就坚持使用你的老式something: function something(..)方法定义。

你的很多方法都将可能从简约方法定义中受益,这是个非常好的消息!只要小心几处未命名的灾难就好。

ES5 Getter/Setter

技术上讲,ES5定义了getter/setter字面形式,但是看起来它们没有被太多地使用,这主要是由于缺乏转译器来处理这种新的语法(其实,它是ES5中加入的唯一的主要新语法)。所以虽然它不是一个ES6的新特性,我们也将简单地复习一下这种形式,因为它可能会随着ES6的向前发展而变得有用得多。

考虑如下代码:

  1. var o = {
  2. __id: 10,
  3. get id() { return this.__id++; },
  4. set id(v) { this.__id = v; }
  5. }
  6. o.id; // 10
  7. o.id; // 11
  8. o.id = 20;
  9. o.id; // 20
  10. // 而:
  11. o.__id; // 21
  12. o.__id; // 还是 —— 21!

这些getter和setter字面形式也可以出现在类中;参见第三章。

警告: 可能不太明显,但是setter字面量必须恰好有一个被声明的参数;省略它或罗列其他的参数都是不合法的语法。这个单独的必须参数 可以 使用解构和默认值(例如,set id({ id: v = 0 }) { .. }),但是收集/剩余...是不允许的(set id(...v) { .. })。

计算型属性名

你可能曾经遇到过像下面的代码段那样的情况,你的一个或多个属性名来自于某种表达式,因此你不能将它们放在对象字面量中:

  1. var prefix = "user_";
  2. var o = {
  3. baz: function(..){ .. }
  4. };
  5. o[ prefix + "foo" ] = function(..){ .. };
  6. o[ prefix + "bar" ] = function(..){ .. };
  7. ..

ES6为对象字面定义增加了一种语法,它允许你指定一个应当被计算的表达式,其结果就是被赋值属性名。考虑如下代码:

  1. var prefix = "user_";
  2. var o = {
  3. baz: function(..){ .. },
  4. [ prefix + "foo" ]: function(..){ .. },
  5. [ prefix + "bar" ]: function(..){ .. }
  6. ..
  7. };

任何合法的表达式都可以出现在位于对象字面定义的属性名位置的[ .. ]内部。

很有可能,计算型属性名最经常与Symbol(我们将在本章稍后的“Symbol”中讲解)一起使用,比如:

  1. var o = {
  2. [Symbol.toStringTag]: "really cool thing",
  3. ..
  4. };

Symbol.toStringTag是一个特殊的内建值,我们使用[ .. ]语法求值得到,所以我们可以将值"really cool thing"赋值给这个特殊的属性名。

计算型属性名还可以作为简约方法或简约generator的名称出现:

  1. var o = {
  2. ["f" + "oo"]() { .. } // 计算型简约方法
  3. *["b" + "ar"]() { .. } // 计算型简约generator
  4. };

设置[[Prototype]]

我们不会在这里讲解原型的细节,所以关于它的更多信息,参见本系列的 this与对象原型

有时候在你声明对象字面量的同时给它的[[Prototype]]赋值很有用。下面的代码在一段时期内曾经是许多JS引擎的一种非标准扩展,但是在ES6中得到了标准化:

  1. var o1 = {
  2. // ..
  3. };
  4. var o2 = {
  5. __proto__: o1,
  6. // ..
  7. };

o2是用一个对象字面量声明的,但它也被[[Prototype]]链接到了o1。这里的__proto__属性名还可以是一个字符串"__proto__",但是要注意它 不能 是一个计算型属性名的结果(参见前一节)。

客气点儿说,__proto__是有争议的。在ES6中,它看起来是一个最终被很勉强地标准化了的,几十年前的自主扩展功能。实际上,它属于ES6的“Annex B”,这一部分罗列了JS感觉它仅仅为了兼容性的原因,而不得不标准化的东西。

警告: 虽然我勉强赞同在一个对象字面定义中将__proto__作为一个键,但我绝对不赞同在对象属性形式中使用它,就像o.__proto__。这种形式既是一个getter也是一个setter(同样也是为了兼容性的原因),但绝对存在更好的选择。更多信息参见本系列的 this与对象原型

对于给一个既存的对象设置[[Prototype]],你可以使用ES6的工具Object.setPrototypeOf(..)。考虑如下代码:

  1. var o1 = {
  2. // ..
  3. };
  4. var o2 = {
  5. // ..
  6. };
  7. Object.setPrototypeOf( o2, o1 );

注意: 我们将在第六章中再次讨论Object。“Object.setPrototypeOf(..)静态函数”提供了关于Object.setPrototypeOf(..)的额外细节。另外参见“Object.assign(..)静态函数”来了解另一种将o2原型关联到o1的形式。

对象super

super通常被认为是仅与类有关。然而,由于JS对象仅有原型而没有类的性质,super是同样有效的,而且在普通对象的简约方法中行为几乎一样。

考虑如下代码:

  1. var o1 = {
  2. foo() {
  3. console.log( "o1:foo" );
  4. }
  5. };
  6. var o2 = {
  7. foo() {
  8. super.foo();
  9. console.log( "o2:foo" );
  10. }
  11. };
  12. Object.setPrototypeOf( o2, o1 );
  13. o2.foo(); // o1:foo
  14. // o2:foo

警告: super仅在简约方法中允许使用,而不允许在普通的函数表达式属性中。而且它还仅允许使用super.XXX形式(属性/方法访问),而不是super()形式。

在方法o2.foo()中的super引用被静态地锁定在了o2,而且明确地说是o2[[Prototype]]。这里的super基本上是Object.getPrototypeOf(o2) —— 显然被解析为o1 —— 这就是他如何找到并调用o1.foo()的。

关于super的完整细节,参见第三章的“类”。