js实现继承的方法及优缺点总结


Posted in Javascript onMay 08, 2019

整理《javascript高级程序设计》中继承的方法以及优缺点。

1. 原型链

ECMAScript中描述了原型链的概念,并将原型链作为实现继承的主要方法。

原型链继承的基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。

简单回顾一下构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。

那么,假如我们让原型对象等于另一个类型的实例,结果会怎么样呢?显然,此时的原型对象将包含一个指向另一个原型的指针,相应地,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立,如此层层递进,就构成了实例与原型的链条。这就是所谓原型链的基本概念。

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

var instance = new SubType();
console.log(instance.getSuperValue());  //true

以上代码定义了两个类型:SuperType和SubType。每个类型分别有一个属性和一个方法。它们的主要区别是SubType继承了SuperType,而继承是通过创建SuperType的实例,并将该实例赋给SubType.prototype实现的。实现的本质是重写原型对象,代之以一个新类型的实例。换句话说,原来存在于SuperType的实例中的所有属性和方法,现在也存在于SubType.prototype中了。在确立了继承关系之后,我们给SubType.prototype添加了一个方法,这样就在继承了SuperType的属性和方法的基础上又添加了一个新方法

js实现继承的方法及优缺点总结

要注意instance.constructor现在指向的是SuperType,这是因为原来SubType.prototype中的constructor被重写了的缘故。实际上,不是SubType的原型的constructor属性被重写了,而是SubType的原型指向了另一个对象——SuperType的原型,而这个原型对象的constructor属性指向的是SuperType

别忘记默认的原型

事实上,前面例子中展示的原型链还少一环。我们知道,所有引用类型默认都继承了Object,而这个继承也是通过原型链实现的。大家要记住,所有函数的默认原型都是Object的实例,因此默认原型都会包含一个内部指针,指向Object.prototype。这也正是所有自定义类型都会继承toString()、valueOf()等默认方法的根本原因

js实现继承的方法及优缺点总结

原型链的问题

原型链虽然很强大,可以用它来实现继承,但它也存在一些问题。其中,最主要的问题来自包含引用类型值的原型

function SuperType(){
 this.colors = ["red", "blue", "green"];
}

function SubType(){
}
//继承了SuperType
SubType.prototype = new SuperType();

var instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors);  //"red,blue,green,black"

var instance2 = new SubType();
console.log(instance2.colors);  //"red,blue,green,black"

原型链的第二个问题是:在创建子类型的实例时,不能向超类型的构造函数中传递参数。实际上,应该说是没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。有鉴于此,再加上前面刚刚讨论过的由于原型中包含引用类型值所带来的问题,实践中很少会单独使用原型链

2. 借用构造函数

在子类型构造函数的内部调用超类型构造函数

function SuperType() {
 this.colors = ['red', 'blue', 'green'];
}
function SubType() {
 SuperType.call(this);
}
var instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors); //"red,blue,green,black"

var instance2 = new SubType();
console.log(instance2.colors); //"red,blue,green"

通过使用call()方法(或apply()方法也可以),我们实际上是在(未来将要)新创建的SubType实例的环境下调用了SuperType构造函数。这样一来,就会在新SubType对象上执行SuperType()函数中定义的所有对象初始化代码。结果,SubType的每个实例就都会具有自己的colors属性的副本了

对于原型链而言,借用构造函数有一个很大的优势,即可以在子类型构造函数中向超类型构造函数传递参数

function SuperType() {
 this.colors = ['red', 'blue', 'green'];
}
function SubType() {
 SuperType.call(this);
}
var instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors); //"red,blue,green,black"

var instance2 = new SubType();
console.log(instance2.colors); //"red,blue,green"

借用构造函数问题:

方法都在构造函数中定义,因此函数复用就无从谈起了。而且,在超类型的原型中定义的方法,对子类型而言也是不可见的,结果所有类型都只能使用构造函数模式

3. 组合继承

组合继承(combination inheritance),有时候也叫做伪经典继承,指的是将原型链和借用构造函数的技术组合到一块,从而发挥二者之长的一种继承模式。其背后的思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有它自己的属性

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

SuperType.prototype.sayName = function(){
 console.log(this.name);
};

function SubType(name, age){ 

 //继承属性
 SuperType.call(this, name);
 
 this.age = age;
}

//继承方法
SubType.prototype = new SuperType();
SubType.prototype.sayAge = function(){
 console.log(this.age);
};

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

var instance2 = new SubType("Greg", 27);
console.log(instance2.colors);  //"red,blue,green"
instance2.sayName();   //"Greg";
instance2.sayAge();   //27

