对象字面量扩展
ES6给不起眼儿的{ .. }
对象字面量增加了几个重要的便利扩展。
简约属性
你一定很熟悉用这种形式的对象字面量声明:
var x = 2, y = 3,
o = {
x: x,
y: y
};
如果到处说x: x
总是让你感到繁冗,那么有个好消息。如果你需要定义一个名称和词法标识符一致的属性,你可以将它从x: x
缩写为x
。考虑如下代码:
var x = 2, y = 3,
o = {
x,
y
};
简约方法
本着与我们刚刚检视的简约属性相同的精神,添附在对象字面量属性上的函数也有一种便利简约形式。
以前的方式:
var o = {
x: function(){
// ..
},
y: function(){
// ..
}
}
而在ES6中:
var o = {
x() {
// ..
},
y() {
// ..
}
}
警告: 虽然x() { .. }
看起来只是x: function(){ .. }
的缩写,但是简约方法有一种特殊行为,是它们对应的老方式所不具有的;确切地说,是允许super
(参见本章稍后的“对象super
”)的使用。
Generator(见第四章)也有一种简约方法形式:
var o = {
*foo() { .. }
};
简约匿名
虽然这种便利缩写十分诱人,但是这其中有一个微妙的坑要小心。为了展示这一点,让我们检视一下如下的前ES6代码,你可能会试着使用简约方法来重构它:
function runSomething(o) {
var x = Math.random(),
y = Math.random();
return o.something( x, y );
}
runSomething( {
something: function something(x,y) {
if (x > y) {
// 使用相互对调的`x`和`y`来递归地调用
return something( y, x );
}
return y - x;
}
} );
这段蠢代码只是生成两个随机数,然后用大的减去小的。但这里重要的不是它做的是什么,而是它是如何被定义的。让我把焦点放在对象字面量和函数定义上,就像我们在这里看到的:
runSomething( {
something: function something(x,y) {
// ..
}
} );
为什么我们同时说something:
和function something
?这不是冗余吗?实际上,不是,它们俩被用于不同的目的。属性something
让我们能够调用o.something(..)
,有点儿像它的公有名称。但是第二个something
是一个词法名称,使这个函数可以为了递归而从内部引用它自己。
你能看出来为什么return something(y,x)
这一行需要名称something
来引用这个函数吗?因为这里没有对象的词法名称,要是有的话我们就可以说return o.something(y,x)
或者其他类似的东西。
当一个对象字面量的确拥有一个标识符名称时,这其实是一个很常见的做法,比如:
var controller = {
makeRequest: function(..){
// ..
controller.makeRequest(..);
}
};
这是个好主意吗?也许是,也许不是。你在假设名称controller
将总是指向目标对象。但它也很可能不是 —— 函数makeRequest(..)
不能控制外部的代码,因此不能强制你的假设一定成立。这可能会回过头来咬到你。
另一些人喜欢使用this
定义这样的东西:
var controller = {
makeRequest: function(..){
// ..
this.makeRequest(..);
}
};
这看起来不错,而且如果你总是用controller.makeRequest(..)
来调用方法的话它就应该能工作。但现在你有一个this
绑定的坑,如果你做这样的事情的话:
btn.addEventListener( "click", controller.makeRequest, false );
当然,你可以通过传递controller.makeRequest.bind(controller)
作为绑定到事件上的处理器引用来解决这个问题。但是这很讨厌 —— 它不是很吸引人。
或者要是你的内部this.makeRequest(..)
调用需要从一个嵌套的函数内发起呢?你会有另一个this
绑定灾难,人们经常使用var self = this
这种用黑科技解决,就像:
var controller = {
makeRequest: function(..){
var self = this;
btn.addEventListener( "click", function(){
// ..
self.makeRequest(..);
}, false );
}
};
更讨厌。
注意: 更多关于this
绑定规则和陷阱的信息,参见本系列的 this与对象原型 的第一到二章。
好了,这些与简约方法有什么关系?回想一下我们的something(..)
方法定义:
runSomething( {
something: function something(x,y) {
// ..
}
} );
在这里的第二个something
提供了一个超级便利的词法标识符,它总是指向函数自己,给了我们一个可用于递归,事件绑定/解除等等的完美引用 —— 不用乱搞this
或者使用不可靠的对象引用。
太好了!
那么,现在我们试着将函数引用重构为这种ES6解约方法的形式:
runSomething( {
something(x,y) {
if (x > y) {
return something( y, x );
}
return y - x;
}
} );
第一眼看上去不错,除了这个代码将会坏掉。return something(..)
调用经不会找到something
标识符,所以你会得到一个ReferenceError
。噢,但为什么?
上面的ES6代码段将会被翻译为:
runSomething( {
something: function(x,y){
if (x > y) {
return something( y, x );
}
return y - x;
}
} );
仔细看。你看出问题了吗?简约方法定义暗指something: function(x,y)
。看到我们依靠的第二个something
是如何被省略的了吗?换句话说,简约方法暗指匿名函数表达式。
对,讨厌。
注意: 你可能认为在这里=>
箭头函数是一个好的解决方案。但是它们也同样不够,因为它们也是匿名函数表达式。我们将在本章稍后的“箭头函数”中讲解它们。
一个部分地补偿了这一点的消息是,我们的简约函数something(x,y)
将不会是完全匿名的。参见第七章的“函数名”来了解ES6函数名称的推断规则。这不会在递归中帮到我们,但是它至少在调试时有用处。
那么我们怎样总结简约方法?它们简短又甜蜜,而且很方便。但是你应当仅在你永远不需要将它们用于递归或事件绑定/解除时使用它们。否则,就坚持使用你的老式something: function something(..)
方法定义。
你的很多方法都将可能从简约方法定义中受益,这是个非常好的消息!只要小心几处未命名的灾难就好。
ES5 Getter/Setter
技术上讲,ES5定义了getter/setter字面形式,但是看起来它们没有被太多地使用,这主要是由于缺乏转译器来处理这种新的语法(其实,它是ES5中加入的唯一的主要新语法)。所以虽然它不是一个ES6的新特性,我们也将简单地复习一下这种形式,因为它可能会随着ES6的向前发展而变得有用得多。
考虑如下代码:
var o = {
__id: 10,
get id() { return this.__id++; },
set id(v) { this.__id = v; }
}
o.id; // 10
o.id; // 11
o.id = 20;
o.id; // 20
// 而:
o.__id; // 21
o.__id; // 还是 —— 21!
这些getter和setter字面形式也可以出现在类中;参见第三章。
警告: 可能不太明显,但是setter字面量必须恰好有一个被声明的参数;省略它或罗列其他的参数都是不合法的语法。这个单独的必须参数 可以 使用解构和默认值(例如,set id({ id: v = 0 }) { .. }
),但是收集/剩余...
是不允许的(set id(...v) { .. }
)。
计算型属性名
你可能曾经遇到过像下面的代码段那样的情况,你的一个或多个属性名来自于某种表达式,因此你不能将它们放在对象字面量中:
var prefix = "user_";
var o = {
baz: function(..){ .. }
};
o[ prefix + "foo" ] = function(..){ .. };
o[ prefix + "bar" ] = function(..){ .. };
..
ES6为对象字面定义增加了一种语法,它允许你指定一个应当被计算的表达式,其结果就是被赋值属性名。考虑如下代码:
var prefix = "user_";
var o = {
baz: function(..){ .. },
[ prefix + "foo" ]: function(..){ .. },
[ prefix + "bar" ]: function(..){ .. }
..
};
任何合法的表达式都可以出现在位于对象字面定义的属性名位置的[ .. ]
内部。
很有可能,计算型属性名最经常与Symbol
(我们将在本章稍后的“Symbol”中讲解)一起使用,比如:
var o = {
[Symbol.toStringTag]: "really cool thing",
..
};
Symbol.toStringTag
是一个特殊的内建值,我们使用[ .. ]
语法求值得到,所以我们可以将值"really cool thing"
赋值给这个特殊的属性名。
计算型属性名还可以作为简约方法或简约generator的名称出现:
var o = {
["f" + "oo"]() { .. } // 计算型简约方法
*["b" + "ar"]() { .. } // 计算型简约generator
};
设置[[Prototype]]
我们不会在这里讲解原型的细节,所以关于它的更多信息,参见本系列的 this与对象原型。
有时候在你声明对象字面量的同时给它的[[Prototype]]
赋值很有用。下面的代码在一段时期内曾经是许多JS引擎的一种非标准扩展,但是在ES6中得到了标准化:
var o1 = {
// ..
};
var o2 = {
__proto__: o1,
// ..
};
o2
是用一个对象字面量声明的,但它也被[[Prototype]]
链接到了o1
。这里的__proto__
属性名还可以是一个字符串"__proto__"
,但是要注意它 不能 是一个计算型属性名的结果(参见前一节)。
客气点儿说,__proto__
是有争议的。在ES6中,它看起来是一个最终被很勉强地标准化了的,几十年前的自主扩展功能。实际上,它属于ES6的“Annex B”,这一部分罗列了JS感觉它仅仅为了兼容性的原因,而不得不标准化的东西。
警告: 虽然我勉强赞同在一个对象字面定义中将__proto__
作为一个键,但我绝对不赞同在对象属性形式中使用它,就像o.__proto__
。这种形式既是一个getter也是一个setter(同样也是为了兼容性的原因),但绝对存在更好的选择。更多信息参见本系列的 this与对象原型。
对于给一个既存的对象设置[[Prototype]]
,你可以使用ES6的工具Object.setPrototypeOf(..)
。考虑如下代码:
var o1 = {
// ..
};
var o2 = {
// ..
};
Object.setPrototypeOf( o2, o1 );
注意: 我们将在第六章中再次讨论Object
。“Object.setPrototypeOf(..)
静态函数”提供了关于Object.setPrototypeOf(..)
的额外细节。另外参见“Object.assign(..)
静态函数”来了解另一种将o2
原型关联到o1
的形式。
对象super
super
通常被认为是仅与类有关。然而,由于JS对象仅有原型而没有类的性质,super
是同样有效的,而且在普通对象的简约方法中行为几乎一样。
考虑如下代码:
var o1 = {
foo() {
console.log( "o1:foo" );
}
};
var o2 = {
foo() {
super.foo();
console.log( "o2:foo" );
}
};
Object.setPrototypeOf( o2, o1 );
o2.foo(); // o1:foo
// o2:foo
警告: super
仅在简约方法中允许使用,而不允许在普通的函数表达式属性中。而且它还仅允许使用super.XXX
形式(属性/方法访问),而不是super()
形式。
在方法o2.foo()
中的super
引用被静态地锁定在了o2
,而且明确地说是o2
的[[Prototype]]
。这里的super
基本上是Object.getPrototypeOf(o2)
—— 显然被解析为o1
—— 这就是他如何找到并调用o1.foo()
的。
关于super
的完整细节,参见第三章的“类”。