JS核心基础知识总结(一)——原型和继承

380 阅读7分钟

原型/原型链

JS是一门基于原型实现继承的语言。那么,什么是原型?基于原型实现的继承又是怎么一回事?

原型(prototype),根据字面意思,可以理解为一件事物的模板。比如iPhone的原型是以前只能打电话、发短信的功能机,这表示,iPhone也拥有打电话、发短信的功能(继承),但是相比它的原型又拥有了更多功能(可以扩展更多功能)。这种关系有点类似于Java中的子类与父类的关系(Java是基于类的继承,而Javascript是基于原型)。

在JS中,每一个函数(Function)都有一个prototype属性,这个prototype对象有一个属性constructor指向这个构造函数:

function Person() {
​
}
Person.prototype.constructor === Person // true

另外,每个JS对象还有一个隐藏属性proto,指向它的构造函数的原型对象(prototype),即:

var person = new Person();
person._proto_ === Person.prototype; // true

对象实例、构造函数和原型的关系可以表示成下图:

prototype3.png

更进一步的,我们知道Person.prototype也是一个对象,那么它也拥有proto属性,指向Person.prototype构造函数的原型对象,这个原型对象又有proto属性…...通过这样一个实例对象的proto指向构造函数prototype、prototype对象又拥有proto属性的指向循环,我们就可以建立起一条原型链。

person._proto_ => Person.prototype
Person.prototype._proto_ => Object.prototype
Object.prototype._proto_ === null // true

注意,所有原型链的终点是Object.prototype.proto,这个对象没有对应构造函数的原型了,所以为null。

基于原型对象(prototype)和原型链,我们就可以实现继承。

继承

按照《Javascript高级程序设计》中所写,在JS中实现继承大致有6种方式:

一、借用构造函数

子类型要如何拥有父类型的属性呢?如果父类的属性都是通过构造函数定义的,那么最简单粗暴的方法当然是直接在子类的构造函数中调用父类的构造函数,此时子类就具有了父类的所有属性。

function SuperType(name) {
    this.name = name;
    this.colors = ['pink', 'blue', 'green'];
}
function SubType(name) {
    SuperType.call(this, name);
}
let instance1 = new SubType('Yvette');
instance1.colors.push('yellow');
console.log(instance1.colors);//['pink', 'blue', 'green', yellow]

let instance2 = new SubType('Jack');
console.log(instance2.colors); //['pink', 'blue', 'green']

此时我们已经让SubType拥有了SuperType的属性。

问题来了,如果我们想让SubType能够直接复用/继承SuperType的方法,这种继承的方式就无法实现了。因此这种方式是有缺陷的,在实践中也不可能单纯用它实现继承。

此时就需要我们前面铺垫了很久的原型对象出场了。

二、原型链继承

在JS中读取一个实例属性时,首先会在该实例上读取,如果读取不到需要的属性,则会在实例的原型上搜索。那么如果我们让A类型的原型指向另一个B类型,在A类型上读取不到的属性就可以接着去A类型的原型(也就是B类型)去读取,更进一步的会去搜过B类型的原型,一直到原型链的末端。这就是原型链继承

因此我们想要SubType继承SuperType,只需要让前者的prototype指向后者的实例就行了。

function SuperType() {
    this.name = 'Yvette';
    this.colors = ['pink', 'blue', 'green'];
}
SuperType.prototype.getName = function () {
    return this.name;
}
function SubType() {
    this.age = 22;
}
SubType.prototype = new SuperType();
SubType.prototype.getAge = function() {
    return this.age;
}
SubType.prototype.constructor = SubType;
let instance1 = new SubType();
instance1.colors.push('yellow');
console.log(instance1.getName()); //'Yvette'
console.log(instance1.colors);//[ 'pink', 'blue', 'green', 'yellow' ]

let instance2 = new SubType();
console.log(instance2.colors);//[ 'pink', 'blue', 'green', 'yellow' ] ,注意这里instance2的属性和instanse1共享了

此时我们不仅可以继承父类的属性,函数也获得了继承。

但是这样实现继承的缺陷在于——所有实例的属性都共享了。这显然也是不可接受的,我们想要每个实例能有自己的属性,只用继承同样的函数就行了。此时我们会想到:借用构造函数的继承可以使每个实例拥有自己的属性,而原型链继承可以继承父类的函数,能不能把二者的优点集合起来呢?

