JavaScript实现继承的几种方法

190 阅读7分钟

前言

相信对于H5前端开发的同学而言,刚入门的时候JavaScript的继承一直是一件比较懵的事情。因为JavaScript在es6出现之前其实是没有原生支持继承的,连类的概念都没有,到了es6之后才有了class的概念。在这之前大家都是用JavaScript原型来hack一些继承的方式的。接下来我们就简单地过一下这些hack的方法到底是怎么样的,有什么优劣的地方。

一、理解原型

原型的思想是复制。关于原型方面的思考和解说,主要是参考我的另外一篇文章周爱民老师关于面向对象的解说

二、JavaScript的原型设计

在JavaScript里面,每一个函数都会有一个对象叫做prototype,需要注意的是这个对象只有函数才会有,对象实例是没有的。prototype其实就是这个函数的原型,prototype里面有一个constructor属性,指向函数本身。也就是假设a是一个function,那么a.prototype.constructor === a。通常prototype里面放置的是可以被继承的函数或属性。

而对于每一个对象,对象有一个隐藏属性叫做__proto__,该属性指向的是构造这个对象的函数的原型。比如let a = new A();那么 a.__ proto__=== A.prototype。

三、原型链

有对象实例的a.__ proto__指向的是函数A的prototype。因此a可以使用函数A的原型上的方法和属性。那么同样的A的prototype其实也是一个对象,他也有一个__ proto__指向构造他的函数的原型。基于这样的机制,那么如果我们修改函数A的prototype,使得他指向另外一个函数实例,那么就可以实现继续继承另一个对象了,这就是原型链的由来。代码大概如下:

function SuperType(){ 
 this.property = true; 
}

SuperType.prototype.getSuperValue = function(){ 
 return this.property; 
}; 
function SubType(){ 
 this.subproperty = false; 
} 
//继承了 SuperType 
SubType.prototype = new SuperType(); 
SubType.prototype.getSubValue = function (){ 
 return this.subproperty; 
}; 
var instance = new SubType(); 
alert(instance.getSuperValue()); //true

在上面的例子中SubType的prototype被指向到了一个父类的对象,于是SubType的实例就可以继承SuperType的属性和方法了。

四、几种常见的继承方式

JavaScript一开始只有对象,并没有继承,只不过因为原型的特殊作用,可以利用原型来实现继承。

  • 1.原型链继承 上面那种方式就是原型链继承。原型链虽然是实现继承的强大工具,但它也有问题。主要问题出现在原型中包含引用值的时候。原型中包含的引用值会在所有实例间共享。
function SuperType() {
 this.colors = ["red", "blue", "green"];
}
function SubType() {}
// 继承SuperType
SubType.prototype = new SuperType();
let instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors); // "red,blue,green,black"
let instance2 = new SubType();
console.log(instance2.colors); // "red,blue,green,black"

如上述代码所示,当我们修改了instance1的引用属性时,instance2的对应属性也同样被改变了。(因为他们其实是同一个colors,就是new SuperType产生的那个)

2.构造函数继承

为了解决上面的问题,又引出了一种构造函数继承方法,这种方法的做法是在子类的构造函数内执行一遍父类的构造函数,从而达到各自拥有各自属性的目的。

function SuperType() {
 this.colors = ["red", "blue", "green"];
}

SuperType.prototype.logColors = function() {
 console.log(this.colors);
};

function SubType() {
 // 继承SuperType
 SuperType.call(this);
}
let instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors); // "red,blue,green,black"
let instance2 = new SubType();
console.log(instance2.colors); // "red,blue,green"

通过使用call()(或apply())方法,SuperType构造函数在为SubType的实例创建的新对象的上下文中执行了。这相当于新的SubType对象上运行了SuperType()函数中的所有初始化代码。结果就是每个实例都会有自己的colors属性。相比于使用原型链,盗用构造函数的一个优点就是可以在子类构造函数中向父类构造函数传参。
但是构造函数方法同样也有弊端,那就是函数无法继承。由于父类函数是定义在prototype上的,所以构造器在执行的时候并不会去复制prototype上的函数。问题其实也可以解决,那就是在父类里面函数也定义在构造器中。如下:

