更强大的原型
原型是在 JS 中进行继承的基础,ES6 则在继续让原型更强大。早期的 JS 版本对原型的使用有严重限制,然而随着语言的成熟,开发者也越来越熟悉原型的工作机制,因此他们明显希望能对原型有更多控制权,并能更方便地使用它。于是 ES6 就给原型引入了一些改进。
修改对象的原型
一般来说,对象的原型会在通过构造器或 Object.create() 方法创建该对象时被指定。直到 ES5 为止,JS 编程最重要的假定之一就是对象的原型在初始化完成后会保持不变。尽管 ES5 添加了 Object.getPrototypeOf() 方法来从任意指定对象中获取其原型,但仍然缺少在初始化之后更改对象原型的标准方法。
ES6 通过添加 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 对象,意味着 friend.getGreeting() 方法会输出 "Hello";当它的原型被更改为 dog 对象,friend.getGreeting() 方法就会改而输出 "Woof",因为原先与 person 的关联已经被破坏了。
对象原型的实际值被存储在一个内部属性 [[Prototype]] 上,Object.getPrototypeOf() 方法会返回此属性存储的值,而 Object.setPrototypeOf() 方法则能够修改该值。不过,使用 [[Prototype]] 属性的方式还不止这些。
使用 super 引用的简单原型访问
正如前面提到的,原型对 JS 来说非常重要,而 ES6 也进行了很多工作来让它更易用。关于原型的另一项进步就是引入了 super 引用,这让在对象原型上的功能调用变得更容易。例如,若要覆盖对象实例的一个方法、但依然要调用原型上的同名方法,你可能会这么做:
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
本例中 friend 上的 getGreeting() 调用了对象上的同名方法。Object.getPrototypeOf() 方法确保了能调用正确的原型,并在其返回结果上附加了一个字符串;而附加的 call(this) 代码则能确保正确设置原型方法内部的 this 值。
调用原型上的方法时要记住使用 Object.getPrototypeOf() 与 .call(this),这有点复杂难懂,因此 ES6 才引入了 super。简单来说,super 是指向当前对象的原型的一个指针,实际上就是 Object.getPrototypeOf(this) 的值。知道这些,你就可以像下面这样简化 getGreeting() 方法:
let friend = {
getGreeting() {
// in the previous example, this is the same as:
// Object.getPrototypeOf(this).getGreeting.call(this)
return super.getGreeting() + ", hi!";
}
};
此处调用 super.getGreeting() 等同于在上例的环境中使用 Object.getPrototypeOf(this).getGreeting.call(this)。类似的,你能使用 super 引用来调用对象原型上的任何方法,只要这个引用是位于简写的方法之内。试图在方法简写之外的情况使用 super 会导致语法错误,正如下例:
let friend = {
getGreeting: function() {
// syntax error
return super.getGreeting() + ", hi!";
}
};
此例使用了一个函数作为具名方法,于是调用 super.getGreeting() 就导致了语法错误,因为在这种上下文中 super 是不可用的。
当使用多级继承时,super 引用就是非常强大的,因为这种情况下 Object.getPrototypeOf() 不再适用于所有场景,例如:
let person = {
getGreeting() {
return "Hello";
}
};
// prototype is person
let friend = {
getGreeting() {
return Object.getPrototypeOf(this).getGreeting.call(this) + ", hi!";
}
};
Object.setPrototypeOf(friend, person);
// prototype is friend
let relative = Object.create(friend);
console.log(person.getGreeting()); // "Hello"
console.log(friend.getGreeting()); // "Hello, hi!"
console.log(relative.getGreeting()); // error!
调用 Object.getPrototypeOf() 时,在调用 relative.getGreeting() 处发生了错误。这是因为此时 this 的值是 relative,而 relative 的原型是 friend 对象,这样 friend.getGreeting().call() 调用就会导致进程开始反复进行递归调用,直到发生堆栈错误。
此问题在 ES5 中很难解决,但若使用 ES6 的 super,就很简单了:
let person = {
getGreeting() {
return "Hello";
}
};
// prototype is person
let friend = {
getGreeting() {
return super.getGreeting() + ", hi!";
}
};
Object.setPrototypeOf(friend, person);
// prototype is friend
let relative = Object.create(friend);
console.log(person.getGreeting()); // "Hello"
console.log(friend.getGreeting()); // "Hello, hi!"
console.log(relative.getGreeting()); // "Hello, hi!"
由于 super 引用并非是动态的,它总是能指向正确的对象。在本例中,super.getGreeting() 总是指向 person.getGreeting(),而不管有多少对象继承了此方法。