组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为JavaScript中最常用的继承模式。而且,instanceof和isPrototypeOf也能够用于识别基于组合继承创建的对象。

无论什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部

4. 原型式继承

这种方法并没有使用严格意义上的构造函数。借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型

function object(o){
 function F(){}
 F.prototype = o;
 return new F();
}
var person = {
 name: "Nicholas",
 friends: ["Shelby", "Court", "Van"]
};

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

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

console.log(person.friends); //"Shelby,Court,Van,Rob,Barbie"

ECMAScript 5通过新增Object.create()方法规范化了原型式继承。这个方法接收两个参数:一个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。在传入一个参数的情况下,Object.create()与object()方法的行为相同。
Object.create()方法的第二个参数与Object.defineProperties()方法的第二个参数格式相同:每个属性都是通过自己的描述符定义的。以这种方式指定的任何属性都会覆盖原型对象上的同名属性

var person = {
 name: "Nicholas",
 friends: ["Shelby", "Court", "Van"]
};

var anotherPerson = Object.create(person, {
 name: {
  value: "Greg"
 }
});

console.log(anotherPerson.name); //"Greg"

在没有必要兴师动众地创建构造函数,而只想让一个对象与另一个对象保持类似的情况下,原型式继承是完全可以胜任的。不过别忘了,包含引用类型值的属性始终都会共享相应的值,就像使用原型模式一样

5. 寄生式继承

创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真的是它做了所有工作一样返回对象

function createAnother(original){
 var clone = Object.create(original); //通过调用函数创建一个新对象
 clone.sayHi = function(){   //以某种方式来增强这个对象
  console.log("hi");
 };
 return clone;     //返回这个对象
}

在这个例子中,createAnother()函数接收了一个参数,也就是将要作为新对象基础的对象。然后,把这个对象(original)传递给object()函数,将返回的结果赋值给clone。再为clone对象添加一个新方法sayHi(),最后返回clone对象。可以像下面这样来使用createAnother()函数:

var person = {
 name: "Nicholas",
 friends: ["Shelby", "Court", "Van"]
};

var anotherPerson = createAnother(person);
anotherPerson.sayHi(); //"hi"

这个例子中的代码基于person返回了一个新对象——anotherPerson。新对象不仅具有person的所有属性和方法,而且还有自己的sayHi()方法

使用寄生式继承来为对象添加函数,会由于不能做到函数复用而降低效率;这一点与构造函数模式类似

6. 寄生组合式继承

前面说过,组合继承是JavaScript最常用的继承模式;不过,它也有自己的不足。组合继承最大的问题就是无论什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。没错,子类型最终会包含超类型对象的全部实例属性,但我们不得不在调用子类型构造函数时重写这些属性。再来看一看下面组合继承的例子

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

SuperType.prototype.sayName = function(){
 console.log(this.name);
};

function SubType(name, age){ 
 SuperType.call(this, name);   //第二次调用SuperType()

 this.age = age;
}

SubType.prototype = new SuperType(); //第一次调用SuperType()
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
 console.log(this.age);
};

在第一次调用SuperType构造函数时,SubType.prototype会得到两个属性:name和colors;它们都是SuperType的实例属性,只不过现在位于SubType的原型中。当调用SubType构造函数时,又会调用一次SuperType构造函数,这一次又在新对象上创建了实例属性name和colors,于是,这两个属性就屏蔽了原型中的两个同名属性

js实现继承的方法及优缺点总结

如上图所示,有两组name和colors属性:一组在实例上,一组在SubType原型中。这就是调用两次SuperType构造函数的结果。好在我们已经找到了解决这个问题方法——寄生组合式继承。

所谓寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。

其背后的基本思路是:不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。寄生组合式继承的基本模式如下所示

function inheritPrototype(subType, superType){
 var prototype = Object.create(superType.prototype);  //创建对象
 prototype.constructor = subType;     //增强对象
 subType.prototype = prototype;      //指定对象
}

这个示例中的inheritPrototype()函数实现了寄生组合式继承的最简单形式。这个函数接收两个参数:子类型构造函数和超类型构造函数。在函数内部,第一步是创建超类型原型的一个副本。第二步是为创建的副本添加constructor属性,从而弥补因重写原型而失去的默认的constructor属性。最后一步,将新创建的对象(即副本)赋值给子类型的原型。这样,我们就可以用调用inheritPrototype()函数的语句,去替换前面例子中为子类型原型赋值的语句了

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

SuperType.prototype.sayName = function(){
 console.log(this.name);
};

function SubType(name, age){ 
 SuperType.call(this, name);

 this.age = age;
}

