JavaScript的9种继承实现方式归纳


Posted in Javascript onMay 18, 2015

不同于基于类的编程语言,如 C++ 和 Java,JavaScript 中的继承方式是基于原型的。同时由于 JavaScript 是一门非常灵活的语言,其实现继承的方式也非常多。

首要的基本概念是关于构造函数和原型链的,父对象的构造函数称为Parent,子对象的构造函数称为Child,对应的父对象和子对象分别为parent和child。

对象中有一个隐藏属性[[prototype]](注意不是prototype),在 Chrome 中是__proto__,而在某些环境下则不可访问,它指向的是这个对象的原型。在访问任何一个对象的属性或方法时,首先会搜索本对象的所有属性,如果找不到的话则会根据[[prototype]]沿着原型链逐步搜索其原型对象上的属性,直到找到为止,否则返回undefined。

1.原型链继承:

原型链是 JavaScript 中实现继承的默认方式,如果要让子对象继承父对象的话,最简单的方式是将子对象构造函数的prototype属性指向父对象的一个实例:

function Parent() {}

function Child() {}

Child.prototype = new Parent()

这个时候,Child的prototype属性被重写了,指向了一个新对象,但是这个新对象的constructor属性却没有正确指向Child,JS 引擎并不会自动为我们完成这件工作,这需要我们手动去将Child的原型对象的constructor属性重新指向Child:
Child.prototype.constructor = Child

以上就是 JavaScript 中的默认继承机制,将需要重用的属性和方法迁移至原型对象中,而将不可重用的部分设置为对象的自身属性,但这种继承方式需要新建一个实例作为原型对象,效率上会低一些。

2.原型继承(非原型链):

为了避免上一个方法需要重复创建原型对象实例的问题,可以直接将子对象构造函数的prototype指向父对象构造函数的prototype,这样,所有Parent.prototype中的属性和方法也能被重用,同时不需要重复创建原型对象实例:

Child.prototype = Parent.prototype

Child.prototype.constructor = Child

但是我们知道,在 JavaScript 中,对象是作为引用类型存在的,这种方法实际上是将Child.prototype和Parent.prototype中保存的指针指向了同一个对象,因此,当我们想要在子对象原型中扩展一些属性以便之后继续继承的话,父对象的原型也会被改写,因为这里的原型对象实例始终只有一个,这也是这种继承方式的缺点。

3.临时构造器继承:

为了解决上面的问题,可以借用一个临时构造器起到一个中间层的作用,所有子对象原型的操作都是在临时构造器的实例上完成,不会影响到父对象原型:

var F = function() {}

F.prototype = Parent.prototype

Child.prototype = new F()

Child.prototype.constructor = Child

同时,为了可以在子对象中访问父类原型中的属性,可以在子对象构造器上加入一个指向父对象原型的属性,如uber,这样,可以在子对象上直接通过child.constructor.uber访问到父级原型对象。

我们可以将上面的这些工作封装成一个函数,以后调用这个函数就可以方便实现这种继承方式了:

function extend(Child, Parent) {

    var F = function() {}

    F.prototype = Parent.prototype

    Child.prototype = new F()

    Child.prototype.constructor = Child

    Child.uber = Parent.prototype

}

然后就可以这样调用:
extend(Dog, Animal)

4.属性拷贝:

这种继承方式基本没有改变原型链的关系,而是直接将父级原型对象中的属性全部复制到子对象原型中,当然,这里的复制仅仅适用于基本数据类型,对象类型只支持引用传递。

function extend2(Child, Parent) {

    var p = Parent.prototype

    var c = Child.prototype

    for (var i in p) {

        c[i] = p[i]

    }

    c.uber = p

}

这种方式对部分原型属性进行了重建,构建对象的时候效率会低一些,但是能够减少原型链的查找。不过我个人觉得这种方式的优点并不明显。

5.对象间继承:

除了基于构造器间的继承方法,还可以抛开构造器直接进行对象间的继承。即直接进行对象属性的拷贝,其中包括浅拷贝和深拷贝。

