0%

第四章 对象

对象功能扩展

ES6在改进对象能力上花了很多的功夫,这是因为基本大部分js的值都是某种类型的对象,另外随着js应用程序复杂程度的提升代码中对象数量也随之提升。这也意味着对象需要更加高效的使用。

ES6通过各种方法改进对象,从简单的语法扩展再到提供更多选择处理对象。

对象类型

js使用混合术语来描述标准里的对象,而不是通过执行环境(浏览器,Node.js)来描述,ES6规格书里明确定义了每种类别的对象。对于全面了解这面语言理解这些术语是相当重要的,对象的类别如下:

  • __普通对象__拥有所有在js中内置默认行为的对象。
  • __外来对象__拥有和内置行为和默认行为不一致的对象。
  • __标准对象__哪些由ES6定义的对象,如Array,Date等,标准对象有可能是普通对象也有可能是外来对象。
  • __内置对象__在js开始执行之前就存在的对象,所有标准对象对象都是内置对象。

我将在整本书里用这些术语来解释ES6定义的对象

对象字面量语法扩展

对象字面量是js中最流行的模式之一,JSON就是基于它的语法,并且几乎存在于所有的在因特网上js文件,它流行的原因在于其简洁创建对象的方式。幸运的是ES6通过各种扩展语法的方法使用对象字面量更加有用。

属性初始化速记

在早先ES5之前,对象字面量只是简单的name-value对集合,这意味着当属性和值初始化时有可能重复,如例:

1
2
3
4
5
6
function createPerson(name, age) {
return {
name: name,
age: age
};
}

在ES6中你可以通过属性初始化速记消除属性和本地变量。当对象属性和本地属性同名时,你可以仅包含名字而不需要冒号和值,如例createPerson()可以用ES6的方式重写:

1
2
3
4
5
6
function createPerson(name, age){
return {
name,
age
};
}

当对象字面量的一个属性只有一个名字,js引擎将在附近作用域寻找与之名字相同的变量,如果找到了相同名字变量,将这个变量的值赋给字面量的这同名变量,在上面例子中对象字面量属性name就被赋予本地变量name值。

这种扩展让对象字面量初始化更加简洁并且对消除命名错误起着一定的帮助,在js中将属性的值赋予一个本地同名变量的值是一种非常常见模式,这让这种扩展相当受欢迎。

简洁方法

ES6中添加了一种在对象字面中添加方法的语法,在早先的ES5和之前的语法中,你在对象字面量中添加方法必须需要指定一个名字和一个完整的函数定义,如下:

1
2
3
4
5
6
var person = {
name: "Nicholas",
sayName: function() {
console.log(this.name);
}
};

在ES6中,可以将这种语法通过消除分号和function关键词来简化,这意味着你可以重写前面的例子如下:

1
2
3
4
5
6
var person = {
name: "Nicholas",
sayName() {
console.log(this.name);
}
};

这种速写语法也称简洁方法语法,像先前的那个对象Person,sayName属性被赋予了一个匿名函数并且和ES5中sayName()函数具有相同特性。唯一的不同是简洁方法也许使用super,但是非简洁方法也许不需要。

计算属性名称

在ES5和之前中在对象实例上可以使用方括号而不是点标记来计算属性,方括号让你可以使用字符串和变量来指定属性名,这些字符串包含的字符如果被当做标识符使用会造成一个语法错误。如下:

1
2
3
4
5
6
7
8
var person = {},
lastName = "last name";

person["first name"] = "Nicholas";
person[lastName] = "Zakas";

console.log(person["first name"]); // "Nicholas"
console.log(person[lastName]); // "Zakas"

因为lastName被赋值为”last name”,属性名字有空格就不能使用使用点标记。

另外你可以直接使用字符串字面量当做对象字面量属性名,如下:

1
2
3
4
5
var person = {
"first name": "Nicholas"
};

console.log(person["first name"]); // "Nicholas"

