阅读 1739

图解JavaScript 继承

来说说这个继承

可能有很多人知道实现继承的方法,却对实现继承的原理不明所以。就像我,在不理解实现原理的情况下,就记不住并且不能正确使用这部分知识。一做题或者一面试,就会各种不坚定,各种懵逼树上懵逼果懵逼树下你和我。后来发现,把这继承画一画,对学习这方面的知识帮助很大。于是想记录一下。(吸收了各路知识,转化成了自己的一套理解,如果有理解不到位或者有偏差的地方,还请各位指正)

说到继承,必然离不开原型和原型链,如果对这方面知识不太了解,推荐读一下这篇文章 ,文章简短易懂。

继承呢,我认为核心就是两点:1、构造函数继承属性 2、原型链继承方法

ES5继承

构造函数、原型和实例的关系:

每一个构造函数(函数对象)都有一个prototype属性,指向函数的原型对象;每一个原型对象都有一个constructor属性,指向构造函数;每一个实例都有一个__proto__属性,指向构造函数的原型对象

1、原型链继承

实现本质:重写原型对象,代之以一个新类型的实例

function SuperType(){
    this.property = true;
}
SuperType.prototype.getSuperValue = function(){
    return this.property;
}
function SubType(){
    this.subproperty = false;
}

//继承了SuperType。重写了SubType原型,让它等于superType的实例(作为父类的实例,这样就拥有的父类的属性和方法), 实现了原型链继承
SubType.prototype = new SuperType(); 
SubType.prototype.getSubValue =function(){
    return this.subproperty;
}
var instance = new SubType();
alert(instance.getSuperValue());//true

复制代码

关键代码:SubType.prototype = new SuperType(); 通过这行代码实现了

A:重写了SubType原型,使子类原型与子类断开了默认的连接(当然了,新的子类原型的constructor指向父类原型);子类原型是父类的实例,使得子类原型的__proto__指向父类原型。(因为实例的__proto__指向构造函数原型对象)

B:通过子类原型的__proto__指向父类原型这条链接,子类可以沿着原型链访问到父类的getSuperValue的方法(子类继承了父类的方法)

C:因为子类原型是父类的实例,通过父类的构造函数,子类原型继承了父类的属性(子类继承了父类的属性)

存在的问题:

1、包含引用类型值的原型:父类的实例属性变成了子类的原型属性(property),原型属性会被子类的所有实例所共享。如果这个值是基本类型的话,没什么问题,但如果是引用类型(数组啊,是堆存储,同一个内存地址引用),如果实例1修改了该属性(比如往数组里push一个值,同样改变的是原型里的引用类型属性的值),实例2的该属性的值也会发生改变,后来新创建的实例也会拿到最新的引用类型的值。

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

SubType.prototype = new SuperType();
var instance1 = new SubType();
var instance2 = new SubType();
instance1.color.push('skyblue');
console.log(instance1.colors) //['red','blue','pink','skublue']
console.log(instance2.colors) //['red','blue','pink','skublue']
var instance3 = new SubType();
console.log(instance3.colors) //['red','blue','pink','skublue']
复制代码

2、在创建子类实例时,不能向父类的构造函数传参。

由于这些问题,实践中是很少会单独使用原型链继承的

构造函数继承

function SuperType(name){ 
    this.name  =  name;
    this.colors = ['red','blue','green'];
}
function SubType(){
    //继承了SuperType(继承了属性)
    //这里的优于原型链继承的是子类构造函数可以向父类构造函数传参了
    SuperType.call(this,'boom shakalaka');//函数只是在特定环境执行代码的对象,因此通过使用apply或call方法就可以在新对象上执行构造函数
    //这一步 就相当于把父类构造函数的代码在这里运行了一遍
    //this.name = 'boom shakalaka';
    //this.colors = ['red','blue','green'];
}
var instance1 = new SubType();
instance1.colors.push('black')
console.log(instance1.name);  //'boom shakalaka'
console.log(instance1.colors);  //['red','blue','green','black'];
var instance2 = new SubType();
console.log(instance2.name);  //'boom shakalaka'
console.log(instance2.colors);  //['red','blue','green'];
复制代码

