首页 > 编程学习 > 理解基于原型链的JS继承,什么是prototype,什么是constructor?

理解基于原型链的JS继承,什么是prototype,什么是constructor?

在ES5中,与其他大部分面向对象语言(如Java,C#)通过类来创建实例或者是继承不同,JavaScript的继承是通过直接调用构造函数来创建实例、prototype来实现继承的。

构造函数的缺点以及prototype的使用

在JavaScript中,我们通过new关键字调用构造函数来创建实例,实例的属性以及方法都通过this关键字被定义在构造函数的内部。但是这有一个缺点,多个实例之间无法共享属性/方法,每次创建实例的时候,内部的属性和方法都会完整的创建一次。而很多时候方法的行为和目的是一样的,重复创建会造成资源的浪费。通过以下代码来理解这个概念:

function Dog(name){this.name = name;this.bark = function(){console.log('汪!')}
}var dog1 = new Dog('dog1');
var dog2 = new Dog('dog2');
dog1.bark === dog2.bark //false

以上代码可以看出,bark行为并没有任何差异,但依旧在每次初始化实例时创建了一个新的,没有必要。

那么解决这个的办法就是prototype属性的使用了。JavaScript规定的是每个函数都有一个prototype属性。普通函数的prototype没有什么用,但是构造函数的prototype属性会自动的成为所有实例对象的原型。如果我们将方法和属性定义在prototype上,那么就可以被它所有的实例对象所共享。试着将上面的代码改写一下。

function Dog(name){this.name = name;}
Dog.prototype.bark = function(){console.log('汪!我是', this.name)};var dog1 = new Dog('dog1');
var dog2 = new Dog('dog2');dog1.bark === dog2.bark //true

可以看到,即使在构造函数内没有定义bark方法,但是通过往Dog的原型对象上添加,它的实例对象依旧可以直接使用,且共享bark方法,没有造成资源的浪费。

如果实例对象自身有一个和原型对象相同的属性呢?那么根据JavaScript的规定,优先使用自身的属性,而不会再使用原型对象的同名属性。

原型链

还是基于上面的例子,让我们输出以下代码。

typeof Dog.prototype //object

可以看到prototype对象本身也是一个对象,那么它自己也会有一个原型对象。如此一来便形成了一个原型链(prototype chain)。这么一环一环的向上追溯后,所有对象的原型都可以上溯到Object.prototype。这也是为什么所有对象都有toStringtoValue方法——从Object.prototype继承而来。

而对于Object.prototype,它的原型对象是NULL,也就是所有原型链的终点。

在读取某个实例对象的属性或者方法时,也是按照原型链的顺序,从最底层的实例到最顶层的Object.prototype一个个去寻找,如果最后仍然找不到,则返回undefined。注意,这意味着属性位于越上层,那么因为遍历原型链所产生的性能影响就越大。

prototype的constructor属性

prototype默认会有一个constructor属性,指向prototype所在的构造函数。

Dog.prototype.constructor === Dog //true
dog1.constructor = Dog //true

constructor的作用主要明确实例对象到底是由哪一个构造函数产生的。而且我们也可以通过它,用一个实例对象来创建另外一个实例对象

dog3 = new dog1.constructor('dog3') ;
dog3 instanceof Dog //true

由于constructor表达了原型对象与构造函数的关系,那么我们在修改原型对象时,也要记得修改constructor属性,防止引用时出错以及instanceof失真(instanceof运算符检测右边函数的prototype属性是否在在左边对象的原型链上)。让我们来接着看例子:

dog3 instanceof Dog //true
dog3.constructor === Dog //true

这儿没什么问题。但是如果我们想对Dog的原型做出修改,丰富它的方法和属性,如下:

Dog.prototype = {
bark : function(){console.log('汪!我是', this.name)},
run :  function(){console.log('I am running')},
sit :  function(){console.log('I am sitting')}
}var new_dog = new Dog('new dog');
dog1.constructor === Dog //true
new_dog.constructor === Dog //false
new_dog.constructor === Object //true
new_dog instanceof Dog //true

可以看到,和dog1不一样,new_dogconstructor属性已经不再指向Dog了,而是指向了一个普通的对象。但是通过instanceofDog判断的结果仍然为True,这就产生了失真。

为了避免这种情况出现,我们有如下两种办法。1、在修改原型对象的时候手动声明constructor属性。2、在原有的原型对象上添加方法而不是直接替换。

//方法一
Dog.prototype = {
constructor: Dog,
bark : function(){console.log('汪!我是', this.name)},
run :  function(){console.log('I am running')},
sit :  function(){console.log('I am sitting')}
}//方法二
Dog.prototype.run = function(){console.log('I am running')};
Dog.prototype.sit = function(){console.log('I am sitting')};

继承

在讲完了这些以后我们就可以开始看在JavaScript中想要通过原型链继承要如何操作了。继承的目的就是子类在拥有父类属性和方法的基础上,还能拥有自己的一些额外方法。所以我们这里需要三步:

1、在子类构造函数中调用父类的构造函数,这样才能使得子类拥有父类的实例属性。

function Animal(name,age){
this.name = name;
this.age = age;
}
Animal.prototype.eat = function(){console.log("I am eating!")};function Cat(name, age, color){
Animal.call(this, name, age);
this.color = color;
}

2、让子类的原型对象指向父类的原型对象,使得子类可以继承父类的原型属性。注意这里要通过Object.create方法来创造一个父类的prototype原型对象,如果直接将父类的原型对象直接赋值给子类的原型对象,那么后续对子类的原型对象修改也会应用到父类的原型对象上!

Cat.prototype = Object.create(Animal.prototype);

3、让子类原型对象的constructor属性等于子类自己的构造函数,防止instanceof方法失真以及原型链紊乱。

Cat.prototype.constructor =  Cat;

以上三步完成后,一个继承了Animal父类的Cat类就成功创建了。

参考文章

本文主要基于阮一峰老师《JavaScript教程》中对象的继承一节,梳理和整理了一下自己认为是关键的部分。

https://wangdoc.com/javascript/oop/prototype.html


本文链接:https://www.ngui.cc/el/414782.html
Copyright © 2010-2022 ngui.cc 版权所有 |关于我们| 联系方式| 豫B2-20100000