首页 web前端 js教程 JavaScript的原型继承详解_javascript技巧

JavaScript的原型继承详解_javascript技巧

May 16, 2016 pm 04:14 PM
javascript

JavaScript是一门面向对象的语言。在JavaScript中有一句很经典的话,万物皆对象。既然是面向对象的,那就有面向对象的三大特征:封装、继承、多态。这里讲的是JavaScript的继承,其他两个容后再讲。

JavaScript的继承和C++的继承不大一样,C++的继承是基于类的,而JavaScript的继承是基于原型的。

现在问题来了。

原型是什么?原型我们可以参照C++里的类,同样的保存了对象的属性和方法。例如我们写一个简单的对象

复制代码 代码如下:

function Animal(name) {
    this.name = name;
}
Animal.prototype.setName = function(name) {
    this.name = name;
}
var animal = new Animal("wangwang");

我们可以看到,这就是一个对象Animal,该对象有个属性name,有个方法setName。要注意,一旦修改prototype,比如增加某个方法,则该对象所有实例将同享这个方法。例如

复制代码 代码如下:

function Animal(name) {
    this.name = name;
}
var animal = new Animal("wangwang");

这时animal只有name属性。如果我们加上一句,

复制代码 代码如下:

Animal.prototype.setName = function(name) {
    this.name = name;
}

这时animal也会有setName方法。

继承本复制——从空的对象开始我们知道,JS的基本类型中,有一种叫做object,而它的最基本实例就是空的对象,即直接调用new Object()生成的实例,或者是用字面量{ }来声明。空的对象是“干净的对象”,只有预定义的属性和方法,而其他所有对象都是继承自空对象,因此所有的对象都拥有这些预定义的 属性与方法。原型其实也是一个对象实例。原型的含义是指:如果构造器有一个原型对象A,则由该构造器创建的实例都必然复制自A。由于实例复制自对象A,所以实例必然继承了A的所有属性、方法和其他性质。那么,复制又是怎么实现的呢?方法一:构造复制每构造一个实例,都从原型中复制出一个实例来,新的实例与原型占用了相同的内存空间。这虽然使得obj1、obj2与它们的原型“完全一致”,但也非常不经济——内存空间的消耗会急速增加。如图:


方法二:写时复制这种策略来自于一致欺骗系统的技术:写时复制。这种欺骗的典型示例就是操作系统中的动态链接库(DDL),它的内存区总是写时复制的。如图:


我们只要在系统中指明obj1和obj2等同于它们的原型,这样在读取的时候,只需要顺着指示去读原型即可。当需要写对象(例如obj2)的属性时,我们就复制一个原型的映像出来,并使以后的操作指向该映像即可。如图:


这种方式的优点是我们在创建实例和读属性的时候不需要大量内存开销,只在第一次写的时候会用一些代码来分配内存,并带来一些代码和内存上的开销。但此后就不再有这种开销了,因为访问映像和访问原型的效率是一致的。不过,对于经常进行写操作的系统来说,这种方法并不比上一种方法经济。方法三:读遍历这种方法把复制的粒度从原型变成了成员。这种方法的特点是:仅当写某个实例的成员,将成员的信息复制到实例映像中。当写对象属性时,例如(obj2.value=10)时,会产生一个名为value的属性值,放在obj2对象的成员列表中。看图:

可以发现,obj2仍然是一个指向原型的引用,在操作过程中也没有与原型相同大小的对象实例创建出来。这样,写操作并不导致大量的内存分配,因此内存的使用上就显得经济了。不同的是,obj2(以及所有的对象实例)需要维护一张成员列表。这个成员列表遵循两条规则:保证在读取时首先被访问到如果在对象中没有指定属性,则尝试遍历对象的整个原型链,直到原型为空或或找到该属性。原型链后面会讲。显然,三种方法中,读遍历是性能最优的。所以,JavaScript的原型继承是读遍历的。constructor熟悉C++的人看完最上面的对象的代码,肯定会疑惑。没有class关键字还好理解,毕竟有function关键字,关键字不一样而已。但是,构造函数呢?实际上,JavaScript也是有类似的构造函数的,只不过叫做构造器。在使用new运算符的时候,其实已经调用了构造器,并将this绑定为对象。例如,我们用以下的代码

复制代码 代码如下:

var animal = Animal("wangwang");

animal将是undefined。有人会说,没有返回值当然是undefined。那如果将Animal的对象定义改一下:

复制代码 代码如下:

function Animal(name) {
    this.name = name;
    return this;
}