基本思想:在子类的构造函数里调用父类的构造函数,通过使用apply()和call()方法可以在将来新创建的对象上执行构造函数

核心代码:SuperType.call(this,'boom shakalaka')

A:在子类构造函数借调了父类的构造函数。在创建子类实例的时候,会执行子类构造函数(包含了父类构造函数的代码呦)。这样一来新的实例在创建的时候,就会执行父类构造函数中定义的所有对象初始化代码,因此每个子类实例都会拥有自己引用类型值的副本了,这样就完成了继承。

存在问题:

1、根据图示,很明显的子类和父类原型没有创建起任何连接关系,因此子类实例是无法访问父类原型上的属性和方法(我这这样理解的,只是借用了父类构造函数的代码,然后其他什么都没干)

2、方法都在构造函数中定义(父类原型里的方法又访问不到),无法实现函数复用。比如父类里有个getColors方法,在创建子类实例的时候,每个实例都会创建一个新的getColors方法,instance1.getColors!==instance2.getColors

考虑到这些问题,借用构造函数的技术 也是很少单独使用的

组合式继承

function SuperType(name){
     this.name = name; 
     this.colors = ["red","blue","green"];
}
SuperType.prototype.sayName = function(){
     alert(this.name);
}
function SubType(name,age){
    //继承属性 
    SuperType.call(this,name);      //第二次调用SuperTyper()
    this.age = age;
}
//继承方法
SubType.prototype = new SuperType(); //第一次调用SuperTyper()
SubType.prototype.constructor = SubType;//因为重写了SubType.prototype嘛,所以我们要把新的原型对象的constructor再重新赋值回来
SubType.prototype.sayAge = function(){
   alert(this.age);
}

var instance1 = new SubType("Nicholas",29);
instance1.colors.push("black");
alert(instance1.colors);  // "red","blue","green","black"
instance1.sayName();      //"Nicholas"
instance1.sayAge();       //29

var instance2 = new SubType("Greg",27);
alert(instance2.colors);  // "red","blue","green"
instance2.sayName();      //"Greg"
instance2.sayAge();       //27
复制代码

基本思路:使用原型链实现了对原型属性和方法的继承(B处),通过借用构造函数实现对属性的继承(A处)。这样既通过在原型上定义方法实现了函数复用,又能保证每个实例都有自己的属性

C:实例上自己有的属性,就不会沿着原型链去原型上查找了,也就相当于是屏蔽了原型上的同名属性。(自己有的东西,就不会再问别人要了)

组合式继承就是原型式和构造函数式两者的结合,避免了各自的缺陷,融合了它们的优点,这样一来,子类实例既能拥有自己的属性 也能使用相同的方法了。成为js中最常用的模式。

存在问题: 父类的构造函数被调用了两次。 分别在继承属性和继承原型进行了调用, 会产生两组name和colors属性,一组在实例上 一组在原型上,会造成很大的浪费。

下边的寄生组合式继承 会解决这个问题

原型式继承

function object(o){ //在object函数内部,出现创建了一个临时性构造函数,
//将传入的对象作为构造函数的原型,最后返回这个临时类型的一个新实例
//本质上讲,就是对传入的对象执行了一次浅复制
    function F(){};
    F.prototype = o; 
    return new F();
}
var person = { 
    name:"Nicholas", 
    friends:["Shelby","Court","Van"]
};

var anotherPerson = object(person);
//用es5新增的Object.create()方法规范了继承式继承。Object.create()就是浅复制一个对象。本质上就是用上边object那个方法实现的。

anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");

