更强大的原型

原型是在 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

此代码定义了两个基础对象:persondog,二者都拥有一个名为 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 中很难解决,但若使用 ES6super,就很简单了:

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(),而不管有多少对象继承了此方法。