浅拷贝:
接受要继承的对象,同时创建一个新的空对象,将要继承对象的属性拷贝至新对象中并返回这个新对象:

function extendCopy(p) {

    var c = {}

    for (var i in p) {

        c[i] = p[i]

    }

    c.uber = p

    return c

}

拷贝完成之后对于新对象中需要改写的属性可以进行手动改写。

深拷贝:
浅拷贝的问题也显而易见,它不能拷贝对象类型的属性而只能传递引用,要解决这个问题就要使用深拷贝。深拷贝的重点在于拷贝的递归调用,检测到对象类型的属性时就创建对应的对象或数组,并逐一复制其中的基本类型值。

function deepCopy(p, c) {

    c = c || {}

    for (var i in p) {

        if (p.hasOwnProperty(i)) {

            if (typeof p[i] === 'object') {

                c[i] = Array.isArray(p[i]) ? [] : {}

                deepCopy(p[i], c[i])

            } else {

                c[i] = p[i]

            }

        }

    }

    return c

}

其中用到了一个 ES5 的Array.isArray()方法用于判断参数是否为数组,没有实现此方法的环境需要自己手动封装一个 shim。
Array.isArray = function(p) {

    return p instanceof Array

}

但是使用instanceof操作符无法判断来自不同框架的数组变量,但这种情况比较少。

6.原型继承:

借助父级对象,通过构造函数创建一个以父级对象为原型的新对象:

function object(o) {

    var n

    function F() {}

    F.prototype = o

    n = new F()

    n.uber = o

    return n

}

这里,直接将父对象设置为子对象的原型,ES5 中的 Object.create()方法就是这种实现方式。

7.原型继承和属性拷贝混用:

原型继承方法中以传入的父对象为原型构建子对象,同时还可以在父对象提供的属性之外额外传入需要拷贝属性的对象:

function ojbectPlus(o, stuff) {

    var n

    function F() {}

    F.prototype = o

    n = new F()

    n.uber = o
    for (var i in stuff) {

        n[i] = stuff[i]

    }

    return n

}

8.多重继承:

这种方式不涉及原型链的操作,传入多个需要拷贝属性的对象,依次进行属性的全拷贝:

function multi() {

    var n = {}, stuff, i = 0,

        len = arguments.length

    for (i = 0; i < len; i++) {

        stuff = arguments[i]

        for (var key in stuff) {

            n[i] = stuff[i]

        }

    }

    return n

}

根据对象传入的顺序依次进行拷贝,也就是说,如果后传入的对象包含和前面对象相同的属性,后者将会覆盖前者。

9.构造器借用:

JavaScript中的call()和apply()方法非常好用,其改变方法执行上下文的功能在继承的实现中也能发挥作用。所谓构造器借用是指在子对象构造器中借用父对象的构造函数对this进行操作:

function Parent() {}

Parent.prototype.name = 'parent'
function Child() {

    Parent.apply(this, arguments)

}

var child = new Child()

console.log(child.name)

这种方式的最大优势就是,在子对象的构造器中,是对子对象的自身属性进行完全的重建,引用类型的变量也会生成一个新值而不是一个引用,所以对子对象的任何操作都不会影响父对象。

而这种方法的缺点在于,在子对象的构建过程中没有使用过new操作符,因此子对象不会继承父级原型对象上的任何属性,在上面的代码中,child的name属性将会是undefined。

要解决这个问题,可以再次手动将子对象构造器原型设为父对象的实例:

Child.prototype = new Parent()

但这样又会带来另一个问题,即父对象的构造器会被调用两次,一次是在父对象构造器借用过程中,另一次是在继承原型过程中。

要解决这个问题,就要去掉一次父对象构造器的调用,构造器借用不能省略,那么只能去掉后一次调用,实现继承原型的另一方法就是迭代复制:

extend2(Child, Parent)