var yetAnotherPerson = object(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");

alert(person.friends);  //"Shelby,Court,Van,Rob,Barbie"
复制代码

基本思想:不是严格意义上的构造函数。新的实例以person基础对象作为原型,person的属性就会被实例所共享。实际上,相当于创建了person的两个副本。实质上可以理解为复制

ES5新增Object.create规范了原型式继承,接收两个参数,一个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象,在传入一个参数的情况下,Object.create()和object()行为相同。

在没有必要兴师动众地创建构造函数,而想让一个对象与另一个对象保持类似的情况下,原型式继承是完全可以胜任的。同时存在的问题同原型链继承是一致的,引用类型值的属性会共享相同的值

寄生式继承

function createAnother(original){
    var clone = object(original);//object()函数创建对象
    // 或者用var clone =Object.create(original) 是一样的
    clone.sayHi = function(){    //增强这个对象 (就是给这个对象增加一些其他方法啊啥的)
        alert("hi");
    };
    return clone;                //返回这个对象
}
var person = {
    name:"Nicholas";
    friends:["Shelby","Court","Van"];
}     //基础对象
var anotherPerson = createAnother(person);  //新对象(不仅拥有了person的属性和方法,而且还拥有了自己的sayHi方法)
anotherPerson.sayHi();   //"hi"
复制代码

寄生式和原型式方法相同,都是复制一个基础对象来得到新对象,不同的是它将对象实例的修改放到也放到函数中,将整个过程(创建、增强、返回)封装了起来。

组合寄生式继承

//创建组合式继承的基本模型
function inheritPrototype(subType,superType){
//参数是子类构造函数 父类构造函数
    var prototype = Object.create(superType.prototype);  //创建对象(创建一个父类原型的副本)
    prototype.constructor = subType;              //增强对象(为创建的副本添加constructor属性,从而弥补了因重新原型而失去默认的constructor属性)
    subType.prototype = prototype;                //指定对象
}
function SuperType(name){
     this.name = name; 
     this.colors = ["red","blue","green"];
}
SuperType.prototype.sayName = function(){
     alert(this.name);
}
function SubType(name,age){
    //继承属性 
    SuperType.call(this,name);   //只调用一次SuperTyper()
    this.age = age;
}
//继承方法
inheritPrototype(SubType,SuperType);
SubType.prototype.sayAge = function(){
   alert(this.age);
}
复制代码

去掉那些等号以后

基本思想:通过借用构造函数来继承属性,通过原型链的混合形成来继承方法。还有个背后的思路就是 :不必为了指定子类型的原型而调用父类的构造函数,我们需要的无非就是父类原型的一个副本而已。本质上,就是使用寄生式继承来继承父类的原型,然后再将结果指定给子类型的原型

寄生式(复制)组合式(原型链+构造函数)继承,多种方式组合起来,解决了调用两次父类构造函数的问题

es6继承

说实话,es6的继承我就没搞明白跟es5的继承到底是什么联系,是基于寄生组合式继承实现的吗?请了解这一块的各位看官大老爷们来救救孩子吧,就是整不明白了。先写一点点吧

es6的继承就是class的继承,通过extends实现继承

class A{

}

class B extends A{

}

es5中每个对象都有__proto__属性,指向对应构造函数的prototype

Class作为构造函数的语法糖,同时有__proto__和prototype两个属性,因此同时存在两条继承链

B.proto === A(子类B的原型是父类A) B.prototype.proto === A.prototype(作为一个构造函数,子类(B)的原型(prototype属性)是父类的实例)

ES5和ES6的继承机制是不一样的:

ES5的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。ES6的继承机制完全不同,实质是先创造父类的实例对象this(所以必须先调用super方法),然后再用子类的构造函数修改this。

寄生组合式继承那个图,不知道我自己的理解对不对,要是我理解的有问题的话,大家指出来。很希望能跟大家一起交流一下,共同进步。

文章分类
前端
文章标签