Javascript中类式继承和原型式继承的实现方法和区别之处


Posted in Javascript onApril 25, 2017

在所有面向对象的编程中,继承是一个重要的话题。一般说来,在设计类的时候,我们希望能减少重复性的代码,并且尽量弱化对象间的耦合(让一个类继承另一个类可能会导致二者产生强耦合)。关于“解耦”是程序设计中另一个重要的话题,本篇重点来看看在javascript如何实现继承。

其它的面向对象程序设计语言都是通过关键字来解决继承的问题(比如extend或inherit等方式)。但是javascript中并没有定义这种实现的机制,如果一个类需要继承另一个类,这个继承过程需要程序员自己通过编码来实现。

一、类式继承的实现

1、创建一个类的方式:

//定义类的构造函数
function Person(name) {
  this.name = name || '默认姓名';
}
//定义该类所有实例的公共方法
Person.prototype.getName = function() {
  return this.name;
}
var smith = new Person('Smith');
var jacky = new Person('Jacky');
console.log( smith.getName(), jacky.getName() ); //Smith Jacky

2、继承这个类:

这需要分两个步骤来实现,第1步是继承父类构造函数中定义的属性,第2步是继承父类的prototype属性

//定义类的构造函数
function Person(name) {
  this.name = name || '默认姓名';
}
//定义该类所有实例的公共方法
Person.prototype.getName = function() {
  return this.name;
}
function Author(name, books) {
  //继承父类构造函数中定义的属性
  //通过改变父类构造函数的执行上下文来继承
  Person.call(this, name);
  this.books = books;
}
//继承父类对应的方法
Author.prototype = new Person(); //Author.prototype.constructor === Person
Author.prototype.constructor = Author; //修正修改原型链时造成的constructor丢失
Author.prototype.getBooks = function() {
  return this.books;
};
//测试
var smith = new Person('Smith');
var jacky = new Author('Jacky', ['BookA', 'BookB']);
console.log(smith.getName()); //Smith
console.log(jacky.getName()); //Jacky
console.log(jacky.getBooks().join(', ')); //BookA, BookB
console.log(smith.getBooks().join(', ')); //Uncaught TypeError: smith.getBooks is not a function

从测试的结果中可以看出,Author正确继承了Person,而且修改Author的原型时,并不会对Person产生影响。这其中的关键一句就是 Author.prototype = new Person(),要与Author.prototype = Person.prototype区分开来。前者产生了一个实例,这个实例有Person.prototype的副本(这里先这么理解,后面有更详细的解析)。后者是指将两者的prototype指向同一个原型对象。

那么,这也意味着每次继承都将产生一个父类的副本,肯定对内存产生消耗,但为了类式继承这个内存开销必须得支付,但还可以做得更节省一点:Author.prototype = new Person()这一句其实多执行了构造函数一次(而这一次其实只需在子类构造函数中执行即可),尤其是在父类的构造函数很庞大时很耗时和内存。修改一下继承的方式,如下:

Author.prototype = (function() {
  function F() {}
  F.prototype = Person.prototype;
  return new F();
})();

如上所示的代码,new时,去掉了对父类的构造函数的调用,节省了一次调用的开销。

3、类式继承显著的特点是每一次实例化对象时,子类都将执行一次父类的构造函数。如果E继承了D,D继承了C,C继承了B,B继承了A,在实例化一个E时,一共要经过几次构造函数的调用呢?

/*继承方法的函数*/
function extend(son, father) {
  function F() {}
  F.prototype = father.prototype;
  son.prototype = new F();
  son.prototype.constructor = son;
}
//A类
function A() {
  console.log('A()');
}
A.prototype.hello = function() {
  console.log('Hello, world.');
}
//B类
function B() {
  A.call(this);
  console.log('B()');
}
extend(B, A);
//C类
function C() {
  B.call(this);
  console.log('C()');
}
extend(C, B);
//D类
function D() {
  C.call(this);
  console.log('D()');
}
extend(D, C);
//E类
function E() {
  D.call(this);
  console.log('E()');
}
extend(E, D);
//创建一个E的实例
var e = new E(); //A() B() C() D() E()
e.hello(); //hello, world.

5次,这还只是实例化一个E时调用的次数。所以,我们应该尽可能的减少继承的级别。但这并不是说不要使用这种类式继承,而是应该根据自己的应用场合决定采用什么方法。

二、原型式继承

