安全地继承JavaScript中的原型

Safely inheriting prototypes in JavaScript

本文关键字:原型 JavaScript 继承 安全      更新时间:2023-09-26

假设我正在为应用程序中的一些基本继承而努力,我可以通过将孩子的原型设置为父对象来实现这一点。

// Parent "class"
var Car = function(year) {
    this.year = year;
};
Car.prototype.drive = function() {
    console.log('Vrooom');
};
// Child "class"
var Toyota = function() {
    Car.apply(this, arguments);
};
Toyota.prototype = Car.prototype;
var myCar = new Car(2010), myToyota = new Toyota(2010);
myCar.drive(); // Vrooom
myToyota.drive(); // Vrooom

似乎有效,但显然很糟糕,因为如果我在Toyota.prototype上设置一个新方法,它将在Car的原型上设置。

Toyota.prototype.stop = function() {
    console.log('stooop');
};
myCar.stop(); // stooop  <--- bad
myToyota.stop(); // stooop  <--- good

为了解决这个问题,我可以添加一个中介:,而不是Toyota.prototype = Car.prototype;

var ctor = function() { };
ctor.prototype = Car.prototype;
Toyota.prototype = new ctor();
Toyota.prototype.stop = function() {
    console.log('stooop');
};
myCar.stop(); // throws undefined error  <--- good
myToyota.stop(); // stooop  <--- good

但我不明白为什么这样有效。ctor创建一个新实例,其原型设置为Car's原型,然后Toyota将其原型设置为此新实例。

但是,为什么要创建一个空对象,其中包含一个被Toyota的原型引用的原型呢
为什么在Toyota上设置一个新方法不像在第一个示例中那样在Car上设置它?如果我想要几层继承,似乎每次都需要用new ctor将它们粘合在一起,该怎么办?

但是,为什么要创建一个带有被引用的原型的空对象呢丰田的原型?

因为如果你这样做:Toyota.prototype = new Car();

现在Toyota的原型是Car实例,这意味着除了链中有Car原型外,它还具有Car构造函数本身中设置的任何属性。基本上,您不希望Toyota.protype有一个类似year的属性:Toyota.prototype.year。正因为如此,有一个空的构造函数要好得多,比如:

var ctor = function() { };
ctor.prototype = Car.prototype;
Toyota.prototype = new ctor();

现在Toyota.protypenew ctor()实例作为其原型,而Car.prototype又在其自己的链中。这意味着丰田的实例现在可以执行汽车原型中存在的方法。


为什么不在丰田上设置一个新方法,就像在Car中设置一样在第一个例子中?

通过这个:Toyota.prototype = Car.prototype;,您将丰田的原型设置为与Car.prototype所包含的对象完全相同的对象。由于它是同一个对象,在一个地方更改它也会在其他地方更改它。这意味着对象是通过引用而不是通过值传递的,这是另一种说法,即当您将一个对象分配给3个不同的变量时,无论您使用哪个变量,它都是同一个对象。例如,字符串是按值传递的。下面是一个字符串示例:

var str1 = 'alpha';
var str2 = str1;
var str3 = str1;
str2 = 'beta';
// Changing str2 doesn't change the rest.
console.log(str1); //=> "alpha"
console.log(str3); //=> "alpha"
console.log(str2); //=> "beta"

现在比较对象及其属性;

var obj1 = {name: 'joe doe'};
var obj2 = obj1;
var obj3 = obj1;
console.log(obj1.name); //=> "joe doe"
console.log(obj2.name); //=> "joe doe"
console.log(obj3.name); //=> "joe doe"
obj2.name = 'JOHN SMITH';
console.log(obj1.name); //=> "JOHN SMITH"
console.log(obj2.name); //=> "JOHN SMITH"
console.log(obj3.name); //=> "JOHN SMITH"


如果我想要几层继承呢。。。

这里有另一种创建继承层的方法:

var Car = function(year) {
    this.year = year;
};
Car.prototype.drive = function() {
    console.log('Vrooom');
};
var Toyota = function() {
    Car.apply(this, arguments);
};
// `Object.create` does basically the same thing as what you were doing with `ctor()`
// Check out the documentation for `Object.create` as it takes a 2nd argument too.
Toyota.prototype = Object.create(Car.prototype);

// Create instances
var 
  myCar = new Car(2010), 
  myToyota = new Toyota(2010);

// Add method to Toyota's prototype
Toyota.prototype.stop = function() {
    console.log('stooop');
};

让我们现在试试:

myToyota.stop(); //=> 'stooop'
myCar.stop(); //=> 'TypeError: undefined is not a function'
myCar.drive(); //=> Vrooom
myToyota.drive(); //=> Vrooom

您的问题是以下行:

Toyota.prototype = Car.prototype;

然后修改这个对象:

Toyota.prototype.stop = function() {
    console.log('stooop');
};

因为在第一行中,您将Toyota.prototype设置为与Car.prototype完全相同的对象这不是副本,而是对同一对象的引用因此,一旦在Toyota.prototype上修改stop,实际上就同时修改了Toyota.prototypeCar.prototype,因为它是一个相同的。

你真正想做的是将第一行替换为:

Toyota.prototype = Object.create(Car);

因此,现在Toyota的原型指向Car函数,而应该指向Car自己的prototype。祝贺你,你已经使用了原型链!(注意:使用Object.create进行类继承本质上更稳定可靠,因为它不运行Car函数中可能包含的任何代码,而只设置原型链接。)

为了进一步讨论这里到底发生了什么,以及为什么你最好而不是在JS中使用"类继承",你可能想阅读《你不知道JS》(对象和原型)的第4-6章。

关于你的最后一个问题:"如果我想要几层继承,似乎每次都需要用一个新的ctor把它们粘在一起怎么办?"——是的,你需要这样做,这是JavaScript中(假)类和继承的标准模式。