function SuperType() {
 this.colors = ["red", "blue", "green"];
 this.logColors = function() {
 	console.log(this.colors);
	};
}

这样子类就拥有了父类的函数了,但问题又来了,这样子父类的函数就无法共用,因为这样的做法就等于每次new SuperType的时候都初始化了一次logColors函数。

3.组合继承

由于原型继承和构造函数继承的方式都有不同的弊端。那么就诞生了一种把两种继承方式合并的方式,叫组合继承。组合继承的思路是使用原型链继承原型上的属性和方法,而通过构造函数继承实例属性。这样既可以把方法定义在原型上以实现重用,又可以让每个实例都有自己的属性

function SuperType(name){
 this.name = name;
 this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function() {
 console.log(this.name);
};
function SubType(name, age){
 // 继承属性
 SuperType.call(this, name);
 this.age = age;
}
// 继承方法
SubType.prototype = new SuperType();
SubType.prototype.sayAge = function() {
 console.log(this.age);
};
let instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
console.log(instance1.colors); // "red,blue,green,black"
instance1.sayName(); // "Nicholas";
instance1.sayAge(); // 29
let instance2 = new SubType("Greg", 27);
console.log(instance2.colors); // "red,blue,green"
instance2.sayName(); // "Greg";
instance2.sayAge(); // 27

组合继承弥补了原型链和盗用构造函数的不足,是JavaScript中使用最多的继承模式。而且组合继承也保留了instanceof操作符和isPrototypeOf()方法识别合成对象的能力。

4.寄生组合继承

组合继承其实也存在问题,最大的问题是父类构造函数始终会被调用两次:一次在是创建子类原型时调用,另一次是在子类构造函数中调用。本质上,子类原型最终是要包含超类对象的所有实例属性,子类构造函数只要在执行时重写自己的原型就行了。于是就又有了一种继承方法,叫做寄生式组合继承。寄生式组合继承通过盗用构造函数继承属性,但使用混合式原型链继承方法。基本思路是不通过调用父类构造函数给子类原型赋值,而是通过 object()函数取得父类原型的一个副本。其代码如下:

function inheritPrototype(subType, superType) {
 let prototype = object(superType.prototype); // 创建对象
 prototype.constructor = subType; // 增强对象
 subType.prototype = prototype; // 赋值对象
}

这个inheritPrototype()函数实现了寄生式组合继承的核心逻辑。这个函数接收两个参数:子类构造函数和父类构造函数。在这个函数内部,第一步是创建父类原型的一个副本。然后,给返回的prototype对象设置constructor属性,解决由于重写原型导致默认constructor丢失的问题。最后将新创建的对象赋值给子类型的原型。调用inheritPrototype()就可以实现前面例子中的子类型原型赋值:

function SuperType(name) {
 this.name = name;
 this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function() {
 console.log(this.name);
};
function SubType(name, age) {
 SuperType.call(this, name);
 this.age = age;
}
inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function() {
 console.log(this.age);
};

这里只调用了一次SuperType构造函数,避免了SubType.prototype上不必要也用不到的属性,因此可以说这个例子的效率更高。而且,原型链仍然保持不变,因此instanceof操作符和isPrototypeOf()方法正常有效。寄生式组合继承可以算是引用类型继承的最佳模式。如果大家有看过es6 的class继承编译后的代码的话,其实会发现babel编译class继承产生的代码也是这种方式。

图片名称

结语

以上就是JavaScript利用es5实现继承的几种方式了。虽然现在有了es6的class,并且各大浏览器基本上都已经支持了es6的写法,现在可能已经很少需要这么写继承了,但是这不妨碍我们去了解一下以前的继承实现方法,这也是理解JavaScript原理的一种途径。好了,关于JavaScript的继承就写到这里,谢谢大家。