ES6学习笔记3--对象

  |  

前言

ES6对对象功能的扩展


4



ES6注重于提高对象的效用,这是因为在JS中几乎所有的值都是某种类型的对象。此外,随着JS应用的复杂度增长,在JS程序中所使用的对象的平均数也在持续增长,更多的对象就让有效使用它们变得更加重要

对象类别

JS使用混合属于描述能在标准中找到的对象,并且ES6明确定义了对象的每种类别

  • 普通对象:拥有JS对象所有默认的内部行动
  • 奇异对象:其内部行为在某些方面有别于默认行为
  • 标准对象:在ES6中被定义的对象,例如Array、Data,等等,标准对象可以是普通的也可以是奇异的
  • 内置对象:在脚本开始运行时由JS运行环境提供的对象。所有的标准对象都是内置对象

对象字面量语法的扩展

对象字面量是JS中创建对象最流行的方式,ES6用几种方式扩展了对象字面量

属性初始化器的速记法

在ES6及更早版本中,对象字面两是”键值对”的简单集合

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

在ES6中我们可以简化这种写法

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

当对象字面量中的属性只有名称时,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);
}
}

sayName属性被一个匿名函数赋值,且这种简写能使用super而非简写不能,并且这种简写创建的方法其name属性就是括号之前的名称

需计算属性名

在ES5及以前,我们访问计算属性名或者赋值计算属性名的值只能使用方括号

1
2
3
4
var person = {},
lastName = "last name";
person["first name"] = "Nicholas";
person[lastName] = "Zakas"

还可以直接在对象字面量中将字符串字面量直接用作属性名

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

这种模式要求属性名事先已知、并且能用字符串字面量表示,若属性名被包含在变量中,或者必须通过计算才能获得,那么在ES5的对象字面量中就无法定义这种属性

ES6可以直接在对象字面量中使用方括号来使用需计算属性名

1
2
3
4
5
var suffix = " name";
var person = {
["first" + suffix]: "Nicholas",
["last" + suffix]: "Zakas"
}

新的方法

ES6在Object对象上引入了两个新方法,以便让特定任务更易完成

Object.is()方法

在原先的JS中,当我们要比较两个值时,我们使用相等运算符(==)或严格相等运算符(===),但是即使是严格相等运算符,也存在一些问题,譬如它认为+0和-0是相等的、NaN===NaN返回false。

ES6引入了Object.is方法来弥补这些缺陷,此方法接收两个参数,并会在二者的类型相同值相等时返回true

1
2
3
4
NaN === NaN //false
Object.is(NaN, NaN) //true
+0 === -0 //true
Object.is(+0, -0) // false

Object.assign()方法

混入(Mixin)是在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对象(浅复制,当属性值为对象时,仅复制其引用)

由于这种模式的流行,所以ES6添加了Object.assign()方法来完成同样的行为。该方法接受一个接收者和任意数量的供应者,并会返回接收者。

1
2
3
4
5
6
7
8
function EventTarget() {/*...*/}
EvetnTarget.prototype = {
consturctor: EventTarget,
emit: function() {/*...*/)},
on: function(0 {/*...*/})
}
var myObject = {};
Object.assign(myObject, EventTarget.prototype);

Object.assign()方法接收任意数量的供应者,而接收者会按照供应者在参数中国你的顺序来一次接收它们的属性,这意味着后面的供应者可能会覆盖之前供应者已经提供的属性

除此之外,Object.assign()并不会在接收者上创建访问器属性,即使供应者拥有访问器属性。由于Object.assign()使用赋值运算符,所以供应者的访问器属性就会被转换为接收者的数据属性

1
2
3
4
5
6
7
8
9
10
11
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

重复的对象字面量属性

ES5严格模式为重复的对象字面量属性引入了一个检查,若找到重复的属性名,就会抛出错误

ES6移除了重复属性的检查,严格模式与非严格模式下都不再检查重复的属性。当存在重复属性时,排在后面的属性的值就会覆盖前面的值,成为该属性的实际值

自有属性的枚举顺序

ES5并没有定义对象属性的枚举顺序,ES6则严格定义了对象自有属性在被枚举时返回的顺序。浙江对Object.getOwnPropertyNames()Reflect.ownKeys如何返回属性造成了影响,还同时会影响到Object.assign()处理属性的顺序

自有属性枚举时的基本顺序:

  • 所有的数字类型键,按升序排序
  • 所有字符串类型键,按被添加到对象的顺序排序
  • 所有符号类型键,也按添加顺序排序
1
2
3
4
5
6
7
8
9
10
var obj = {
a: 1,
0: 1,
c: 1,
2: 1,
b: 1,
1: 1
}
obj.d = 1;
console.log(Object.getOwnPropertyNames(obj).join("")); //"012acbd"

数值类型的键会被合并并排序,即使这未遵守在对象字面量中的顺序。字符串类型的键会跟在数值类型的键之后,按照被添加到对象的顺序,在对象字面量中定义的键会首先出现,接下来是此后动态添加到对象的键

注意:

for-in循环的枚举顺序仍未被明确规定,因为并非所有的JS引擎都采用相同的方式。而Object.keys()JSON.stringify()也是用与for-in一样的枚举顺序

更强大的原型

原型是JS的继承的核心基础,ES6使得原型变的更加强大,早期的ES版本对原型做出了太多的限制,ES6解放了这些限制

修改对象的原型