使用之前实现的extend2()方法即可。
Javascript 相关文章推荐
jQuery Ajax提交表单查询获得数据实例代码
Sep 19 Javascript
AJAX跨域请求json数据的实现方法
Nov 11 Javascript
javascript常用的正则表达式实例
May 15 Javascript
在页面加载完成后通过jquery给多个span赋值
May 21 Javascript
jQuery中extend函数详解
Feb 13 Javascript
深入浅出分析javaScript中this用法
May 09 Javascript
js密码强度检测
Jan 07 Javascript
JS实现设置ff与ie元素绝对位置的方法
Mar 08 Javascript
更高效的使用JQuery 这里总结了8个小技巧
Apr 13 Javascript
js中利用cookie实现记住密码功能
Aug 20 Javascript
VueJs组件prop验证简单介绍
Sep 12 Javascript
Vue+Java+Base64实现条码解析的示例
Sep 23 Javascript
JQuery中上下文选择器实现方法
May 18 #Javascript
JQuery中两个ul标签的li互相移动实现方法
May 18 #Javascript
JQuery球队选择实例
May 18 #Javascript
JQuery实现动态添加删除评论的方法
May 18 #Javascript
TypeError document.getElementById(...) is null错误原因
May 18 #Javascript
JQuery实现带排序功能的权限选择实例
May 18 #Javascript
JQuery中clone方法复制节点
May 18 #Javascript
You might like
PHP+Mysql+jQuery实现动态展示信息
2011/10/08 PHP
使用PHP备份MySQL和网站发送到邮箱实例代码
2013/11/28 PHP
PHP 下载文件时自动添加bom头的方法实例
2014/01/10 PHP
Zend Framework实现将session存储在memcache中的方法
2016/03/22 PHP
PHP连接MySQL进行增、删、改、查操作
2017/02/19 PHP
PHP+ajax实现上传、删除、修改单张图片及后台处理逻辑操作详解
2020/02/12 PHP
让任务管理器中的CPU跳舞的js代码
2008/11/01 Javascript
JavaScript对象链式操作代码(jquery)
2010/07/04 Javascript
JavaScript打印iframe内容示例代码
2013/08/20 Javascript
jQuery焦点控制图层展示延迟隐藏的方法
2015/03/09 Javascript
JS实现pasteHTML兼容ie,firefox,chrome的方法
2016/06/22 Javascript
js复制内容到剪贴板代码,js复制代码的简单实例
2016/10/27 Javascript
用jQuery旋转插件jqueryrotate制作转盘抽奖
2017/02/10 Javascript
Node.js常用工具之util模块
2017/03/09 Javascript
微信小程序 图片加载(本地,网路)实例详解
2017/03/10 Javascript
Bootstrap学习笔记 轮播(Carousel)插件
2017/03/21 Javascript
vue  directive定义全局和局部指令及指令简写
2018/11/20 Javascript
Node4-5静态资源服务器实战以及优化压缩文件实例内容
2019/08/29 Javascript
vue路由传参三种基本方式详解
2019/12/09 Javascript
[41:13]完美世界DOTA2联赛PWL S2 Forest vs Rebirth 第一场 11.20
2020/11/20 DOTA
python实现在控制台输入密码不显示的方法
2015/07/02 Python
python实现识别相似图片小结
2016/02/22 Python
Python读取英文文件并记录每个单词出现次数后降序输出示例
2018/06/28 Python
python爬虫 爬取58同城上所有城市的租房信息详解
2019/07/30 Python
pytorch 改变tensor尺寸的实现
2020/01/03 Python
tensorflow 2.0模式下训练的模型转成 tf1.x 版本的pb模型实例
2020/06/22 Python
Django filter动态过滤与排序实现过程解析
2020/11/26 Python
paramiko使用tail实时获取服务器的日志输出详解
2020/12/06 Python
详解pycharm的python包opencv(cv2)无代码提示问题的解决
2021/01/29 Python
Python中对象的比较操作==和is区别详析
2021/02/12 Python
css3的transition效果和transfor效果示例介绍
2013/10/30 HTML / CSS
房产销售经理职责
2013/12/20 职场文书
法定代表人身份证明书
2014/09/10 职场文书
2014年学校工作总结
2014/11/20 职场文书
文员岗位职责范本
2015/04/16 职场文书
2015重阳节敬老活动总结
2015/07/29 职场文书