1、先来看一段代码:我们先将之前类式继承中的继承prototype那一段改成另一个函数clone,然后通过字面量创建一个Person,最后让Author变成Person的克隆体。

//这个函数可以理解为克隆一个对象
function clone(object) {
  function F() {}
  F.prototype = object;
  return new F();
}
var Person = {
  name: 'Default Name';
  getName: function() {
    return this.name;
  }
}
//接下来让Author变为Person的克隆体
var Author = clone(Person);

问一个问题:clone函数里的new F()为这个实例开辟内存空间来存储object的副本了吗?

按我之前的理解,回答是肯定的。但是,当我继续将代码写下去的时候,奇怪的事情发生了,代码如下:

//接下来让Author变为Person的克隆体
var Author = clone(Person);
Author.books = [];
Author.getBooks = function() {
  return this.books.join(', ');
}
//增加一个作者Smith
var Smith = clone(Author);
console.log(Smith.getName(), Smith.getBooks()); //Default Name
Smith.name = 'Smith';
Smith.books.push('<<Book A>>', '<<Book B>>'); //作者写了两本书
console.log(Smith.getName(), Smith.getBooks()); //Smith <<Book A>>, <<Book B>>
//再增加一个作者Jacky
var Jacky = clone(Author);
console.log(Jacky.getName(), Jacky.getBooks()); // Default Name <<Book A>>, <<Book B>>

当我们继续增加作者Jacky时,奇怪的现象发生了!!Jacky的名字依然是Default Name,但是他居然也写两本与Smith一样的书?Jacky的书都还没push呢。到了这里,我想到了引用对象的情况(引用一个对象时,引用的是该对象的内存地址),发生这样的现象,问题肯定出在clone()函数中的new F()这里。

事实上,这个clone中的new F()确实返回了一个新的对象,该对象拥有被克隆对象的所有属性。但这些属性保留的是对被克隆对象中相应属性的引用,而非一个完全独立的属性副本。换句话说,新对象的属性 与 被克隆的对象的属性指向同一个内存地址(学过C语言的同学应该明白指针类型,这里意义差不多)。

那么为什么上面的代码中,Jacky的书与Smith的书相同了,为什么Jacky的名字却不是Smith而是Default Name呢?这就是Javascript中继承的机制所在,当Smith刚刚继承自Author时,他的属性保留了对Author的属性的引用,一旦我们显示的对Smith的属性重新赋值时,Javascritp引擎就会给Smith的该属性重新划分内存空间来存储相应的值,由于重新划分了内址地址,那么对Smith.name的改写就不会影响到Author.name去了。这就很好的解释了前面的那个问题——为什么Jacky的名字却不是Smith而是Default Name。

2、基于原型继承

通过前面的情况分析,可以看出基于原型继承的方式更能节约内存(只有在需要时候才开辟新的内存空间)。但要注意:基于原型继承时,对象的属性一定要重新赋值后(重新划分内存)再去引用该属性。对于对象的方法,如果有不同的处理方式,我们只需重新定义即可。

下面将前一段代码做一个完整、正确的范例出来,以说明原型继承的特点和使用方式:

//这个函数可以理解为克隆一个对象
function clone(object) {
  function F() {}
  F.prototype = object;
  return new F();
}
var Person = {
  name: 'Default Name',
  getName: function() {
    return this.name;
  }
}
//接下来让Author变为Person的克隆体
var Author = clone(Person);
Author.books = [];
Author.getBooks = function() {
  return this.books.join(', ');
}
//增加一个作者Smith
var Smith = clone(Author);
Smith.name = 'Smith';
Smith.books = [];
Smith.books.push('<<Book A>>', '<<Book B>>'); //作者写了两本书
console.log(Smith.getName(), Smith.getBooks()); //Smith <<Book A>>, <<Book B>>
//再增加一个作者Jacky
var Jacky = clone(Author);
Jacky.name = 'Jacky';
Jacky.books = [];
Jacky.books.push('<<Book C>>', '<<Book D>>');
console.log(Jacky.getName(), Jacky.getBooks()); // Jacky <<Book C>>, <<Book D>>

三、类式继承与原型式继承的区别与相式之处

1、类式继承中:使用构造函数初始化对象的属性,通过调用父类的构造函数来继承这些属性。通过new 父类的prototype来继承方法。

2、原型式继承中:去掉了构造函数,但需要将对象的属性和方法写一个{}里申明。准确的说,原型式继承就是类式继承中继承父类的prototype方法。

以上所述是小编给大家介绍的Javascript中类式继承和原型式继承的实现方法和区别,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对三水点靠木网站的支持!

