【图解】ES5的各种继承方式,及其优缺点。
一,原型及原型链
ECMAScript中描述了原型链的概念,并将原型链作为实现继承的主要方式,其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法,简单回顾一下构造函数,原型和实例的关系,每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针,那么,假如我们让原型对象等于另一个类型的实例,结果会怎么样呢?显然此时的原型对象将包含一个指向另一个原型的指针,相应的另一个原型中,也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立,如此层层递进,就构成的实例与原型的链条,这就是所谓原型链的基本概念,下面用图来直观的展现构造函数,原型,实例之间的关系。
1.1 这里有几点需要注意的地方:
1. 每个对象也只有__proto__属性,表示隐式原型,并且__proto__其实是[[Prototype]]的另一种表示。
2. prototype是构造函数特有的属性,表示显式原型。
4. 所有函数的默认原型都是Object构造函数的实例,因此默认原型都会包含一个[[Prototype]]指向Object.prototype,这也正是所有自定义类型都会继承 toString() , valueOf() 等默认方法的根本原因。
3. constructor属性是定义构造函数时产生的原型对象所特有的,如果人为改变原型对象则constructor会被重写,需要手动更正,如上图红色部分所示。而constructor属性的作用是可以正确的找到实例的构造函数以及原型,所以手动更正很有必要。
1.2 有了这样的原型链机制,当你试图引用对象的属性时会触发[[get]]操作,对于默认的[[get]]操作,第一步是检查对象本身是否有这个属性,如果有的话就使用它。如果无法在对象本身找到需要的属性,就会继续访问对象的原型链。一直向上查找,知道访问到原始Object构造函数的原型。
二, 原型链继承
function Parent() { this.parentProperty = true } Parent.prototype.getParentValue = function() { return this.parentProperty } function Children(name) { this.childProperty = false } // 继承了 Parent Children.prototype = new Parent() let child = new Children() child.getParentValue() // true
第一个要说的是原型链继承,如顶部的图所示,这就是原型链继承。
优点:实现比较简单,能通过instanceOf和isPrototypeOf的检测。
缺点:1. 在通过原型链实现继承时,原型实际上会成为另一个类型的实例。所以父类的实例属性实际上会成为子类的原型属性。结果就是所有的子类的实例都会共享父类的实例属性(引用类型的)。
2. 在创建子类型的实例时,没有办法在不影响所有实例的情况下,向父类型的构造函数传递参数。因此实践中很少单独使用原型链继承。
三,构造函数继承
构造函数继承的思想很简单,即在子类构造函数中调用父类构造函数。如上图所示,你可以认为子类把父类构造函数拷贝了一份到自己的作用域,这个过程通过调用call或apply方法实现。 这样创建新对象的时候就会执行父类和子类的构造函数,从而实现了子类继承父类。
function Animal(eat) { this.eat = eat this.play = function () { return '${this.name}正在玩耍' } } function Dog(name) { Animal.call(this, 'bone') // 可传参 this.name = name } let dog = new Dog('jerry') console.log(dog.eat) // 'bone' dog.play() // jerry正在玩耍
优点:可以向父类构造函数传参数,并且每一个子类实例都有父类属性和方法的副本
缺点:正是因为这种方法调用父类,所以也只能继承父类的属性和方法,且继承的方法也无法复用。并且不能继承父类原型的属性和方法。(子类原型并未链接父类原型)
四,组合式继承(原型链继承和构造函数继承组合)
既然以上两种继承方式都有各自的缺点,那么能不能结合两种方式消除这些缺点发挥各自的长处呢?答案是肯定的,看图。
如图所示,父类定义实例属性,并用构造函数继承让子类继承。父类原型定义可复用的方法或属性,并用原型链继承让子类继承。 这样既可以在原型上定义方法实现了函数复用,又能够保证每个实例都有他自己的属性。
function Animal(eat) { this.eat = eat } Animal.prototype.play = function () { return `${this.name}正在玩耍` } function Dog(name,food) { Animal.call(this, food) // 调用父类,可传参,继承属性 this.name = name } Dog.prototype = new Animal('bone') // 继承方法,(注意:这里又调用了一次父类!) let dog = new Dog('jerry','bone') console.log(dog.eat) // 'bone' dog.play() // jerry正在玩耍
(这样每只动物都有自己喜欢吃的食物,并且有共同的技能play)
优点:组合继承避免了原型链和借用构造函数的缺陷。融合了他们的优点成为JavaScript中最常用的继承模式。而且instanceof 和 isPrototypeOf() 也能够用于识别基于组合继承创建的对象。
缺点:组合继承调用了两次父类,因此子类实例和子类的原型上各存有一份父类实例的属性和方法。
五,原型式继承
原型式继承最初由 道格拉斯·克罗克福德 在2006年提出,基本思路是借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。如下图
这种继承方式,要求你必须有一个对象可以作为另一个对象的基础,如果有这么一个对象的话,可以把它传递给Object()函数。
function object(o) { function fn() {} fn.prototype = o return new fn() } let dog = { name: jerry, play = function () { console.log(`${this.name}正在玩耍`) } } let bigDog = object(dog) bigDog.age = 6 bigDog.play() // jerry正在玩耍
返回的新对象将dog对象作为原型,所以它的原型中就包含一个基本类型值属性和一个引用类型值属性。实际上,相当于创建了一个dog对象的副本。ES5通过新增Object.create()方法规范化了原型式继承。这个方法接收两个参数一个用作新对象原型的对象和(可选的)一个作为新对象定义额外属性的对象。在传入一个参数的情况下,object.create() 和 object() 方法的行为相同。
优点:在没有必要兴师动众的创建构造函数,而只想让一个对象与另外一个对象保持类似的情况下,原型式继承是完全可以胜任的。
缺点:包含引用类型值的属性始终都会共享相同的值,并且这种方法有局限性。
六,寄生式继承
寄生式继承是与原型式继承紧密相关的一种思路,并且同样也是由克罗克福德推而广之的。寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真的是他做的所有工作一样返回对象。
function object(o) { function fn() {} fn.prototype = o return new fn() } function createAnother(original) { var clone = object(original) // 先应用原型式继承 clone.sleep = function () { // 增强对象 console.log(`${this.name}正在睡觉`) } return clone; } let dog = { name: jerry, play = function () { console.log(`${this.name}正在玩耍`) } } let bigDog = createAnother(dog) bigDog.age = 6 bigDog.play() // jerry正在玩耍 bigDog.sleep() // jerry正在睡觉
优点:在主要考虑对象,而不是自定义类型和构造函数的情况下,寄生式继承也是一种可选方案。
缺点:和原型继承类似有一定局限性。
七,寄生组合式继承
前面说到组合继承是JavaScript最常用的继承模式。但它也有自己的缺陷,而且也分析清楚了,那就是调用了两次父类。(不理解为什么调用了两次父类构造函数的可以结合上面的图思考),第二次是在继承父类原型属性和方法时不可避免的调用了父类构造函数,而我们发现原型式继承和寄生式继承正是可以不必为了指定子类型的原型而调用父类的构造函数。 这样就好办了,可以用构造函数继承继承属性,用寄生与原型链继承结合的方式继承父类原型,这就是寄生组合继承。如图:
function inheritPrototype(Children, Parent){ var protoType = Object.create(Parent.prototype); //创建对象 protoType.constructor = Children; //增强对象 Children.prototype = protoType; //指定对象 } function Animal(food){ this.eat = food; } Animal.prototype.play = function(){ console.log(`${this.name}正在玩耍`) } function Dog(name, food){ Animal.call(this, food); this.name = name; } inheritPrototype(Dog, Animal) // 继承父类原型而不用再次调用父类构造函数 Dog.prototype.sayAge = function(){ alert(this.age); } var dog = new Dog("Bob", 'bone'); dog.name; // 'Bob' dog.play(); // Bob正在玩耍