理解 JavaScript 中的原型继承

我是 JavaScript 面向对象编程的新手。你能解释一下以下代码块之间的区别吗?我测试过了,两块都能用。什么是最佳实践,为什么?

第一个街区:

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");
46533 次浏览

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

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

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

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

要添加到 Norbert Hartl 的回答中,不需要 SuperCar.Prototype.structor,但有些人将其用作获取对象(本例中为 SuperCar 对象)构造函数的便捷方法。

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

var mySuperCar = new SuperCar("SuperCar");

这就是 JavaScript 的功能:

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

注意 JavaScript 没有为您调用 Car。原型是因为他们是,任何属性或方法,你不设置自己的超级汽车将在汽车中查找。有时这是好事,例如 SuperCar 没有 Drive 方法,但是它可以共享 Car 的 Drive 方法,所以所有的 SuperCar 都会使用相同的 Drive 方法。其他时候你不想分享,比如每辆超级跑车都有自己的名字。那么,如何去设置每个超级跑车的名称,它自己的事情?你可以设置这个。SuperCar 构造函数中的名称:

function SuperCar(name){
this.Name = name;
}

这个可以,但是等一下。我们在 Car 构造函数中不是做了完全相同的事情吗?我们不想重复。既然 Car 已经设置了名字,我们就叫它吧。

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

哎呀,您永远不会想要更改特殊的 this对象引用。还记得那四个步骤吗?紧紧抓住 JavaScript 给你的那个对象,因为这是保持 SuperCar 对象和 Car 之间珍贵的内部原型链接的唯一方法。因此,我们如何设置名称,而不重复自己,并没有扔掉我们的新的 SuperCar 对象 JavaScript 花了这么多的特殊努力为我们准备?

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

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

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

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

一旦理解了原型,就不会觉得可怕,但它们与经典的类/继承 OOP 模型完全不同。我写了一篇关于 JavaScript 中的原型概念的文章。它是为一个使用 JavaScript 的游戏引擎编写的,但它与 Firefox 使用的 JavaScript 引擎相同,因此它应该都是相关的。希望这个能帮上忙。

诺伯特,你应该注意到你的第一个例子基本上就是道格拉斯·克罗克福特所说的伪古典遗传。需要注意的是:

  1. 您将调用 Car 构造函数两次,一次来自 SuperCar.model = new Car ()行,另一次来自“构造函数窃取”行 Car.call (这... 您可以创建一个 helper 方法来继承原型,而且您的 Car 构造函数只需运行一次就可以使设置更有效。
  2. Structor = SuperCar 行将允许您使用 instanceof 来标识构造函数。有些人希望这样做,其他人只是避免使用 instanceof
  3. 当在 super (例如 Car)上定义时,诸如: var arr = [‘ one’,‘ two’]之类的引用变量将被所有实例共享。这意味着 inst1.arr.push [‘ three’]、 inst2.arr.push [‘ four’]等等,将显示所有实例!本质上,静态行为,你可能不想要。
  4. 第二个块在构造函数中定义 fly 方法。这意味着每次调用它时,都会创建一个“方法对象”。最好使用方法的原型!然而,如果你愿意的话,你可以把它保留在构造函数中——你只需要保护它,这样你只需要初始化原型文字一次(伪) : 如果(SuperCar.Prototype.myMethod!= ‘ function’) ... 然后定义原型文字。
  5. 为什么要调用 Car.call (this,name) ... ...”: 我没有时间仔细查看你的代码,所以我可能是错的,但这通常是为了让每个实例都能保持自己的状态,以修复我上面描述的原型链接的“静态”行为问题。

最后,我想提到的是,我有几个 TDD JavaScript 继承代码的例子,在这里工作: JavaScript 继承代码和文章我希望得到您的反馈,因为我希望改进它,并保持开源。我们的目标是帮助经典的程序员快速掌握 JavaScript,同时补充克罗克福德和扎卡斯书籍的研究。

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

这里有几个问题:

你能解释一下下面代码块之间的区别吗。

第一个只创建一个 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类中声明为 this.Drive方法,在 SuperCar类中声明为 this.Fly方法?

更好的做法是在原型上定义方法,因为:

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

为什么 SuperCar.prototype.constructor需要设置回 SuperCar?设置 prototype时是否重写 constructor属性?我把这一行注释掉了,什么也没有改变。

设置 prototype时不重写 constructor属性。但是 new Car()的构造函数是 Car,所以如果将 new Car()设置为 SuperCar.prototype,那么显然 SuperCar.prototype.constructorCar

只要不重新分配到 prototype,就有一个不变性: Constructor.prototype.constructor === Constructor。例如,这对于 Car: Car.prototype.constructor === Car是正确的,但对于 ArrayObjectString等同样是正确的。

但是如果你重新分配一个不同的对象到 prototype,这个不变性就被打破了。通常这不是一个问题(正如您已经注意到的) ,但是最好还原它,因为它回答了问题 “哪个构造函数在创建新实例时使用这个原型对象?”有些代码可能会执行这样的检查并依赖于它。这种情况见 “为什么需要设置原型构造函数?”

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

var myCar = new Car("Car");

如果您不执行 Car.call(this, name);,那么您的 SuperCar实例将没有 name 属性。当然,您可以决定只执行 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属性,就像您的问题中的第一个块所做的那样。