猜猜现在animal是什么?
此时的animal变成window了,不同之处在于扩展了window,使得window有了name属性。这是因为this在没有指定的情况下,默认指向window,也即最顶层变量。只有调用new关键字,才能正确调用构造器。那么,如何避免用的人漏掉new关键字呢?我们可以做点小修改:

复制代码 代码如下:

function Animal(name) {
    if(!(this instanceof Animal)) {
        return new Animal(name);
    }
    this.name = name;
}

这样就万无一失了。构造器还有一个用处,标明实例是属于哪个对象的。我们可以用instanceof来判断,但instanceof在继承的时候对祖先对象跟真正对象都会返回true,所以不太适合。constructor在new调用时,默认指向当前对象。

复制代码 代码如下:

console.log(Animal.prototype.constructor === Animal); // true

我们可以换种思维:prototype在函数初始时根本是无值的,实现上可能是下面的逻辑

// 设定__proto__是函数内置的成员,get_prototyoe()是它的方法

复制代码 代码如下:

var __proto__ = null;
function get_prototype() {
    if(!__proto__) {
        __proto__ = new Object();
        __proto__.constructor = this;
    }
    return __proto__;
}

这样的好处是避免了每声明一个函数都创建一个对象实例,节省了开销。constructor是可以修改的,后面会讲到。基于原型的继承继承是什么相信大家都差不多知道,就不秀智商下限了。

JS的继承有好几种,这里讲两种

1. 方法一这种方法最常用,安全性也比较好。我们先定义两个对象

复制代码 代码如下:

function Animal(name) {
    this.name = name;
}
function Dog(age) {
    this.age = age;
}
var dog = new Dog(2);

要构造继承很简单,将子对象的原型指向父对象的实例(注意是实例,不是对象)

复制代码 代码如下:

Dog.prototype = new Animal("wangwang");

这时,dog就将有两个属性,name和age。而如果对dog使用instanceof操作符

复制代码 代码如下:

console.log(dog instanceof Animal); // true
console.log(dog instanceof Dog); // false

这样就实现了继承,但是有个小问题

复制代码 代码如下:

console.log(Dog.prototype.constructor === Animal); // true
console.log(Dog.prototype.constructor === Dog); // false

可以看到构造器指向的对象更改了,这样就不符合我们的目的了,我们无法判断我们new出来的实例属于谁。因此,我们可以加一句话:

复制代码 代码如下:

Dog.prototype.constructor = Dog;

再来看一下:

复制代码 代码如下:

console.log(dog instanceof Animal); // false
console.log(dog instanceof Dog); // true

done。这种方法是属于原型链的维护中的一环,下文将详细阐述。2. 方法二这种方法有它的好处,也有它的弊端,但弊大于利。先看代码

复制代码 代码如下:

function Animal(name) {<br>
    this.name = name;<br>
}<br>
Animal.prototype.setName = function(name) {<br>
    this.name = name;<br>
}<br>
function Dog(age) {<br>
    this.age = age;<br>
}<br>
Dog.prototype = Animal.prototype;<br>
登录后复制

这样就实现了prototype的拷贝。

这种方法的好处就是不需要实例化对象(和方法一相比),节省了资源。弊端也是明显,除了和上文一样的问题,即constructor指向了父对象,还只能复制父对象用prototype声明的属性和方法。也即是说,上述代码中,Animal对象的name属性得不到复制,但能复制setName方法。最最致命的是,对子对象的prototype的任何修改,都会影响父对象的prototype,也就是两个对象声明出来的实例都会受到影响。所以,不推荐这种方法。

原型链

写过继承的人都知道,继承可以多层继承。而在JS中,这种就构成了原型链。上文也多次提到了原型链,那么,原型链是什么?一个实例,至少应该拥有指向原型的proto属性,这是JavaScript中的对象系统的基础。不过这个属性是不可见的,我们称之为“内部原型链”,以便和构造器的prototype所组成的“构造器原型链”(亦即我们通常所说的“原型链”)区分开。我们先按上述代码构造一个简单的继承关系:

复制代码 代码如下:

function Animal(name) {
    this.name = name;
}
function Dog(age) {
    this.age = age;
}
var animal = new Animal("wangwang");
Dog.prototype = animal;
var dog = new Dog(2);

提醒一下,前文说过,所有对象都是继承空的对象的。所以,我们就构造了一个原型链:


我们可以看到,子对象的prototype指向父对象的实例,构成了构造器原型链。子实例的内部proto对象也是指向父对象的实例,构成了内部原型链。当我们需要寻找某个属性的时候,代码类似于

