[译]Understanding ECMAScript6 对象
对象
ECMAScript6将大量精力聚焦在提升对象的实用性性上。聚焦的意义在于JavaScript中几乎每一个值是由对象中的某种类型表示。此外,在一个普通的JavaScript程序中使用对象的数量持续增长,这个意味着开发人员总是在写更多的对象。随着对象越来越多,更高效地使用它们就很有必要了。
ECMAScript在很多方式上提升了对象。从简单的语法到对象操纵和交互的新方式。
对象类别
ECMAScript6规范引入了一些新的术语来帮助区分对象之间的类别。javascript曾长期充斥着用来描述标准中找到的相对于那些通过执行环境如浏览器添加的对象的混合术语。ECMAScript6花时间清楚定义了对象的各个类别,理解这个术语以对这门语言有一个整体的理解是很重要的。对象类别有:
- 普通对象是JavaScript中所有拥有默认内部行为的对象
- 外来对象是内部行为在某些方面与默认行为的不同的对象
- 标准对象是ECMAScript6定义的对象,比如
Array
,Date等。标准对象可能是普通或外来对象。
- 内建对象是当脚本开始执行时存在于JavaScript执行环境中的对象。所有的标准对象是内建对象。
这些术语在全书中使用,解释ECMAScript6中定义的对象变化。
对象字面量扩展
JavaScript中最流行的模式之一是对象字面量。JSON是建立在它的语法之上,且几乎可以在网络上的任何一个JavaScript文件中看到它。流行的原因很清楚:创建对象的简洁语法,否则要花费几行代码来完成。ECMAScript承认了对象字面量的流行,且在几个方面扩展了语法,使得对象字面量更强大甚至更简洁。
属性初始化简写
在ECMAScript5及更早版本,对象字面量是简单的键值对集合。这意味着当属性值被初始化时可能会有一些重复。比如:
function createPerson(name, age) { return { name: name, age: age }; }
createPerson()函数创建了一个属性名与函数参数名一样的对象,结果就是name和age重复出现,尽管它们每一个表示过程的不同方面。
在ECMAScript中,你可以通过使用属性初始化简写消除存在于属性名和本地变量之间的重复。当属性名与本地变量名相同时,你可以简单的包括名字,而不需要冒号和值。比如:createPerson()可以被重写如下:
function createPerson(name, age) { return { name, age }; }
当一个对象字面量中的属性只有名字而没有值,JavaScript引擎在周围查找同名的变量。如果找到,那个值就分配给对象字面上同名的属性。所以在此例中,对象字面量属性name被分配了本地变量name的值。
这一扩展的目的是时对象字面量的初始化比原来更简单。分配一个与本地变量同名的属性在JavaScript中是非常普遍的模式,所以这个扩展是很值得欢迎的。
方法初始化简写
ECMAScript6也改进了对象字面量分配方法的语法。在ECMAScript5及更早,你在给对象添加方法时必须指定一个名字和完整的函数定义。比如:
var person = { name: "Nicholas", sayName: function() { console.log(this.name); } };
在ECMAScript6中,通过消除冒号和function关键字,使得这一语法更加简洁。然后你可以重写之前的例子如下:
var person = { name: "Nicholas", sayName() { console.log(this.name); } };
正如之前的例子所做的一样,这一简写语法在person对象上创建了一个方法。除了为你节省一些按键外并没有区别,所以 sayName()被分配了一个匿名函数表达式,拥有前一例子中定义的函数的同样的特性。
注意:使用这一简写创建的方法的name属性是在括号之前使用的名字。前一例子中person.sayName()
的name属性是 "sayName"。
计算后的属性名称
JavaScript在使用方括号代替点符号中长期有计算后的属性名称。方括号允许你使用可能包含作为标识符使用会是一个语法错误的字符的变量和字符串字面量指定属性名称。比如:
var person = {}, lastName = "last name"; person["first name"] = "Nicholas"; person[lastName] = "Zakas"; console.log(person["first name"]); // "Nicholas" console.log(person[lastName]); // "Zakas"
这个例子中,两个属性名都有一个空格,我们不可能使用点符号引用这些名字。然而,方括号允许任何字符串作为属性名。
在ECMAScript5中,你可以在对象字面量中使用字符串字面量作为属性名,比如:
var person = { "first name": "Nicholas" }; console.log(person["first name"]); // "Nicholas"
如果你可以在对象字面量属性定义中提供字符串字面量,你就都准备好了。然而,如果属性名被包含在变量中或者需要经过计算,就没有办法用对象字面量定义这个属性了。
ECMAScript6通过使用相同的方括号表示法给对象字面量语法添加了计算后的属性名,用来在对象实例中引用计算后的属性名。比如:
var lastName = "last name"; var person = { "first name": "Nicholas", [lastName]: "Zakas" }; console.log(person["first name"]); // "Nicholas" console.log(person[lastName]); // "Zakas"
对象字面量中的方括号表明属性名是被计算过的,所以它的内容求值后是一个字符串。这意味着你也可以包含这样的表达式:
var suffix = " name"; var person = { ["first" + suffix]: "Nicholas", ["last" + suffix]: "Zakas" }; console.log(person["first name"]); // "Nicholas" console.log(person["last name"]); // "Zakas"
当在对象实例中使用括号表示法时,任何你要放到方括号中的东西都会为对象字面量中的属性名工作。
Object.assign()
对象组成最流行的模式之一是混合,即一个对象从另一个对象中接收属性和方法。许多JavaScript库有如下相似的mixin方法:
function mixin(receiver, supplier) { Object.keys(supplier).forEach(function(key) { receiver[key] = supplier[key]; }); return receiver; }
mixin()方法遍历supplier的自有属性然后将它们复制到receiver上。这使得receiver不需继承就可获得新的行为。比如:
function EventTarget() { /*...*/ } EventTarget.prototype = { constructor: EventTarget, emit: function() { /*...*/ }, on: function() { /*...*/ } }; var myObject = {}; mixin(myObject, EventTarget.prototype); myObject.emit("somethingChanged");
这个例子中,myObject接收了来自EventTarget.prototype的行为,这分别给予了myObject发布事件和其他订阅者使用emit()和on()的能力。
这一模式足够流行因此ECMAScript添加了起同样作用的Object.assign()。名字上的差异反映的是发生的实际操作,因为mixin()方法使用赋值操作符 (=
),它无法将存取器属性作为存取器属性复制到接收者中,Object.assign()
名字就被选用以反映这一区别。
注意:相似的方法在不同库中可能有其他的名字,对于相同的基本功能,一些流行的名字有areextend()和
mix()。
你可以在任何可能使用 mixin()
方法的地方使用Object.assign():
function EventTarget() { /*...*/ } EventTarget.prototype = { constructor: EventTarget, emit: function() { /*...*/ }, on: function() { /*...*/ } } var myObject = {} Object.assign(myObject, EventTarget.prototype); myObject.emit("somethingChanged");
Object.assign()方法接收任何数量的供应者,而接收者按照供应者被指定的顺序接收属性。这意味着第二个供应者可能重写接收者上来自第一个供应者的值。比如:
var receiver = {}; Object.assign(receiver, { type: "js", name: "file.js" }, { type: "css" } ); console.log(receiver.type); // "css" console.log(receiver.name); // "file.js"
receiver.type的值是“css”因为第二个供应者重写了第一个的值。
Object.assign()
方法对ECMAScript6而言不是一个大的增加,但它正式化了一个在许多JavaScript库中都能找到的常用函数。
处理存取器属性记住你不能在接收者上使用带有存取器属性的供应者创建存取器属性。因为Object.assign()使用了赋值运算符,供应者的存取器属性在接收者上会成为数据属性。比如: var receiver = {},
supplier = {
get name() {
return "file.js"
}
};
Object.assign(receiver, supplier);
var descriptor = Object.getOwnPropertyDescriptor(receiver, "name");
console.log(descriptor.value); // "file.js"
console.log(descriptor.get); // undefined
|
重复的对象字面量属性
ECMAscript5引入了重复对象字面量属性检测,如果找到重复就会抛出错误。比如:
var person = { name: "Nicholas", name: "Greg" // syntax error in ES5 strict mode };
当在ECMAScript5严格模式下运行时,这个例子会在第二个name属性中产生语法错误。
在ECMAScript6中,重复的属性检测被移除。严格模式和非严格模式都不再检测重复属性,而是用给出的名字的最后一个属性作为实际值来替代。
var person = { name: "Nicholas", name: "Greg" // not an error in ES6 }; console.log(person.name); // "Greg"
这个例子中, person.name
的值是 "Greg"。因为这是分配给这一属性的最后一个值。
原型变化
原型是继承的基础,所以ECMAScript6继续加强了原型。ECMAScript5为检索任意给定对象的原型添加了Object.getPrototypeOf()
方法。ECMAScript6添加了相反的操作,即Object.setPrototypeOf(),它使得你可以改变任意给定对象的原型。
通常,一个对象的原型是在其创建时指定的,要么通过构造器要么通过Object.create()。在ECMAScript6之前,没有一个标准的方法在一个对象已经被创建后去改变该对象的原型。在某种程度上, Object.setPrototypeOf()在这一点上改变了JavaScript中关于对象的最大设定之一,即一个对象的原型在创建后保持不变。
Object.setPrototypeOf()
方法接收两个参数,要改变原型的对象和要成为第一个参数的原型的对象。比如:
let person = { getGreeting() { return "Hello"; } }; let dog = { getGreeting() { return "Woof"; } }; // prototype is person let friend = Object.create(person); console.log(friend.getGreeting()); // "Hello" console.log(Object.getPrototypeOf(friend) === person); // true // set prototype to dog Object.setPrototypeOf(friend, dog); console.log(friend.getGreeting()); // "Woof" console.log(Object.getPrototypeOf(friend) === dog); // true
这段代码定义了两个基本对象,person和dog。两个对象都有一个返回一个字符串的getGreeting()方法。对象friend开始是继承自person,这意味着getGreeting()会输出“hello”。当改变原型用dog替换时,person.getGreeting()输出“Woof”,因为原来同person
的关系已经断开。
对象原型的实际值存储在仅限内部的叫做 [[Prototype]]的属性中。
Object.getPrototypeOf()方法返回
[[Prototype]]
中存储的值,而Object.setPrototypeOf()改变了存储在[[Prototype]]的值。然而,这并不是处理[[Prototype]]值的唯一方式。
甚至在ECMAScript5完成之前,一些JavaScript引擎已经实现了一个叫做 __proto__的自定义属性,它可以同时用来设置和获取一个对象的原型。实际上,对
Object.getPrototypeOf()和
Object.setPrototypeOf()而言,__proto__可以说是最早的先驱。期待所有的JavaScript引擎移除这一属性是不切实际的,所以ECMAScript6正式化了__proto__的行为。
在ECMAScript6引擎中,Object.prototype.__proto__被定义为一个存取属性,它的get方法调用了 Object.getPrototypeOf()而set方法调用了
Object.setPrototypeOf()。这意味着使用__proto__和其他除了__proto__的允许你直接设置对象字面量原型的方法并无不同。比如:
let person = { getGreeting() { return "Hello"; } }; let dog = { getGreeting() { return "Woof"; } }; // prototype is person let friend = { __proto__: person }; console.log(friend.getGreeting()); // "Hello" console.log(Object.getPrototypeOf(friend) === person); // true console.log(friend.__proto__ === person); // true // set prototype to dog friend.__proto__ = dog; console.log(friend.getGreeting()); // "Woof" console.log(friend.__proto__ === dog); // true console.log(Object.getPrototypeOf(friend) === dog); // true
这一例子在功能上同之前的是一样的。 Object.create()的调用被替换为分配了__proto__值的对象字面量。用
Object.create()
创建的对象同带 __proto__的对象字面量唯一的不同之处在于,前者要求你对任何附加的对象指定全属性描述符,而后者只是一个标准的对象字面量。
警告:__proto__属性在许多方面都很特殊:
1、在对象字面量中你只能指定一次。如果你指定了两个 __proto__属性,将抛出错误。这是有此限制的唯一一个对象字面量属性。
2、计算后的形式 ["__proto__"]
就像常规的属性一样,并不设置或者返回当前对象的原型。所有与对象字面量属性相关的规则以这种形式应用,相对于非计算形式,其中也有例外。当使用 __proto__时最好小心以确保你不要引起这些差异。
超级引用
正如前面所提到的,原型对JavaScript非常重要,ECMAScript6中大量的工作使得它们更容易使用。在改进的超类引用的引入使得对象原型上的访问功能变得更加简单。比如,如果你想要重写一个对象实例上的方法这样它也会调用同名的原型上的方法,在ECMAScript5中你要像下面这样做:
let person = { getGreeting() { return "Hello"; } }; let dog = { getGreeting() { return "Woof"; } }; // prototype is person let friend = { __proto__: person, getGreeting() { // same as this.__proto__.getGreeting.call(this) return Object.getPrototypeOf(this).getGreeting.call(this) + ", hi!"; } }; console.log(friend.getGreeting()); // "Hello, hi!" console.log(Object.getPrototypeOf(friend) === person); // true console.log(friend.__proto__ === person); // true // set prototype to dog friend.__proto__ = dog; console.log(friend.getGreeting()); // "Woof, hi!" console.log(friend.__proto__ === dog); // true console.log(Object.getPrototypeOf(friend) === dog); // true
这个例子中,friend的getGreeting()调用了同名的原型方法, Object.getPrototypeOf()方法用来确保该方法总是获取准确的原型然后添加一个额外的字符串。额外的.call(this)确保原型方法中this的值设置正确。
需要记住的是,使用Object.getPrototypeOf()
与使用.call(this)调用原型方法是有点关联的,所以,ECMAScript6引入了super。
最简单的,super充当指向当前对象原型的指针,有效地起到Object.getPrototypeOf(this)的作用。所以你可以按如下重写来简化getGreeting()方法:
let friend = { __proto__: person, getGreeting() { // same as Object.getPrototypeOf(this).getGreeting.call(this) // or this.__proto__.getGreeting.call(this) return super.getGreeting() + ", hi!"; } };
调用super.getGreeting()同调用Object.getPrototypeOf(this).getGreeting.call(this)
或 this.__proto__.getGreeting.call(this)是一样的。类似的,你可以使用super引用调用对象原型上的任何方法。
注意:super引用只能用在简洁函数内部,不能被用在其他函数或者全局作用域中。试图在简洁函数外使用super将导致语法错误。
方法
ECMAScript6之前,没有一个正式的“方法”的定义,方法只是包含了函数而非数据的对象的属性。ECMAScript6正式地将有内部[[HomeObject]]
属性包含了其所属对象的函数定义为方法。考虑如下代码:
let person = { // method getGreeting() { return "Hello"; } }; // not a method function shareGreeting() { return "Hi!"; }
这个例子定义了带有单一getGreeting()方法的person
。 由于直接将函数分配给了一个对象,getGreeting()
的[[HomeObject]]为person。另一方面,shareGreeting()函数没有被指定 [[HomeObject]]
,因为它在创建时未被分配给一个对象。大多数情况而言,这一区别并不重要,但在使用super时会非常重要。
任何对super的引用使用[[HomeObject]]
来决定该做什么。第一步是调用 [[HomeObject]]
上的Object.getPrototypeOf()来检索原型引用。然后原型查找同名函数来执行函数。最后,this绑定被设置,方法被调用。如果一个函数没有 [[HomeObject]]或者有一个同预期不同的
[[HomeObject]],这段程序就不会工作。比如:
let person = { getGreeting() { return "Hello"; } }; // prototype is person let friend = { __proto__: person, getGreeting() { return super() + ", hi!"; } }; function getGlobalGreeting() { return super.getGreeting() + ", yo!"; } console.log(friend.getGreeting()); // "Hello, hi!" getGlobalGreeting(); // throws error
调用friend.getGreeting()
返回一个字符串,而调用getGlobalGreeting()由于super的使用不当抛出一个错误。由于getGlobalGreeting()方法没有[[HomeObject]],也就不可能执行查找。有趣的是,如果 getGlobalGreeting()之后被作为方法添加到了friend,这种情况也不会改变:
// prototype is person let friend = { __proto__: person, getGreeting() { return super() + ", hi!"; } }; function getGlobalGreeting() { return super.getGreeting() + ", yo!"; } console.log(friend.getGreeting()); // "Hello, hi!" // assign getGreeting to the global function friend.getGreeting = getGlobalGreeting; friend.getGreeting(); // throws error
这里,全局的getGlobalGreeting()函数用来重写之前在friend上定义的 getGreeting()方法。此时调用friend.getGreeting()同样导致错误。
[[HomeObject]]的值只在方法被第一次创建时设置,所以即便添加到对象上也无法修复这个问题。
总结
在JavaScript中,对象是编程的中心,ECMAScript6已经为对象做了许多有用的改变,使它们变得更易处理且更强大。
ECMAScript6对对象字面量做了一些改变。简写属性的定义使得更容易分配与作用域内变量同名的属性。计算后的属性名允许你指定非字面量的值作为属性名,这是你在其他语言领域早就能够做到的事情。简写方法通过完全省略冒号让你在对象字面量上定义方法时少输了许多字符与function关键字。严格模式检查对重复的字面量属性名的放松也同样被引入,意味着在一个对象字面量中,现在可以有两个相同的属性名,而不会抛出任何错误。
Object.assign()方法使得更容易一次改变单个对象字面量上的多个属性。如果你使用混合模式,这会非常有用。
现在也可以在对象使用Object.setPrototypeOf()创建后去修改对象的原型。ECMAScript6也定义了__proto__属性行为,它是一个getter调用Object.getPrototypeOf()而setter调用Object.setPrototypeOf()的访问器属性。
super关键字现在可以用来调用对象原型上的方法。它可以作为一个独立的方法使用,比如super(),也可以作为原型本身的引用,比如super.getGreeting()。这两种情况,this绑定都会被自动建立以处理this的当前值。