这种模式在属性名称提前知道情况下可以用字符串字面量表示,然而如果属性名”first name”被包含在一个变量中或者必须通过计算,这就没有 用ES5方式定义对象字面量的属性了。

在ES6中,计算属性名称是对象字面量语法的一部分,并且它们也同样使用方括号,如下:

1
2
3
4
5
6
7
8
9
var lastName = "last name";

var person = {
"first name": "Nicholas",
[lastName]: "Zakas"
};

console.log(person["first name"]); // "Nicholas"
console.log(person[lastName]); // "Zakas"

对象字面量中的方括号暗示这个属性名称是计算过的,所以它的内容被求值为字符,这也就意味着你也可以用方括号包含一个表达式:

1
2
3
4
5
6
7
8
9
var suffix = " name";

var person = {
["first" + suffix]: "Nicholas",
["last" + suffix]: "Zakas"
};

console.log(person["first name"]); // "Nicholas"
console.log(person["last name"]); // "Zakas"

这些属性被求值为”first name”和”last name”,之后这些字符串可以被当做对象属性的引用。

新方法

在ES设计目标中,有一个从ES5就开始了的,那就是通过在全局对象增加新的方法来避免在Object.prototype上创建新的全局方法或函数。因此ES6引进许多新方法在全局对象上结果是让某些工作更容易。

Object.is()

在js中当你想对比两个值时,你也许会用等于操作符(==)或全等操作符(===)来比较。大多数开发者更喜欢后者,这是因为这样可以避免比较中的强制类型转换。但是就算是全等操作符也不可能完全正确。如 +0和-0通过全等符(===)比较也相等,但是在js引擎中它们表示两种不一样的值,同样 NaN === NaN 返回false,这就必须要用isNaN()来检查NaN属性了。

ES6通过引进Object.is()方法来弥补全等操作符(===)的缺陷,这个方法接受两个参数如果返回true则表明真正的相等,只有当两个值拥有相同类型和值时才能算真正相等:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
console.log(+0 == -0);              // true
console.log(+0 === -0); // true
console.log(Object.is(+0, -0)); // false

console.log(NaN == NaN); // false
console.log(NaN === NaN); // false
console.log(Object.is(NaN, NaN)); // true

console.log(5 == 5); // true
console.log(5 == "5"); // true
console.log(5 === 5); // true
console.log(5 === "5"); // false
console.log(Object.is(5, 5)); // true
console.log(Object.is(5, "5")); // false

在许多情况,Object.is()和全等操作符(===)作用相同,唯一的不同就体现在+0和-0,NaN的比较上,但是没有必要停止使用全等操作符(===),是否选择使用Object.is()取决于你代码实际情况。

Obejct.assign()

在js中混合是对象组成最流行的模式,在使用混合时一个对象接受另一个对象的属性和方法,许多js库都有实现这样的一个混合方法:

1
2
3
4
5
6
function mixin(receiver, supplier){
Object.keys(supplier).forEach(function(key){
receiver[key] = supplier[key];
});
return receiver;
}

这个mixin()方法通过遍历supplier本身属性来复制到receiver上()(这是一个浅复制,当复制一个对象时只复制对象的一个引用)。这让receiver不需要继承而得到新属性,如下:

1
2
3
4
5
6
7
8
9
10
11
function EventTarget() { /*...*/ }
EventTarget.prototype = {
constructor: EventTarget,
emit: function() { /*...*/ },
on: function() { /*...*/ }
};

var myObject = {};
mixin(myObject, EventTarget.prototype);

myObject.emit("somethingChanged");

这里myObject 通过接收EventTarget.prototype对象行为,而获得相应的订阅和发布能力。

这种模式流行到ES6都为之添加一个Obejct.assign()方法,它的表现方式和mixin()都相同接收一个receiver和多个suppliers,最后返回一个ceceiver,它的名字从mixin变到assign,同时这个名字反应实际的操作。因为这个mixin()函数实际上使用 =操作符,所以不能复制 accessor 属性,Object.assign()就是反应这个区别的。