三、组合继承(借用构造函数 + 原型链继承)

我们可以使用构造函数来实现对属性的继承,再用原型链实现对方法的继承,综合二者各自的优点就能实现一个功能完善的继承了。

function SuperType(name) {
    this.name = name;
    this.colors = ['pink', 'blue', 'green'];
}
SuperType.prototype.sayName = function () {
    console.log(this.name);
}
function SuberType(name, age) {
    SuperType.call(this, name);
    this.age = age;
}
SuberType.prototype = new SuperType();
SuberType.prototype.constructor = SuberType;
SuberType.prototype.sayAge = function () {
    console.log(this.age);
}
let instance1 = new SuberType('Yvette', 20);
instance1.colors.push('yellow');
console.log(instance1.colors); //[ 'pink', 'blue', 'green', 'yellow' ]
instance1.sayName(); //Yvette

let instance2 = new SuberType('Jack', 22);
console.log(instance2.colors); //[ 'pink', 'blue', 'green' ]
instance2.sayName();//Jack

如上所示,每个子类的实例都拥有了自己的属性,并且都继承了父类的sayName方法(注意父类的方法是定义在prototype对象上的)。

这个方案已经基本能在实践中使用了,但是它还是有一个小问题——父类的构造函数调用了2次。一次是在子类构造函数中调用父类构造函数,另一次是在把子类的原型用父类的一个实例赋值时。理论上还是有优化的空间。

现在我们再来看看另一种继承的实现,也是一种很有名的实现。

四、原型式继承

借助原型可以基于已有的对象创建新对象,不必因此创建新类型。我们来看一个函数:

function object(o) {
    function F() { }
    F.prototype = o;
    return new F();
}

object()函数内部创建了一个临时类型F,然后将传入的对象作为它的原型并返回了一个实例。本质上相当于对传入的对象进行了一次浅拷贝。实践中我们不需要手写这个函数,而是可以直接使用Object.create()来实现同样的功能。

在没有必要创建单独的构造函数来实现一些定制功能,只是需要让两个对象的行为保持一致时,我们可以使用这样的原型式继承

当然它也具有原型链继承的缺点,无法为每个实例创建自己独有的属性。

五、寄生式继承

寄生式继承是基于原型式继承的,只不过在创建对象的过程中以某种方式对它进行了一些增强。

function createAnother(original) {
    var clone = object(original);// 通过调用函数创建一个新对象
    clone.sayHi = function () {// 以某种方式增强这个对象
        console.log('hi');
    };
    return clone;// 返回这个对象
}
var person = {
    name: 'Yvette',
    hobbies: ['reading', 'photography']
};

var person2 = createAnother(person);
person2.sayHi(); //hi

但是依然没有解决原型式继承的问题。

根据前面提到的组合继承的思路,我们再一次思考能否使用组合多种方案来解决问题。

六、寄生组合式继承

通过名字我们大概能猜到,这种方式是组合了寄生式继承、借用构造函数的方式。

首先我们通过寄生式继承实现方法的继承:

function inherit(superType, subType) {
  var o = object(superType.prototype);
  o.constructor = subType; // 注意这一步——维持constructor是subType,因为上一行将prototype设为了superType
  subType.prototype = o;
}

接着我们补充构造函数来实现对实例属性的继承:

function SuperType(name) {
  this.name = name;
  this.colors = ['red', 'blue', 'yellow'];
}

SuperType.prototype.sayName = function () {
  alert(this.name)
}

function SubType(name, age) {
  SuperType.call(this, name); //只调用了一次构造函数
  
  this.age = age;
}

inherit(SuperType, SubType);

......

现在我们实现了一个较为完善的继承:

它既能实现方法的复用,又能保证每个实例拥有各自的属性,同时它只调用了一次构造函数,因为我们没有像原型链继承一样创建额外的一个父类型的实例给子类型的原型。

这也就是我们实践中可以使用的一种继承的实现方式。

ES 6的继承

ES 6中新增了classextends关键字,可以让我们在JS中实现其他基于类的继承的语言的继承写法。

class SubType extends SuperType {
  
}

当然虽然我们可以用如此简洁的写法完成继承,实际上底层实现仍然是基于原型实现的,只不过Babel帮我们完成了这部分转译工作。而转译出的代码实质上和我们上面所写的寄生组合式继承的代码是大同小异的。