复制代码 代码如下:

function getAttrFromObj(attr, obj) {
    if(typeof(obj) === "object") {
        var proto = obj;
        while(proto) {
            if(proto.hasOwnProperty(attr)) {
                return proto[attr];
            }
            proto = proto.__proto__;
        }
    }
    return undefined;
}

在这个例子中,我们如果在dog中查找name属性,它将在dog中的成员列表中寻找,当然,会找不到,因为现在dog的成员列表只有age这一项。接着它会顺着原型链,即.proto指向的实例继续寻找,即animal中,找到了name属性,并将之返回。假如寻找的是一个不存在的属性,在animal中寻找不到时,它会继续顺着.proto寻找,找到了空的对象,找不到之后继续顺着.proto寻找,而空的对象的.proto指向null,寻找退出。

原型链的维护我们在刚才讲原型继承的时候提出了一个问题,使用方法一构造继承时,子对象实例的constructor指向的是父对象。这样的好处是我们可以通过constructor属性来访问原型链,坏处也是显而易见的。一个对象,它产生的实例应该指向它本身,也即是

复制代码 代码如下:

(new obj()).prototype.constructor === obj;

然后,当我们重写了原型属性之后,子对象产生的实例的constructor不是指向本身!这样就和构造器的初衷背道而驰了。我们在上面提到了一个解决方案:

复制代码 代码如下:

Dog.prototype = new Animal("wangwang");
Dog.prototype.constructor = Dog;

看起来没有什么问题了。但实际上,这又带来了一个新的问题,因为我们会发现,我们没法回溯原型链了,因为我们没法寻找到父对象,而内部原型链的.proto属性是无法访问的。于是,SpiderMonkey提供了一个改良方案:在任何创建的对象上添加了一个名为__proto__的属性,该属性总是指向构造器所用的原型。这样,对任何constructor的修改,都不会影响__proto__的值,就方便维护constructor了。

但是,这样又两个问题:

__proto__是可以重写的,这意味着使用它时仍然有风险

__proto__是spiderMonkey的特殊处理,在别的引擎(例如JScript)中是无法使用的。

我们还有一种办法,那就是保持原型的构造器属性,而在子类构造器函数内初始化实例的构造器属性。

代码如下:改写子对象

复制代码 代码如下:

function Dog(age) {
    this.constructor = arguments.callee;
    this.age = age;
}
Dog.prototype = new Animal("wangwang");

这样,所有子对象的实例的constructor都正确的指向该对象,而原型的constructor则指向父对象。虽然这种方法的效率比较低,因为每次构造实例都要重写constructor属性,但毫无疑问这种方法能有效解决之前的矛盾。ES5考虑到了这种情况,彻底的解决了这个问题:可以在任意时候使用Object.getPrototypeOf() 来获得一个对象的真实原型,而无须访问构造器或维护外部的原型链。因此,像上一节所说的寻找对象属性,我们可以如下改写:

复制代码 代码如下:

function getAttrFromObj(attr, obj) {
    if(typeof(obj) === "object") {
        do {
            var proto = Object.getPrototypeOf(dog);
            if(proto[attr]) {
                return proto[attr];
            }
        }
        while(proto);
    }
    return undefined;
}

当然,这种方法只能在支持ES5的浏览器中使用。为了向后兼容,我们还是需要考虑上一种方法的。更合适的方法是将这两种方法整合封装起来,这个相信读者们都非常擅长,这里就不献丑了。

本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

Video Face Swap

Video Face Swap

使用我们完全免费的人工智能换脸工具轻松在任何视频中换脸!

热门文章

<🎜>:泡泡胶模拟器无穷大 - 如何获取和使用皇家钥匙
3 周前 By 尊渡假赌尊渡假赌尊渡假赌
北端:融合系统,解释
3 周前 By 尊渡假赌尊渡假赌尊渡假赌
Mandragora:巫婆树的耳语 - 如何解锁抓钩
3 周前 By 尊渡假赌尊渡假赌尊渡假赌

热工具

记事本++7.3.1

记事本++7.3.1

好用且免费的代码编辑器

SublimeText3汉化版

SublimeText3汉化版

中文版,非常好用

禅工作室 13.0.1

禅工作室 13.0.1

功能强大的PHP集成开发环境

Dreamweaver CS6

Dreamweaver CS6

视觉化网页开发工具

SublimeText3 Mac版

SublimeText3 Mac版

神级代码编辑软件(SublimeText3)

热门话题