你可以在任何使用mixin()的地方使用Object.assign(),如下:

1
2
3
4
5
6
7
8
9
10
11
function EventTarget() { /*...*/ }
EventTarget.prototype = {
constructor: EventTarget,
emit: function() { /*...*/ },
on: function() { /*...*/ }
}

var myObject = {}
Object.assign(myObject, EventTarget.prototype);

myObject.emit("somethingChanged");

方法Object.assign()可以接受任意多个suppliers,receiver接受属性的顺序由suppliers指定,这就意味着第二个supplier可以重写第一个supplier赋给receiver的值。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var receiver = {};

Object.assign(receiver,
{
type: "js",
name: "file.js"
},
{
type: "css"
}
);

console.log(receiver.type); // "css"
console.log(receiver.name); // "file.js"

reviver.type的值为”css”因为第二个supplier重写第一个赋予的值。

Object.assign方法在ES6中并不是什么大的添加,但这会格式化许多js库的这个方法。

对象字面量属性名重叠

ES5严格模式中引进了检查对象字面量属性重叠,如果发现有重叠属性将会抛出一个错误,如下:

1
2
3
4
5
6
"use strict";

var person = {
name: "Nicholas",
name: "Greg" // syntax error in ES5 strict mode
};

然而在ES6中无论是否在严格模式都将不会检查属性名重叠。

1
2
3
4
5
6
7
8
"use strict";

var person = {
name: "Nicholas",
name: "Greg" // no error in ES6 strict mode
};

console.log(person.name); // "Greg"

可枚举属性排序

ES5并没有定义对象枚举时属性顺序,它将这些都交给js引擎自行决定。然而在ES6中明确定义了枚举属性时属性返回的顺序,它影响Object.getOwnPropertyNames()和Reflect.ownKeys()以及Object.assign()。

对象自身属性枚举基本顺序:

    1. 所有数字键按升序排序。
    1. 所有字符串按添加到对象顺序。
    1. 所有symbol键按添加到对象顺序。

如下例:

1
2
3
4
5
6
7
8
9
10
11
12
var obj = {
a: 1,
0: 1,
c: 1,
2: 1,
b: 1,
1: 1
};

obj.d = 1;

console.log(Object.getOwnPropertyNames(obj).join("")); // "012acbd"

方法Object.getOwnPropertyNames()返回对象键值以0,1,2,a,c,d,b的顺序.注意数字键集合在一起并排序后返回,尽管这不是对象字面量中顺序。字符串键值在数字键值之后以添加到对象的顺序排序,先是在对象字面量上的,然后是动态添加的。

  • note:在最新的chrome中Object.keys(),for-in,JSON.stringify()也是按这种顺序排序的。

更加强大的Prototypes

在js中Prototypes是继承的基石,并且在ES6中prototypes将更强大,在js的早些版本中prototypes的使用被严重的限制起来,然而随着这门语言的成熟和开发者更加熟悉使用Prototypes,开发者想更多的控制prototypes并且以一种更简单的方式使用。结果是ES6引进了一些在prototype上的改进。

对象上prototype改变

正常情况下,对象的prototype在对象被创建时(构造器,Object.create())就指定了,对象的prototype在对象实例化后没有改变是ES5中js程序最大的前提假设。尽管ES5添加了Object.getPrototypeOf()方法来检测对象的prototype。但是现状是仍然缺少标准方法改变实例化后对象的prototype。

ES6通过增加Object.setPrototypeof()方法来改变这种假设,这个方法可以任何对象的prototype。这个方法接受两个参数第一个为将要改变prototype的对象第二个为赋值的prototype值,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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”.当对象的prototype变成对象dog时,调用person.getGreeting()输出为”Woof”这是因为原来的与person对象的绑定已经断裂。

