了解 JavaScript 中的原型继承

Posted

技术标签:

【中文标题】了解 JavaScript 中的原型继承【英文标题】:Understanding prototypal inheritance in JavaScript 【发布时间】:2010-10-27 21:57:22 【问题描述】:

我是 javascript OOP 的新手。您能解释一下以下代码块之间的区别吗?我测试了两个块都可以工作。最佳做法是什么?为什么?

第一个区块:

function Car(name)
    this.Name = name;


Car.prototype.Drive = function()
    console.log("My name is " + this.Name + " and I'm driving.");


SuperCar.prototype = new Car();
SuperCar.prototype.constructor = SuperCar;

function SuperCar(name)
    Car.call(this, name);


SuperCar.prototype.Fly = function()
    console.log("My name is " + this.Name + " and I'm flying!");


var myCar = new Car("Car");
myCar.Drive();

var mySuperCar = new SuperCar("SuperCar");
mySuperCar.Drive();
mySuperCar.Fly();

第二块:

function Car(name)
    this.Name = name;
    this.Drive = function() 
        console.log("My name is " + this.Name + " and I'm driving.");
    


SuperCar.prototype = new Car();

function SuperCar(name)
    Car.call(this, name);
    this.Fly = function()
        console.log("My name is " + this.Name + " and I'm flying!");
    


var myCar = new Car("Car");
myCar.Drive();

var mySuperCar = new SuperCar("SuperCar");
mySuperCar.Drive();
mySuperCar.Fly();

为什么作者在prototype中添加了DriveFly方法,并没有在Car类中声明为this.Drive方法,在SuperCar类中声明为this.Fly ?

为什么需要将SuperCar.prototype.constructor 设置回SuperCar?设置 prototype 时是否会覆盖 constructor 属性?我注释掉了这一行,没有任何改变。

为什么在SuperCar 构造函数中调用Car.call(this, name);?当我这样做时,Car 的属性和方法不会被“继承”

var myCar = new Car("Car");

【问题讨论】:

更现代的方法是使用 Object.create() 而不是 new:ncombo.wordpress.com/2013/07/11/… 请注意,此示例可能会造成混淆:本例中的“super”指的是飞行汽车,而 OOP 中的“super”通常指的是父原型/类。 我认为让 JavaScript 的原型继承难以理解的部分在于它将它与构造函数混合在一起。如果我们去掉构造函数和类的想法,我们只有对象。每个对象要么从无(null)继承,要么从另一个对象继承。这将更容易理解,而这正是 Object.create() 所做的。 How does JavaScript .prototype work?的可能重复 【参考方案1】:

要添加到Norbert Hartl's answer,SuperCar.prototype.constructor 不是必需的,但有些人将其用作获取对象(在本例中为 SuperCar 对象)构造函数的便捷方式。

从第一个例子开始,Car.call(this, name) 就在 SuperCar 构造函数中,因为当你这样做时:

var mySuperCar = new SuperCar("SuperCar");

这就是 JavaScript 的作用:

    一个新的空白对象被实例化。 新对象的内部原型设置为 Car。 SuperCar 构造函数运行。 返回完成的对象并在 mySuperCar 中设置。

请注意 JavaScript 没有为您调用 Car。就原型而言,您没有为 SuperCar 设置的任何属性或方法都将在 Car 中查找。有时这很好,例如SuperCar 没有 Drive 方法,但它可以共享 Car 的方法,因此所有 SuperCar 将使用相同的 Drive 方法。其他时候你不想分享,比如每辆超级跑车都有自己的名字。那么如何将每辆 SuperCar 的名称设置为自己的东西呢?您可以在 SuperCar 构造函数中设置 this.Name:

function SuperCar(name)
    this.Name = name;

这可行,但请稍等。我们不是在 Car 构造函数中做了完全相同的事情吗?不想重蹈覆辙。既然 Car 已经设置了名字,我们就叫它吧。

function SuperCar(name)
    this = Car(name);

哎呀,您永远不想更改特殊的 this 对象引用。还记得这4个步骤吗?抓住 JavaScript 提供给你的那个对象,因为它是保持 SuperCar 对象和 Car 之间宝贵的内部原型链接的唯一方法。那么我们如何设置Name,既不重复自己,又不丢弃我们新鲜的SuperCar对象JavaScript花了这么多特别的精力来为我们准备呢?

两件事。一:this的含义是灵活的。二:汽车是一种功能。可以调用 Car,而不是使用原始的、新鲜的实例化对象,而是使用例如 SuperCar 对象。这为我们提供了最终解决方案,这是您问题中第一个示例的一部分:

function SuperCar(name)
    Car.call(this, name);

作为一个函数,允许使用函数的call method 调用Car,这将Car 中this 的含义更改为我们正在构建的SuperCar 实例。快!现在每辆 SuperCar 都有自己的 Name 属性。

总结起来,SuperCar 构造函数中的Car.call(this, name) 为每个新的 SuperCar 对象提供了它自己唯一的 Name 属性,但不会复制 Car 中已有的代码。

一旦理解了原型,它们就不可怕了,但它们一点也不像经典的类/继承 OOP 模型。我写了一篇关于the prototypes concept in JavaScript 的文章。它是为使用 JavaScript 的游戏引擎编写的,但它与 Firefox 使用的 JavaScript 引擎相同,因此应该都是相关的。希望这会有所帮助。

【讨论】:

"SuperCar.prototype.constructor" 如果您希望 Object.getPrototypeOf 在旧版引擎中模拟工作,则需要 “Javascript 中的原型概念”的当前链接已损坏。还有其他地方可以阅读这篇文章吗? “原型保持原样,您没有为 SuperCar 设置的任何属性或方法都将在 Car 中查找”。如果不调用基础对象构造函数,我不明白派生对象如何从基础对象获取方法和属性。派生对象原型是否查看基础对象并添加派生对象没有的所有内容?还是基础对象原型具有基础对象的方法和属性?【参考方案2】:

这两个块的不同之处在于,在第一个示例中,Drive() 仅存在一次,而在第二种方法中,Drive() 将在每个实例中存在(每次执行 new Car() 时,都会创建函数 drive()再次)。或者不同的是,第一个使用原型存储函数,第二个使用构造函数。函数的查找是构造函数,然后是原型。因此,对于您查找Drive(),无论它是在构造函数中还是在原型中,它都会找到它。使用原型更有效,因为通常每种类型只需要一个函数。

javascript 中的new 调用会自动设置原型中的构造函数。如果要覆盖原型,则必须手动设置构造函数。

javascript 中的继承与super 完全不同。因此,如果您有一个子类,那么调用超级构造函数的唯一机会就是通过它的名称。

【讨论】:

【参考方案3】:

Norbert,您应该注意,您的第一个示例几乎就是 Douglas Crockford 所说的伪经典继承。需要注意的一点:

    您将调用 Car 构造函数两次,一次来自 SuperCar.prototype = new Car() 行,另一个来自“构造器窃取”行 Car.call(this...您可以创建一个辅助方法来继承原型相反,您的 Car 构造函数只需运行一次,从而提高设置效率。 SuperCar.prototype.constructor = SuperCar 行将允许您使用 instanceof 来标识构造函数。有些人希望其他人只是避免使用 instanceof 引用变量如:var arr = ['one','two'] 在超级(例如 Car)上定义时将被所有实例共享。这意味着所有实例都会显示 inst1.arr.push['three']、inst2.arr.push['four'] 等!本质上是您可能不想要的静态行为。 您的第二个块在构造函数中定义了 fly 方法。这意味着每次调用它时,都会创建一个“方法对象”。最好为方法使用原型!但是,如果您愿意,您可以将其保留在构造函数中 - 您只需要注意,因此您实际上只初始化原型文字一次(伪): if (SuperCar.prototype.myMethod != 'function')...then define你的原型文字。 'Why call Car.call(this, name)....':我没有时间仔细查看你的代码,所以我可能错了,但这通常是为了让每个实例都可以保持它自己的状态修复我上面描述的原型链的“静态”行为问题。

最后,我想提一下,我有几个 TDD JavaScript 继承代码示例在这里工作:TDD JavaScript Inheritance Code and Essay 我很想得到您的反馈,因为我希望改进它并保持开源。目标是帮助经典程序员快速上手 JavaScript,同时补充 Crockford 和 Zakas 书籍的学习。

【讨论】:

我之前遇到过第 (1) 点。我发现如果不使用 new 关键字而只使用 Superclass.call(this) (所以 SuperCar = Car.call(this); ; SuperCar.prototype = Car; )这似乎有效。有理由不这样做吗? (为死灵道歉) 来自经典 OOP 语言,我一直觉得奇怪需要创建一个new 实例只是 填充原型链 好书。您应该注意,github页面顶部的链接已损坏,当然,它可以在内容中找到。【参考方案4】:

我不是 100% 肯定,但我相信不同之处在于第二个示例只是将 Car 类的内容复制到 SuperCar 对象中,而第一个示例将 SuperCar 原型链接到 Car 类,以便运行时Car 类的更改也会影响 SuperCar 类。

【讨论】:

【参考方案5】:
function abc() 

为函数 abc 创建的原型方法和属性

abc.prototype.testProperty = 'Hi, I am prototype property';
abc.prototype.testMethod = function()  
   alert('Hi i am prototype method')

为函数 abc 创建新实例

var objx = new abc();

console.log(objx.testProperty); // will display Hi, I am prototype property
objx.testMethod();// alert Hi i am prototype method

var objy = new abc();

console.log(objy.testProperty); //will display Hi, I am prototype property
objy.testProperty = Hi, I am over-ridden prototype property

console.log(objy.testProperty); //will display Hi, I am over-ridden prototype property

http://astutejs.blogspot.in/2015/10/javascript-prototype-is-easy.html

【讨论】:

【参考方案6】:

这里有几个问题:

您能否解释一下以下代码块之间的区别。我进行了测试,两个块都可以工作。

第一个只创建一个Drive 函数,第二个创建其中两个:一个在myCar 上,另一个在mySuperCar 上。

以下代码在执行第一个或第二个块时会给出不同的结果:

myCar.Fly === mySuperCar.Fly // true only in the first case
Object.keys(myCar).includes("Fly") // true only in the second case
Object.keys(Car.prototype).length === 0 // true only in the second case

最佳做法是什么?为什么? 为什么作者使用prototype添加DriveFly方法,却没有在Car类和SuperCar类中声明this.Drive方法和this.Fly

最好在原型上定义方法,因为:

每个方法只定义一次 每个方法也可用于在未执行构造函数的情况下创建的实例(调用Object.create(Car.prototype) 时就是这种情况); 您可以检查在实例原型链的哪个级别定义了某个方法。

为什么SuperCar.prototype.constructor 需要重新设置为SuperCar?设置prototype 时是否覆盖constructor 属性?我注释掉了这一行,没有任何改变。

constructor 属性在设置 prototype 时不会被覆盖。但是new Car()的构造函数是Car,所以如果你把new Car()设置为SuperCar.prototype,那么显然SuperCar.prototype.constructor就是Car

只要不重新分配给prototype,就有一个不变性:Constructor.prototype.constructor === Constructor。例如,CarCar.prototype.constructor === Car 是这样,但ArrayObjectString、...等也是如此。

但是,如果您将不同的对象重新分配给 prototype,那么这种不变性就会被破坏。通常这不是问题(正如您所注意到的),但最好恢复它,因为它回答了“哪个构造函数在创建新实例时使用这个原型对象?”的问题一些代码可能会这样做这样的检查并依赖它。有关此类情况,请参阅"Why is it necessary to set the prototype constructor?"。

为什么在SuperCar 构造函数中调用Car.call(this, name);?当我这样做时,Car 的属性和方法不会被“继承”

var myCar = new Car("Car");

如果您不执行Car.call(this, name);,那么您的SuperCar 实例将没有名称属性。您当然可以决定只使用 this.name = name;,它只是复制 Car 构造函数中的代码,但在更复杂的情况下,重复这样的代码是不好的做法。

SuperCar 构造函数中调用new Car(name) 没有帮助,因为这将创建另一个 对象,而您确实需要扩展this 对象。通过不使用new(改为使用call),你实际上告诉Car函数不要作为构造函数运行(即创建一个新对象),而是使用你的对象而是传递给它。

时代变了

在现代版本的 JavaScript 中,您可以使用 super(name) 代替 Car.call(this, name)

function SuperCar(name) 
    super(name);

今天,您还将使用class 语法并编写问题的第一个代码块,如下所示:

class Car 
    constructor(name) 
        this.name = name;
    
    drive() 
        console.log(`My name is $this.name and I'm driving.`);
    


class SuperCar extends Car 
    constructor(name) 
        super(name);
    
    fly() 
        console.log(`My name is $this.name and I'm flying!`);
    


const myCar = new Car("Car");
myCar.drive();

const mySuperCar = new SuperCar("SuperCar");
mySuperCar.drive();
mySuperCar.fly();

请注意,您甚至不必提及 prototype 属性即可实现目标。 class ... extends 语法还负责将 prototype.constructor 属性设置为问题中的第一个块。

【讨论】:

以上是关于了解 JavaScript 中的原型继承的主要内容,如果未能解决你的问题,请参考以下文章

JavaScript 原型链

JavaScript 原型链

深入理解JavaScript系列:强大的原型和原型链

JavaScript中的原型与原型链

JavaScript继承基础讲解,原型链借用构造函数混合模式原型式继承寄生式继承寄生组合式继承

5.JavaScript原型链和继承详解