一般来说,对象的原型会在通过构造器或者Object.create()方法创建该对象时被指定。知道ES5版本,JS编程的一个重要假设就是对象一旦被创建,它的原型将不在改变。尽管ES5添加了Object.getPrototypeOf()方法来获得一个对象的原型,但是仍然缺少直接修改原型的方法。

ES6通过添加了Object.setPrototypeOf()方法改变了这种假设,该方法接收两个参数,及要修改原型的对象,以及要成为前者原型的对象

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

let dog = {
getGretting() {
return "woof";
}
}

let friend = Object.create(person);
console.log(friend.getGreeting());
console.log(Object.getPrototypeOf(friend) === person);

Object.setPrototypeOf(friend, dog);
console.log(friend.getGreeting());
console.log(Object.getPrototypeOf(friend) === dog);

对象原型的实际值存放在对象的一个内部属性[[Prototype]]上,而Object.getPrototypeOf()方法可以获得并返回这个属性的值,而Object.setPrototypeOf()方法可以修改这个属性的值

使用super引用的简单原型访问

ES6扩张原型功能的另一个方式就是增加了super关键字,这让在对象原型的调用变得更容易,如果我们在ES5中要覆盖对象实例的一个方法、但依然要调用原型上的方法,过程可能有点复杂

1
2
3
4
5
6
7
8
9
10
11
12
13
let person = {
getGreeting() {
return "Hello";
}
}
let friend = {
getGreeting() {
return Object.getPrototypeOf(this).getGreeting.call(this) + ", hi";
}
}

Object.setPrototypeOf(friend, person);
console.log(friend.getGreeting()); // "Hello, hi"

整个的调用过程及其的复杂,getGreeting()方法先是通过Object.getPrototypeOf()方法返回了自身this的原型,及friend的原型,再通过setPrototypeOf方法绑定原型后,这个原型即使person。再调用原型的getGreeting()方法,并用call,将函数的执行上下文绑定到当前方法的上下文上,虽然这里即使不绑定也不影响其方法的执行,但是这是为了确保正确设置原型方法的内部this值,最后再加上附加的值。

整个过程复杂难懂,所以ES6引入了super,super是指向当前对象的原型的一个指针,我们用super来改写上面的示例

1
2
3
4
5
let friend = {
getGreeting(){
return super.getGreeting() + ", hi";
}
}

可以使用super来调用原型上的任何方法,但是需要注意的是super只能在对象的简写方法属性中才能使用,对于

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

这种使用方法是会导致语法错误的

super真正的强大之处在于多级继承中对对象原型的引用,因为这种情况下Object.getPrototypeOf()将不再适用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let person = {
getGreeting() {
return "Hello";
}
}
let friend = {
getGreeting() {
return Object.getPrototypeOf(this).getGreeting.call(this) + ", hi";
}
}

Object.setPrototypeOf(friend, person);
let relative = Object.create(friend);
console.log(relative.getGreeting()); // "error"

再调用relative.getGreeting()方法时会出错。这是因为在多级继承时,relative.getGreeting()方法的this是relative,而relative的原型是friend,这里由于call方法的存在会导致进程反复进行递归调用。简单来说就是当relative的getGreeting()方法执行时,它找到了friend的getGreeting()方法,并把this绑定为relative的this。当绑定后它又去找relative(及this)的原型,这样就陷入了一个死循环。导致这些错误的最主要的原因还是因为getPrototypeOf()方法使用的this的绑定是动态绑定的(实测,去除call绑定并不会导致错误,并且输出结果跟我一开始看到call绑定时误认为的结果一样,及”Hello, hi, hi”)

为了避免this绑定理解混乱导致的非预期结果,我们还是简单粗暴的使用super就好了

1
2
3
4
5
6
7
8
let friend = {
getGreeting() {
return super.getGreeting() + ", h1!";
}
}
Object.setPrototypeOf(freind, person);
let relative = Object.create(friend);
console.log(relative.getGreeting()); //"Hello, hi!";

由于super引用并非动态的,所以它总能指向正确的原型。这里relative的super指向了friend,在调用friend的getGreeting()方法时,super指向了person,所以通过两层的super指向,我们最终正确的调用了person的getGreeting()方法

正确的“方法”定义

在ES6之前,对象“方法”的概念从来没有被正式定义过,它仅仅只是指对象的函数属性。

ES6则正式对方法做出了定义:方法是一个拥有[[HomObject]]内部属性的函数,此内部属性指向该方法所属的对象

任何对super的引用都会使用[[HomeObject]]属性来判断:

  • 在[[HomeObject]]上调用Object.getPrototypeOf()方法来获取对原型的引用
  • 在该原型上查找同名函数
  • 最后创建this绑定并调用该方法

这里虽然也有this的绑定,当需要注意的是super的指向是确定的,this也是确定的,及指向了对象本身

文章目录
  1. 1. 对象类别
  2. 2. 对象字面量语法的扩展
    1. 2.1. 属性初始化器的速记法
    2. 2.2. 方法简写
    3. 2.3. 需计算属性名
  3. 3. 新的方法
    1. 3.1. Object.is()方法
    2. 3.2. Object.assign()方法
  4. 4. 重复的对象字面量属性
  5. 5. 自有属性的枚举顺序
  6. 6. 更强大的原型
    1. 6.1. 修改对象的原型
    2. 6.2. 使用super引用的简单原型访问
  7. 7. 正确的“方法”定义
|