对象的prototype实际上是一个被叫做[[Prototype]]的内部只读属性,方法Object.getPrototypeOf()返回存储在[[Prototype]]的值同时方法Object.setPrototypeOf()改变存储在[[Prototype]]的值,然而这些不是唯一能操作[[Prototype]]的方法。

用Super引用访问Prototype

原来提到过,prototypes在js中相当重要并且为了更容易使用它ES6为此花了很多功夫在上面,这其中有super引用,这让访问对象的prototype功能更加容易,如下为了重载prototype上同名函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
let person = {
getGreeting() {
return "Hello";
}
};

let dog = {
getGreeting() {
return "Woof";
}
};


let friend = {
getGreeting() {
return Object.getPrototypeOf(this).getGreeting.call(this) + ", hi!";
}
};

// set prototype to person
Object.setPrototypeOf(friend, person);
console.log(friend.getGreeting()); // "Hello, hi!"
console.log(Object.getPrototypeOf(friend) === person); // true

// set prototype to dog
Object.setPrototypeOf(friend, dog);
console.log(friend.getGreeting()); // "Woof, hi!"
console.log(Object.getPrototypeOf(friend) === dog); // true

在这个例子中,在对象firend上调用和prototype上同名的getGreeting()方法。Object.getPrototypeOf()是为保障正确的prototype被使用,另一段额外的代码.call(this)是为了保障在prototype中this值设置正确。

为了使用原型(prototype)上的方法而使用Object.getPrototypeOf()和.call(this)这看起来有点复杂了,所以ES6引进了super,super指向当前对象prototype,等效于Object.getPrototypeOf(this),你可以简化getGreeting()方法如下:

1
2
3
4
5
6
7
let friend = {
getGreeting(){
// in the previous example, this is the same as:
// Object.getPrototypeOf(this).getGreeting.call(this)
return super.getGreeting() +" ,h1!";
}
};

在这上下文中调用super.getGreeting()相当于调用Object.getPrototypeOf(this).call(this),你可以通过super引用调用在原型上任何方法,但是如果原型链上没有此方法就会造成一个语法错误。

1
2
3
4
5
6
let friend = {
getGreeting: function() {
// syntax error
return super.getGreeting() + ", hi!";
}
};

这个例子中调用super.getGreeting()造成一个语法错误,这是因为super在这个上下文下是无效的。

正式定义方法

在ES6之前,方法(method)的概念没有正式定义,方法只是对象上为函数而不是数据的属性,然而ES6正式定义方法:一个拥有内部属性[[HomeObject]]的函数,这个属性包含这个函数属于的对象,如下:

1
2
3
4
5
6
7
8
9
10
11
12
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()找寻prototype的引用,如果在prototype上找到需要调用的同名属性或函数,最后this绑定方法调用,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let person = {
getGreeting() {
return "Hello";
}
};

// prototype is person
let friend = {
getGreeting() {
return super.getGreeting() + ", hi!";
}
};
Object.setPrototypeOf(friend, person);

console.log(friend.getGreeting()); // "Hello, hi!"

调用friend.getGreeting()返回一个字符串,这个字符串由person.getGreeting和” hi!”组成。friend.getGreeting()的[[HomeObject]]为friend,所以friend的prototype为person,最终super.getGreeting()等效于person.getGreeting.call(this)。

总结

ES6对对象字面量做了几处改变,简写属性定义让当前作用域同名属性赋值更方便了,计算属性名称让你可以指定非字面量值作为属性名称,简写方法让你在定义方法时少打一些字符(:function),ES6对象严格模式下重复定义属性名不再限制这意味着你可以在严格模式定义两个重名属性而得到最后个属性。

Object.assign()相当于一个标准的mixin模式。Object.is()是可以更加严格的等于比较甚至比===还严格。

枚举自身属性的顺序也在ES6中得到明确定义,数字升序优先,其次字符,最后symbol keys。

多亏ES6对象在创建后仍然可以通过方法Object.setPrototypeOf()改变其prototype.

最终你可以通过关键字super调用原型对象上的方法。(?)