inheritPrototype(SubType, SuperType);

SubType.prototype.sayAge = function(){
 console.log(this.age);
};

这个例子的高效率体现在它只调用了一次SuperType构造函数,并且因此避免了在SubType.prototype上面创建不必要的、多余的属性。与此同时,原型链还能保持不变;因此,还能够正常使用instanceof和isPrototypeOf()。开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对三水点靠木的支持。

Javascript 相关文章推荐
javascript js cookie的存储,获取和删除
Dec 29 Javascript
js 分页全选或反选标识实现代码
Aug 09 Javascript
12款经典的白富美型—jquery图片轮播插件—前端开发必备
Jan 08 Javascript
jQuery对checkbox 复选框的全选全不选反选的操作
Aug 09 Javascript
jquery pagination插件动态分页实例(Bootstrap分页)
Dec 23 Javascript
vue-router跳转页面的方法
Feb 09 Javascript
javascript中的面向对象
Mar 30 Javascript
Angularjs实现下拉框联动的示例代码
Aug 22 Javascript
vue axios登录请求拦截器
Apr 02 Javascript
对vue v-if v-else-if v-else 的简单使用详解
Sep 29 Javascript
JS代码检查工具ESLint介绍与使用方法
Feb 04 Javascript
微信小程序仿通讯录功能
Apr 09 Javascript
微信小程序人脸识别功能代码实例
May 07 #Javascript
iphone刘海屏页面适配方法
May 07 #Javascript
常见的浏览器存储方式(cookie、localStorage、sessionStorage)
May 07 #Javascript
JavaScript实现随机点名器实例详解
May 07 #Javascript
vue父组件触发事件改变子组件的值的方法实例详解
May 07 #Javascript
微信小程序:数据存储、传值、取值详解
May 07 #Javascript
微信小程序实现用table显示数据库反馈的多条数据功能示例
May 07 #Javascript
You might like
如何使用PHP计算上一个月的今天
2013/05/23 PHP
PHP使用 Pear 进行安装和卸载包的方法详解
2019/07/08 PHP
可在线编辑网页文字效果代码(单击)
2013/03/02 Javascript
jquery插件开发注意事项小结
2013/06/04 Javascript
详解JavaScript中基于原型prototype的继承特性
2016/05/05 Javascript
全面解析JS字符串和正则表达式中的match、replace、exec等函数
2016/07/01 Javascript
easyui-combobox 实现简单的自动补全功能示例
2016/11/08 Javascript
浅谈js中function的参数默认值
2017/02/20 Javascript
基于axios封装fetch方法及调用实例
2018/02/05 Javascript
vue中改变选中当前项的显示隐藏或者状态的实现方法
2018/02/08 Javascript
JavaScript选择排序算法原理与实现方法示例
2018/08/06 Javascript
详解Vue结合后台的列表增删改案例
2018/08/21 Javascript
Vue2.0点击切换类名改变样式的方法
2018/08/22 Javascript
Angular7.2.7路由使用初体验
2019/03/01 Javascript
layui lay-verify form表单自定义验证规则详解
2019/09/18 Javascript
vue路由切换之淡入淡出的简单实现
2019/10/31 Javascript
js实现验证码干扰(静态)
2021/02/22 Javascript
使用scrapy实现爬网站例子和实现网络爬虫(蜘蛛)的步骤
2014/01/23 Python
Python之eval()函数危险性浅析
2014/07/03 Python
Python 由字符串函数名得到对应的函数(实例讲解)
2017/08/10 Python
Python 实现淘宝秒杀的示例代码
2018/01/02 Python
python格式化输出保留2位小数的实现方法
2019/07/02 Python
python多线程同步实例教程
2019/08/11 Python
python 使用多线程创建一个Buffer缓存器的实现思路
2020/07/02 Python
马德里运动鞋商店:Nigra Mercato
2020/02/16 全球购物
小区门卫岗位职责
2013/12/31 职场文书
网络编辑岗位职责
2014/03/18 职场文书
领导干部贪图享乐整改措施
2014/09/21 职场文书
2015年大学班长个人工作总结
2015/04/24 职场文书
办公室管理规章制度
2015/08/04 职场文书
ObjectMapper 如何忽略字段大小写
2021/06/29 Java/Android
golang实现一个简单的websocket聊天室功能
2021/10/05 Golang
C3 线性化算法与 MRO之Python中的多继承
2021/10/05 Python
MySQL基于索引的压力测试的实现
2021/11/07 MySQL
Golang获取List列表元素的四种方式
2022/04/20 Golang
Python first-order-model实现让照片动起来
2022/06/25 Python