本篇文章给大家带来了JavaScript中继承的相关知识,其中包括了原型链继承、借用构造函数继承、组合继承和多重继承,希望对大家有帮助。
原型链继承
原理
本质是重写原型对象,代之以一个新类型的实例。下面代码中,原来存在于SuperType的实例对象的属性和方法,现在也存在于SubType.prototype中了。
实现
function Super(){ this.value = true; } Super.prototype.getValue = function(){ return this.value } function Sub(){}; // Sub继承了Super Sub.prototype = new Super(); Sub.prototype.constructor = Sub; const ins = new Sub(); console.log(ins.getValue()); // true
Sub继承了Super,而继承是通过创建Super实例,并将实例赋给Sub.prototype实现的。原来存在于Super的实例中的所有属性和方法,现在也存在与Sub.prototype中。如图所示。
上图可以看出,没有使用Sub默认提供的原型,而是给它换了一个新原型;这个新原型就是Super的实例。于是,新原型不仅具有作为一个Super的实例所拥有的属性和方法,而且它还指向了Super的原型。最终结果就是这样的:
ins=>Sub的原型=>Super的原型
getValue()方法仍然还在Sub.prototype中,但value属性则位于Sub.prototype中。这是因为value是一个实例属性,而getValue()则是一个原型方法。既然Sub.prototype现在是Super的实例,那么value位于该实例中。
此外,要注意ins.constructor现在指向的是 Super,这是因为原来 Sub.prototype 中的 constructor 被重写了的缘故。
缺点
-
私有原型属性会被实例共享
-
在创建子类型的实例时,不能向父类型的构造函数传递参数
原型链继承最主要的问题:私有原型属性会被实例共享,而这也正是为什么要在构造函数中,而不是原型对象中定义属性的原因。在通过原型来实现继承时,原型实例会变成另一个类的实例。于是,原先的实例属性也就顺理成章的变成了现在的原型属性了。
function Super(){ this.colors = ['red','green','blue']; } Super.prototype.getValue = function(){ return this.colors } function Sub(){}; //Sub继承了Super Sub.prototype = new Super(); const ins1 = new Super(); ins1.colors.push('black'); console.log(ins1.colors);//['red','green','blue','black']; const ins2 = new Sub(); console.log(ins2.colors);//['red','green','blue','black'];
原型链的第二个问题,在创建子类型的实例时,不能向父类型的构造函数传递参数。实际上,应该说是没有办法在不影响所有都想实例的情况下,给父类型的构造函数传递参数。再加上包含引用类型值的原型属性会被所有实例共享的问题,在实践中很少会单独使用原型链继承
注意问题
使用原型链继承方法要谨慎地定义方法,子类型有时候需要重写父类的某个方法,或者需要添加父类中不存在的某个方法。但不管怎样,给原型添加方法的代码一定要放在替换原型的语句之后。
function Super() { this.colors = ['red', 'green', 'blue']; } Super.prototype.getValue = function() { return this.colors } function Sub() { this.colors = ['black']; }; //Sub继承了Super Sub.prototype = new Super(); //添加父类已存在的方法,会重写父类的方法 Sub.prototype.getValue = function() { return this.colors; } //添加父类不存在的方法 Sub.prototype.getSubValue = function(){ return false; } const ins = new Sub(); //重写父类的方法之后得到的结果 console.log(ins.getValue()); //['black'] //在子类中新定义的方法得到的结果 console.log(ins.getSubValue());//false //父类调用getValue()方法还是原来的值 console.log(new Super().getValue());//['red', 'green', 'blue']
借用构造函数继承
原理
借用构造函数(有时候也叫做伪类继承或经典继承)。这种技术的基本思想相当简单,即在子类构造函数的内部调用父类构造函数。别忘了,函数只不过是在特定环境中执行代码的对象,因此通过使用apply()和call()方法也可以在新创建的对象上执行构造函数。
实现
function Super() { this.colors = ['red', 'green', 'blue']; } Super.prototype.getValue = function(){ return this.colors; } function Sub(){ //继承了Super Super.call(this);//相当于把构造函数Super中的this替换成了ins实例对象,这样在Super只有定义的私有属性会被继承下来,原型属性中定义的公共方法不会被继承下来 } const ins = new Sub(); console.log(ins.colors);
传递参数:相对于原型链来讲,借用构造函数继承有一个很大的优势,即可以在子类构造函数中向父类构造函数传递参数
function B(name){ this.name = name; } function A(){ //继承了B,同时还传递了参数 B.call(this,'ZZ'); //实例属性 this.age = 100; } const p = new A(); alert(p.name);//'ZZ' alert(p.age);//100
缺点
如果仅仅是借用构造函数,那么将无法避免构造函数模式存在的问题——方法都在构造函数中定义,因此函数复用就无从谈起。而且,在父类的原型中定义的方法,对子类而言是不可见的,所以这种方式使用较少。
组合继承
原理
组合继承,指的是将原型链和借用构造函数技术组合到一起,从而发挥两者之长的一种继承模式。其背后的思想是使用原型链实现对原型上的公共属性和方法的继承,而通过借用构造函数继承来实现对父类私有属性的继承。这样,即通过在父类原型上定义方法实现了函数复用,又能够保证每个实例都有父类的私有属性。
实现
function Super(name){ this.name = name; this.colors = ['red','blue','green']; } Super.prototype.sayName = function(){ alert(this.name); } function Sub(name,age){ Super.call(this,name); this.age = age; } // 继承方法 Sub.prototype = new Super(); Sub.prototype.constructor = Sub; Sub.prototype.sayAge = function(){ alert(this.age); } const ins = new Sub('jarvis',18); ins.colors.push('black'); console.log(ins.colors);// ["red", "blue", "green", "black"] ins.sayName();//'jarvis' ins.sayAge();//18 const ins2 = new Sub('ershiyi',21); console.log(ins2.colors);//["red", "blue", "green"] ins2.sayName();//'ershiyi' ins2.sayAge();//21
在上个例子中,Sub构造函数定义了两个属性:name和age。Super的原型定义了一个sayName()方法。在Sub构造函数中调用Super构造函数时传入了name参数,紧接着又定义它自己的属性age。然后,将Super的实例赋值给Sub的原型,然后又在该新原型上定义了方法sayAge()。这样一来,就可以让不同的Sub实例分别拥有自己的属性——包括colors属性,又可以使用相同的方法组合继承避免了原型链和借用构造函数的缺陷,融合了他们的优点,称为JavaScript中最常用的继承模式。
缺点
无论在什么情况下,都会调用两次父类的构造函数:一次是在创建子类原型的时候,另一次是在子类构造函数内部。
寄生组合式继承
原理
组合继承是JavaScript最常用的继承模式;不过,它也有自己的不足。组合继承最大的问题就是无论什么情况下,都会调用两次父类构造函数:一次是在创建子类原型的时候,另一次是在子类构造函数内部。没错,子类型最终会包含超类型对象的全部实例属性,但不得不在调用子类型构造函数时重写这些属性。再来看一看下面组合继承的例子。
实现
function Super(name){ this.name = name; this.colors = ['red','blue','green']; } Super.prototype.sayName = function(){ alert(this.name); } function Sub(name,age){ Super.call(this,name); this.age = age; } // 继承方法 Sub.prototype = new Super(); Sub.prototype.constructor = Sub; Sub.prototype.sayAge = function(){ alert(this.age); } const ins = new Sub('jarvis',18); ins.colors.push('black'); console.log(ins.colors);// ["red", "blue", "green", "black"] ins.sayName();//'jarvis' ins.sayAge();//18 const ins2 = new Sub('ershiyi',21); console.log(ins2.colors);//["red", "blue", "green"] ins2.sayName();//'ershiyi' ins2.sayAge();//21
所谓寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。其背 后的基本思路是:不必为了指定子类型的原型而调用超类型的构造函数,所需要的无非就是超类型原型的一个副本而已。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。寄生组合式继承的基本模式如下所示。
function Super(name){ this.name = name; this.colors = ['red','blue','green']; } Super.prototype.sayName = function(){ alert(this.name); } function Sub(name,age){ //继承实例属性 Super.call(this,name); this.age = age; } // 继承公有的方法 Sub.prototype = Object.create(Super.prototype); Sub.prototype.constructor = Sub; Sub.prototype.sayAge = function(){ alert(this.age); } const ins = new Sub('jarvis',18); ins.colors.push('black'); console.log(ins.colors);// ["red", "blue", "green", "black"] ins.sayName();//'jarvis' ins.sayAge();//18 const ins2 = new Sub('ershiyi',21); console.log(ins2.colors);//["red", "blue", "green"] ins2.sayName();//'ershiyi' ins2.sayAge();//21
多重继承
JavaScript中不存在多重继承,那也就意味着一个对象不能同时继承多个对象,但是可以通过变通方法来实现。
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>18 多重继承</title> </head> <body> <script type="text/javascript"> // 多重继承:一个对象同时继承多个对象 // Person Parent Me function Person(){ this.name = 'Person'; } Person.prototype.sayName = function(){ console.log(this.name); } // 定制Parent function Parent(){ this.age = 30; } Parent.prototype.sayAge = function(){ console.log(this.age); } function Me(){ // 继承Person的属性 Person.call(this); Parent.call(this); } // 继承Person的方法 Me.prototype = Object.create(Person.prototype); // 不能重写原型对象来实现 另一个对象的继承 // Me.prototype = Object.create(Parent.prototype); // Object.assign(targetObj,copyObj) Object.assign(Me.prototype,Parent.prototype); // 指定构造函数 Me.prototype.constructor = Me; const me = new Me(); </script> </body> </html>
ES5 与 ES6 继承差异
在 ES5 的传统继承中, this 的值会先被派生类创建,随后基类构造器才被调用。这意味着 this 一开始就是派生类的实例,之
后才使用了基类的附加属性对其进行了装饰。
在 ES6 基于类的继承中, this 的值会先被基类创建,随后才被派生类的构造 器所修改。结果是 this 初始就拥有作为基类的内置对象的所有功能,并能正确接收与之关联的所有功能。
【