Javascript 相关文章推荐
JavaScript 变量基础知识
Nov 07 Javascript
js实现俄罗斯方块小游戏分享
Jan 31 Javascript
jq实现左侧显示图片右侧文字滑动切换效果
Aug 04 Javascript
jQuery CSS3相结合实现时钟插件
Jan 08 Javascript
JavaScript浏览器对象模型BOM(BrowserObjectModel)实例详解
Nov 29 Javascript
jquery ajaxfileupload异步上传插件使用详解
Feb 08 Javascript
详解javascript appendChild()的完整功能
Aug 18 Javascript
vue项目部署到Apache服务器中遇到的问题解决
Aug 24 Javascript
解决vue-cli项目webpack打包后iconfont文件路径的问题
Sep 01 Javascript
vue实现百度下拉列表交互操作示例
Mar 12 Javascript
vue中v-show和v-if的异同及v-show用法
Jun 06 Javascript
一篇文章学会Vue中间件管道
Jun 20 Vue.js
整理一些最近经常遇到的前端面试题
Apr 25 #Javascript
Vue.js 2.0学习教程之从基础到组件详解
Apr 24 #Javascript
js实现延迟加载的几种方法
Apr 24 #Javascript
利用Vue.js+Node.js+MongoDB实现一个博客系统(附源码)
Apr 24 #Javascript
浅析Angular2子模块以及异步加载
Apr 24 #Javascript
Angular2使用Guard和Resolve进行验证和权限控制
Apr 24 #Javascript
详解AngularJS 路由 resolve用法
Apr 24 #Javascript
You might like
PHP面向对象编程快速入门
2006/12/14 PHP
php 遍历显示文件夹下所有目录、所有文件的函数,没有分页的代码
2008/11/14 PHP
php feof用来识别文件末尾字符的方法
2010/08/01 PHP
PHP获取MSN好友列表类的实现代码
2013/06/23 PHP
CI框架Session.php源码分析
2014/11/03 PHP
PHP is_array() 检测变量是否是数组的实现方法
2016/06/13 PHP
Yii2框架制作RESTful风格的API快速入门教程
2016/11/08 PHP
幻宇的层模拟窗口效果-提供演示和下载
2007/01/20 Javascript
Html中JS脚本执行顺序简单举例说明
2010/06/19 Javascript
javascript中setTimeout和setInterval的unref()和ref()用法示例
2014/11/26 Javascript
原生js实现移动端瀑布流式代码示例
2015/12/18 Javascript
WordPress中鼠标悬停显示和隐藏评论及引用按钮的实现
2016/01/12 Javascript
解析JavaScript中的字符串类型与字符编码支持
2016/06/24 Javascript
AngularJS ng-mousedown 指令
2016/08/02 Javascript
概述一个页面从输入URL到页面加载完的过程
2016/12/16 Javascript
js鼠标移动时禁止选中文字
2017/02/19 Javascript
js如何获取网页所有图片
2017/05/12 Javascript
jquery实现联想词搜索框和搜索结果分页的示例
2018/10/10 jQuery
详解Vue-axios 设置请求头问题
2018/12/06 Javascript
JS实现使用POST方式发送请求
2019/08/30 Javascript
使用layer弹窗,制作编辑User信息页面的方法
2019/09/27 Javascript
vuex实现数据状态持久化
2019/11/11 Javascript
vue-cli3自动消除console.log()的调试信息方式
2020/10/21 Javascript
python嵌套函数使用外部函数变量的方法(Python2和Python3)
2016/01/31 Python
Python中list初始化方法示例
2016/09/18 Python
Python自定义线程池实现方法分析
2018/02/07 Python
python matplotlib 画dataframe的时间序列图实例
2019/11/20 Python
Python Request类源码实现方法及原理解析
2020/08/17 Python
使用 css3 transform 属性来变换背景图的方法
2019/05/07 HTML / CSS
肯尼迪就职演说稿
2013/12/31 职场文书
《问银河》教学反思
2014/02/19 职场文书
幼儿园教师教育感言
2014/02/28 职场文书
优秀毕业生求职信
2014/06/05 职场文书
2016年妇联“6﹒26国际禁毒日”宣传活动总结
2016/04/05 职场文书
JPA如何使用entityManager执行SQL并指定返回类型
2021/06/15 Java/Android
苹果M1芯片安装nginx 并且部署vue项目步骤详解
2021/11/20 Servers