Java教程
1665
14
CakePHP 教程
1424
52
Laravel 教程
1321
25
PHP教程
1269
29
C# 教程
1249
24
如何使用WebSocket和JavaScript实现在线语音识别系统 如何使用WebSocket和JavaScript实现在线语音识别系统 Dec 17, 2023 pm 02:54 PM

如何使用WebSocket和JavaScript实现在线语音识别系统引言:随着科技的不断发展,语音识别技术已经成为了人工智能领域的重要组成部分。而基于WebSocket和JavaScript实现的在线语音识别系统,具备了低延迟、实时性和跨平台的特点,成为了一种被广泛应用的解决方案。本文将介绍如何使用WebSocket和JavaScript来实现在线语音识别系

WebSocket与JavaScript:实现实时监控系统的关键技术 WebSocket与JavaScript:实现实时监控系统的关键技术 Dec 17, 2023 pm 05:30 PM

WebSocket与JavaScript:实现实时监控系统的关键技术引言:随着互联网技术的快速发展,实时监控系统在各个领域中得到了广泛的应用。而实现实时监控的关键技术之一就是WebSocket与JavaScript的结合使用。本文将介绍WebSocket与JavaScript在实时监控系统中的应用,并给出代码示例,详细解释其实现原理。一、WebSocket技

如何利用JavaScript和WebSocket实现实时在线点餐系统 如何利用JavaScript和WebSocket实现实时在线点餐系统 Dec 17, 2023 pm 12:09 PM

如何利用JavaScript和WebSocket实现实时在线点餐系统介绍:随着互联网的普及和技术的进步,越来越多的餐厅开始提供在线点餐服务。为了实现实时在线点餐系统,我们可以利用JavaScript和WebSocket技术。WebSocket是一种基于TCP协议的全双工通信协议,可以实现客户端与服务器的实时双向通信。在实时在线点餐系统中,当用户选择菜品并下单

如何使用WebSocket和JavaScript实现在线预约系统 如何使用WebSocket和JavaScript实现在线预约系统 Dec 17, 2023 am 09:39 AM

如何使用WebSocket和JavaScript实现在线预约系统在当今数字化的时代,越来越多的业务和服务都需要提供在线预约功能。而实现一个高效、实时的在线预约系统是至关重要的。本文将介绍如何使用WebSocket和JavaScript来实现一个在线预约系统,并提供具体的代码示例。一、什么是WebSocketWebSocket是一种在单个TCP连接上进行全双工

JavaScript和WebSocket:打造高效的实时天气预报系统 JavaScript和WebSocket:打造高效的实时天气预报系统 Dec 17, 2023 pm 05:13 PM

JavaScript和WebSocket:打造高效的实时天气预报系统引言:如今,天气预报的准确性对于日常生活以及决策制定具有重要意义。随着技术的发展,我们可以通过实时获取天气数据来提供更准确可靠的天气预报。在本文中,我们将学习如何使用JavaScript和WebSocket技术,来构建一个高效的实时天气预报系统。本文将通过具体的代码示例来展示实现的过程。We

简易JavaScript教程:获取HTTP状态码的方法 简易JavaScript教程:获取HTTP状态码的方法 Jan 05, 2024 pm 06:08 PM

JavaScript教程:如何获取HTTP状态码,需要具体代码示例前言:在Web开发中,经常会涉及到与服务器进行数据交互的场景。在与服务器进行通信时,我们经常需要获取返回的HTTP状态码来判断操作是否成功,根据不同的状态码来进行相应的处理。本篇文章将教你如何使用JavaScript获取HTTP状态码,并提供一些实用的代码示例。使用XMLHttpRequest

javascript中如何使用insertBefore javascript中如何使用insertBefore Nov 24, 2023 am 11:56 AM

用法:在JavaScript中,insertBefore()方法用于在DOM树中插入一个新的节点。这个方法需要两个参数:要插入的新节点和参考节点(即新节点将要被插入的位置的节点)。

JavaScript和WebSocket:打造高效的实时图像处理系统 JavaScript和WebSocket:打造高效的实时图像处理系统 Dec 17, 2023 am 08:41 AM

JavaScript是一种广泛应用于Web开发的编程语言,而WebSocket则是一种用于实时通信的网络协议。结合二者的强大功能,我们可以打造一个高效的实时图像处理系统。本文将介绍如何利用JavaScript和WebSocket来实现这个系统,并提供具体的代码示例。首先,我们需要明确实时图像处理系统的需求和目标。假设我们有一个摄像头设备,可以采集实时的图像数

See all articles