本篇文章会分别从ES5和ES6的角度上来学习JS的继承。
由于js不像java那样是真正面向对象的语言,js是基于对象的,它没有类的概念。所以,要想实现继承,可以用js的原型prototype机制或者用apply和call方法去实现。
首先讲一下对原型链的理解,原型链可以理解为我们人与人之间的继承关系,上代码:
var parent = {
money: 1000000,
};
var son = Object.create(parent);
var son_1 = Object.create(parent);
son.money // 1000000
son_1.money // 1000000
son === son_1 // false
son.money === parent.money // true
son_1.money === parent.money // true
son.__proto__ === parent // true
son_1.__proto__ === parent // true
上述代码,新建了一个parent对象,并且使用create方法基于parent新建了一个son对象,这就实现了js中一个最简单的js原型链继承。
怎么说呢?create方法实现了,son和parent的血缘关系(原型继承),为了方便理解,可以理解为son的proto指针指向了parent。
这个proto建立起来的继承关系就是我们常说的原型链。
这个有什么魔性的作用呢?那就是让我们访问son某个属性的时候,js会顺着这个链子从对象本身一直往上走,直到找到为止。就像刚才那个例子,找money属性,son没有,就按着原型链一直往上走,找到了parent的money属性。
同时,上面的代码中,我基于parent还新建了另一个son_1对象,根据输出结果我们可以发现,
son和son_1共用了这层继承关系,共用了它们原型的属性。
在面向对象的语言中,我们经常使用类来创建一个自定义对象。然而js中所有事物都是对象,那么用什么办法来创建自定义对象呢?我们可以通过构造函数的方式模拟实现类的功能。
我们看下面这个函数
function Person(name){
this.name = name || 'huihui';
};
Person(); // window.name='huihui'
var p = new Person(); // {name:'huihui'}
这个函数,如果直接调用(非严格模式下,如果严格模式下,会报错),执行结果则是将当前作用域内的this的属性name赋值;如果是以new Person(‘’) 方式调用,则会返回一个全新的对象。好,那么问题来了,new Person(‘’)操作,我们会认为是新建了某种类型的对象,这个过程到底发生了什么呢?
很简单,发生了下面的过程:
1、var obj = {}; // 新建一个空对象
2、Object.setPrototypeOf?
Object.setPrototypeOf(obj,Person.prototype)
:obj.__proto__ = Person.prototype; // 为新对象绑定原型链关系
3、Person.call(obj,...arguments) // 绑定新对象的this作用域
这边引出了一个新的东西【prototype】,在js中,任何一个函数对象都有这么个属性。我们可以简单的把prototype看做是一个模版,基于构造函数新创建的自定义对象都是这个模版(prototype)的一个拷贝 。prototype是一个对象,不是一个函数,它是一个constructor属性指向对应函数的的一个对象。
聊一聊这个prototype对象有个什么用。
首先,从上面代码我们可以看出,基于构造函数创建的对象的原型链(proto)始终指向这个构造函数的prototype对象,好了,这时候我们可以发现prototype的其中一个重要作用就是存放某一类对象的公用特征,让同一个构造函数构造出来的对象共同继承某些公用方法或者属性(放在这个构造函数的prototype对象里);
其次,我们会经常使用instanceof来判断某个对象是否是某类型的实例,比如 p instanceof Person,这个过程其实是根据Person.prototype 是否在p 的原型链中来判断的。
好了,刚才介绍了基于构造函数创建对象的过程。那么,我们来看看这类对象之间的继承怎么做。
看个例子:
function Animal(){
this.type = 'animal';
}
function Dog(){
this.name = 'wangwang';
}
Dog.prototype = Object.create(Animal.prototype);// 建立原型链关系
Dog.prototype.constructor = Dog;
var dog = new Dog();
dog.name // wangwang
dog.type // animal,继承下来的属性
上述代码很清晰的建立起了两个类(Animal\Dog)之间的继承关系。
这部分的代码其实就是纯粹地基于原型链的继承,但是,我们可以发现基于原型链继承存在2个缺点:
一是字面量重写原型会中断关系,原型上的引用类型属性一不小心就会被共享(原因见下面代码);
function Animal(){
this.type = 'animal';
this.foods = ['c','b'];
}
Animal.prototype.addFood = function(food){
this.foods.push(food);
}
function Dog(){
this.name = 'wangwang';
}
Dog.prototype = Object.create(Animal.prototype);// 建立原型链关系
Dog.prototype.constructor = Dog;
var dog = new Dog();
dog.name // wangwang
dog.type // animal,继承下来的属性
dog.addFood('e');
dog.foods // ['c','b','e'];
var a = new Animal();
a.foods // ['c','b','e'];
dog.type = 'dog';
dog // {type:'dog',name:'wangwang',__proto:...}
上面代码可以发现,我们调用子类的实例对象dog的addFood方法,把公用的foods属性都给改变了,这个是为啥呢?这是因为dog.addFood实际上就是执行了dog.prototype.addFood.call(dog,...args);dog中找不到foods属性,会直接原型链顺着查找,找到了再修改掉了,因为原型对象的属性是共享的,那么自然就影响到了所有的。
而这时候可能会有个疑问,那为啥简单类型变量不会受到影响呢?因为简单类型就是直接赋值的啊,就如dog.type = 'dog';这句语句只会直接就在当前实例上加了对应的属性,不会上升到原型对象当中去。(本质上来讲,其实就是如果不需要访问这个属性而直接赋值操作的,就不会影响到原型上的属性,如果是需要访问这个属性然后再对这个属性进行操作的操作,就会影响到原型上对应的这个属性)。
二是子类型还无法给超类型传递参数。
这个时候,为了解决这个问题我们就聊到了构造函数之间的类式继承。看代码:
function Animal(type){
this.type =type;
this.foods = ['c','b'];
}
function Dog(type){
Animal.call(this,type);
this.name = 'wangwang';
}
var d = new Dog('dog');
d // {foods:['c','b'],name:'wangwang',type:'dog'}
上面代码通过超类函数的call调用,完美实现了继承。但是,但是,我们发现这并没有复用任何东西,没有原型继承,何谈复用?
那么,这时候,我们便可以将这2种方式结合起来:
function Animal(type){
this.type =type;
this.foods = ['c','b'];
}
Animal.prototype.addFood = function(food){
this.foods.push(food);
}
function Dog(type){
Animal.call(this,type);
}
Dog.prototype = Object.create(Animal.prototype);// 建立原型链关系
Dog.prototype.constructor = Dog;
var d = new Dog('dog');
d.addFood('e');
d // {type:'dog',foods:['c','b','e'],__proto__}
这种方法叫做:组合式继承,它是比较常用的一种继承方法,其背后的思路是 使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又保证每个实例都有它自己的属性。
上面就是对ES5中的继承实现方式做了自己的认识与理解。接下来介绍一下ES6中对继承做了哪些变化亦或是改善呢?
从上面可以看出,js当中实现继承最佳实践就是组合式继承的方法,为了迎合 java这类后端语言的写法,ES6中引入了class 语法,可以通过extends关键字来实现类之间的继承。下面我们来看看是咋回事儿。
class Animal {
constructor(type){
this.type = type;
}
addFood(food){
this.foods.push(food);
}
}
class Dog extends Animal{
constructor(type){
super(type);
this.name = 'dog';
}
}
typeof Animal //function
typeof Dog //function
下面是经过babel转译后的代码,我们仔细瞧一瞧:
'use strict';
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var Animal = function () {
function Animal(type) {
_classCallCheck(this, Animal);
this.type = type;
}
_createClass(Animal, [{
key: 'addFood',
value: function addFood(food) {
this.foods.push(food);
}
}]);
return Animal;
}();
var Dog = function (_Animal) {
_inherits(Dog, _Animal);
function Dog(type) {
_classCallCheck(this, Dog);
var _this = _possibleConstructorReturn(this, (Dog.__proto__ || Object.getPrototypeOf(Dog)).call(this, type));
_this.name = 'dog';
return _this;
}
return Dog;
}(Animal);
首先,分析class ,我们发现class就是个函数语法糖,本质上是一个立即执行表达式,返回一个跟ES5一样的构造函数。然后我们观察下_createClass这个方法,这个方法主要就是做了一件事情,就是把addFood 方法定义到Animal的prototype上。这部分与ES5无异。得出的结论就是ES6中的class本质上跟前面提到的构造函数一样,在class里面定义的方法直接放在prototype上。
接下来,我们看下ES6中的继承是怎么实现的。我们首先关注下_inherits这个方法,
subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
可以看出这个方法里面做了两件事情:1、建立subClass.prototype和superClass.prototype的继承关系;2、建立subClass和superClass的继承关系
然后,我们来看一下_possibleConstructorReturn这个方法,这个方法,其实就是对应这super,效果等同于Animal.call(this,type);
不过这面可以注意个小细节,子类中的this是通过_possibleConstructorReturn返回的,这里要铭记。与ES5中的差别再于,ES5中是通过call直接将父类的属性继承到子类来,this一直都是子类的this,而ES6中虽然也类似,但是子类中最终返回生成的this是通过_possibleConstructorReturn(super)返回的。
总结下:
JavaScript中实现继承的方式灵活多变。
其中,ES5中最推荐使用组合式继承方式来实现,而ES6中的Class语法糖本质上也是基于组合式继承实现的。不过ES6当中简化了写法,与传统的面向对象语言写法更为接近。
好了,此文结束。
=============
补充:
1、object.create(_proto,props)怎么实现的?通过中间空函数,f(),f.prototype=proto;return new f();
2、为什么要做constructor修正?为了在构造函数外部,通过实例的constructor可以更改prototype;
3、 为什么es6 中class之间和class.prototype之间都要原型继承绑定起来,因为static属性也需要继承,这是挂在class级别的。