一、前言

继承是面向对象(OOP)语言中的一个最为人津津乐道的概念。许多面对对象(OOP)语言都支持两种继承方式::接口继承 和 实现继承 。

接口继承只继承方法签名,而实现继承则继承实际的方法。由于js中方法没有签名,在ECMAScript中无法实现接口继承。ECMAScript只支持实现继承,而且其 实现继承 主要是依靠原型链来实现的。

二、概念

2.1简单回顾下构造函数,原型和实例的关系:

每个构造函数(constructor)都有一个原型对象(prototype),原型对象都包含一个指向构造函数的指针,而实例(instance)都包含一个指向原型对象的内部指针。

关系如下图所示:

​

2.2什么是原型链

每一个对象拥有一个原型对象,通过__proto__指针指向上一个原型,并从中继承方法和属性,同时原型对象也可能拥有原型,这样一层层的,最终指向null。这种关系成为原型链(prototype chain)。

假如有这样一个要求:instance实例通过原型链找到了Father原型中的getFatherValue方法.

function Father(){
this.property = true;
}
Father.prototype.getFatherValue = function(){
return this.property;
}
function Son(){
this.sonProperty = false;
}
//继承 Father
Son.prototype = new Father();//Son.prototype被重写,导致Son.prototype.constructor也一同被重写
Son.prototype.getSonVaule = function(){
return this.sonProperty;
}
var instance = new Son();
console.log(instance.getFatherValue());//true

注意: 此时instance.constructor指向的是Father,这是因为Son.prototype中的constructor被重写的缘故.

2.3确定原型和实例之间的关系

使用原型链后, 我们怎么去判断原型和实例的这种继承关系呢? 方法一般有两种.

1、第一种是使用 instanceof 操作符。只要用这个操作符来测试实例(instance)与原型链中出现过的构造函数,结果就会返回true. 以下几行代码就说明了这点。

console.log(instance instanceof Object);//true
console.log(instance instanceof Father);//true
console.log(instance instanceof Son);//true

由于原型链的关系, 我们可以说instance 是 Object, Father 或 Son中任何一个类型的实例. 因此, 这三个构造函数的结果都返回了true.

2、第二种是使用 isPrototypeOf() 方法,。同样只要是原型链中出现过的原型,isPrototypeOf() 方法就会返回true

console.log(Object.prototype.isPrototypeOf(instance));//true
console.log(Father.prototype.isPrototypeOf(instance));//true
console.log(Son.prototype.isPrototypeOf(instance));//true

2.4原型链的问题

原型链并非十分完美, 它包含如下两个问题:

问题一: 当原型链中包含引用类型值的原型时,该引用类型值会被所有实例共享;

问题二: 在创建子类型(例如创建Son的实例)时,不能向超类型(例如Father)的构造函数中传递参数.

有鉴于此, 实践中很少会单独使用原型链。为此,下面将有一些尝试以弥补原型链的不足。

三、借用构造函数(经典继承)

为解决原型链中上述两个问题, 我们开始使用一种叫做借用构造函数(constructor stealing)的技术(也叫经典继承).

基本思想:即在子类型构造函数的内部调用超类型构造函数。

function Father(){
this.colors = ["red","blue","green"];
}
function Son(){
Father.call(this);//继承了Father,且向父类型传递参数
}
var instance1 = new Son();
instance1.colors.push("black");
console.log(instance1.colors);//"red,blue,green,black" var instance2 = new Son();
console.log(instance2.colors);//"red,blue,green" 可见引用类型值是独立的

很明显,借用构造函数一举解决了原型链的两大问题:

其一, 保证了原型链中引用类型值的独立,不再被所有实例共享;

其二, 子类型创建时也能够向父类型传递参数.

随之而来的是, 如果仅仅借用构造函数,那么将无法避免构造函数模式存在的问题–方法都在构造函数中定义, 因此函数复用也就不可用了.而且超类型(如Father)中定义的方法,对子类型而言也是不可见的. 考虑此,借用构造函数的技术也很少单独使用。

四、组合继承

组合继承, 有时候也叫做伪经典继承,指的是将原型链和借用构造函数的技术组合到一块,从而发挥两者之长的一种继承模式.

基本思路: 使用原型链实现对原型属性和方法的继承,通过借用构造函数来实现对实例属性的继承.

这样,既通过在原型上定义方法实现了函数复用,又能保证每个实例都有它自己的属性. 如下所示.

function Father(name){
this.name = name;
this.colors = ["red","blue","green"];
}
Father.prototype.sayName = function(){
alert(this.name);
};
function Son(name,age){
Father.call(this,name);//继承实例属性,第一次调用Father()
this.age = age;
}
Son.prototype = new Father();//继承父类方法,第二次调用Father()
Son.prototype.sayAge = function(){
alert(this.age);
}
var instance1 = new Son("louis",5);
instance1.colors.push("black");
console.log(instance1.colors);//"red,blue,green,black"
instance1.sayName();//louis
instance1.sayAge();//5 var instance1 = new Son("zhai",10);
console.log(instance1.colors);//"red,blue,green"
instance1.sayName();//zhai
instance1.sayAge();//10

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

同时我们还注意到组合继承其实调用了两次父类构造函数, 造成了不必要的消耗, 那么怎样才能避免这种不必要的消耗呢, 这个我们将在后面讲到。

五、原型继承

该方法最初由道格拉斯·克罗克福德于2006年在一篇题为 《Prototypal Inheritance in JavaScript》(JavaScript中的原型式继承) 的文章中提出. 他的想法是借助原型可以基于已有的对象创建新对象, 同时还不必因此创建自定义类型. 大意如下:

在object()函数内部, 先创建一个临时性的构造函数, 然后将传入的对象作为这个构造函数的原型,最后返回了这个临时类型的一个新实例。

function object(o){
function F(){}
F.prototype = o;
return new F();
}

从本质上讲, object() 对传入其中的对象执行了一次浅复制. 下面我们来看看为什么是浅复制.

在 ECMAScript5 中,通过新增 object.create() 方法规范化了上面的原型式继承.

object.create() 接收两个参数:1、一个用作新对象原型的对象;2、(可选的)一个为新对象定义额外属性的对象

var person = {
friends : ["Van","Louis","Nick"]
};
var anotherPerson = Object.create(person);
anotherPerson.friends.push("Rob"); var yetAnotherPerson = Object.create(person);
yetAnotherPerson.friends.push("Style"); alert(person.friends);//"Van,Louis,Nick,Rob,Style"

object.create() 只有一个参数时功能与上述object方法相同, 它的第二个参数与Object.defineProperties()方法的第二个参数格式相同: 每个属性都是通过自己的描述符定义的.以这种方式指定的任何属性都会覆盖原型对象上的同名属性.例如:

var person = {
name : "Van"
};
var anotherPerson = Object.create(person, {
name : {
value : "Louis"
}
});
alert(anotherPerson.name);//"Louis"

目前支持 Object.create() 的浏览器有 IE9+, Firefox 4+, Safari 5+, Opera 12+ 和 Chrome.

提醒: 原型式继承中, 包含引用类型值的属性始终都会共享相应的值, 就像使用原型模式一样.

六、寄生式继承

寄生式继承是与原型式继承紧密相关的一种思路, 同样是克罗克福德推而广之.

寄生式继承的思路与(寄生)构造函数和工厂模式类似, 即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真的是它做了所有工作一样返回对象. 如下.

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

这个例子中的代码基于person返回了一个新对象–anotherPerson. 新对象不仅具有 person 的所有属性和方法, 而且还被增强了, 拥有了sayH()方法.

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

七、寄生组合式继承

前面讲过,组合继承是 JavaScript 最常用的继承模式; 不过, 它也有自己的不足. 组合继承最大的问题就是无论什么情况下,都会调用两次父类构造函数: 一次是在创建子类型原型的时候, 另一次是在子类型构造函数内部.

寄生组合式继承就是为了降低调用父类构造函数的开销而出现的

其背后的基本思路是: 不必为了指定子类型的原型而调用超类型的构造函数

function extend(subClass,superClass){
var prototype = object(superClass.prototype);//创建对象
prototype.constructor = subClass;//增强对象
subClass.prototype = prototype;//指定对象
}

extend的高效率体现在它没有调用superClass构造函数,因此避免了在subClass.prototype上面创建不必要,多余的属性. 于此同时,原型链还能保持不变; 因此还能正常使用 instanceof 和 isPrototypeOf() 方法。

综上所述有:原型链继承,构造函数继承(经典继承),组合继承,寄生继承,寄生组合继承五种方法,寄生组合式继承,集寄生式继承和组合继承的优点于一身,是实现基于类型继承的最有效方法。

下面我们来看下extend的另一种更为有效的扩展.

function extend(subClass, superClass) {
var F = function() {};
F.prototype = superClass.prototype;
subClass.prototype = new F();
subClass.prototype.constructor = subClass; subClass.superclass = superClass.prototype;
if(superClass.prototype.constructor == Object.prototype.constructor) {
superClass.prototype.constructor = superClass;
}
}

我一直不太明白的是为什么要 "new F()", 既然extend的目的是将子类型的 prototype 指向超类型的 prototype,为什么不直接做如下操作呢?

subClass.prototype = superClass.prototype;//直接指向超类型prototype

显然, 基于如上操作, 子类型原型将与超类型原型共用, 根本就没有继承关系.

八、new操作符

为了追本溯源, 我顺便研究了new运算符具体干了什么?发现其实很简单,就干了三件事情.

var obj  = {};
obj.__proto__ = F.prototype;
F.call(obj);

第一行,我们创建了一个空对象obj;

第二行,我们将这个空对象的__proto__成员指向了F构造函数的prototype对象;

第三行,我们将F函数对象的this指针替换成obj,然后再调用F函数.

 new 操作符调用构造函数的时候,函数内部实际上发生以下变化:

1、创建一个空对象,并且 this 变量引用该对象,同时还继承了该函数的原型。

2、属性和方法被加入到 this 引用的对象中。

3、新创建的对象由 this 所引用,并且最后隐式的返回 this.

九、属性查找

使用了原型链后, 当查找一个对象的属性时,JavaScript 会向上遍历原型链,直到找到给定名称的属性为止,到查找到达原型链的顶部 - 也就是 Object.prototype - 但是仍然没有找到指定的属性,就会返回 undefined。

此时若想避免原型链查找, 建议使用hasOwnProperty方法. 因为hasOwnProperty是 JavaScript 中唯一一个处理属性但是不查找原型链的函数。

console.log(instance1.hasOwnProperty('age'));//true

对比: isPrototypeOf 则是用来判断该方法实例对象是不是参数的原型对象,是则返回true,否则返回false。如

console.log(Father.prototype.isPrototypeOf(instance1));//true

十、instanceof && typeof

instanceof 运算符是用来在运行时指出对象是否是构造器的一个实例。

instanceof 可以正确的判断对象的类型,因为内部机制是通过判断对象的原型链中是不是能找到类型的 prototype。

例如漏写了new运算符去调用某个构造器, 此时构造器内部可以通过 instanceof 来判断.(java中功能类似)

function f(){
if(this instanceof arguments.callee)
console.log('此处作为构造函数被调用');
else
console.log('此处作为普通函数被调用');
}
f();//此处作为普通函数被调用
new f();//此处作为构造函数被调用

对比: typeof 则用以获取一个变量或者表达式的类型, 一般只能返回如下几个结果:

1、typeof 对于基本类型,除了 null 都可以显示正确的类型

2、typeof 对于对象,除了函数都会显示 object

3、对于 null 来说,虽然它是基本类型,但是会显示 object

总的来说:number,boolean,string,function(函数),object(NULL,数组,对象),undefined。

十一、参考

1、《JavaScript高级程序设计》

2、深入理解JavaScript系列(5):强大的原型和原型链 - 汤姆大叔 - 博客园

JavaScript系列--浅析原型链与继承的更多相关文章

  1. 对Javascript 类、原型链、继承的理解

    一.序言   和其他面向对象的语言(如Java)不同,Javascript语言对类的实现和继承的实现没有标准的定义,而是将这些交给了程序员,让程序员更加灵活地(当然刚开始也更加头疼)去定义类,实现继承 ...

  2. JavaScript中的原型链和继承

    理解原型链 在 JavaScript 的世界中,函数是一等公民. 上面这句话在很多地方都看到过.用我自己的话来理解就是:函数既当爹又当妈."当爹"是因为我们用函数去处理各种&quo ...

  3. (四)我的JavaScript系列:原型链

    夜深风竹敲秋韵,万叶千声皆是恨. 原型链对于JavaScript来说是个很核心的概念.JavaScript不是基于类模板的面向对象语言:反而,它的面向对象机制是基于原型的.我们不可能说某个对象属于什么 ...

  4. JavaScript之基于原型链的继承

    本文介绍下js的OOP中的继承. 上图的要点为:Foo函数在创建时会自动生成内置属性prototype,而typeof Foo.prototype是object类型的. 上图的要点为:Foo.prot ...

  5. JavaScript原型链与继承

    最近学习了<Javascript高级程序设计>面向对象部分,结合书中的例子总结一下原型链和继承部分的内容. 创建对象 在Js当中没有类这个概念,当我们想要创建具有相同属性的对象的时候,有如 ...

  6. 《JAVASCRIPT高级程序设计》根植于原型链的继承

    继承是面向对象的语言中,一个最为津津乐道并乐此不疲的话题之一.JAVASCRIPT中的继承,主要是依靠原型链来实现的.上一篇文章介绍过,JAVASCRIPT中,每一个对象都有一个prototype属性 ...

  7. JavaScript原型链及继承

    在JavaScript中,所有的东西都是对象,但是JavaScript中的面向对象并不是面向类,而是面向原型的,这是与C++.Java等面向对象语言的区别,比较容易混淆,因此把我自己学习的过程记录下来 ...

  8. JavaScript原型链和继承

    1.概念 JavaScript并不提供一个class的实现,在ES6中提供class关键字,但是这个只是一个语法糖,JavaScript仍然是基于原型的.JavaScript只有一种结构:对象.每个对 ...

  9. 三张图搞懂JavaScript的原型对象与原型链 / js继承,各种继承的优缺点(原型链继承,组合继承,寄生组合继承)

    摘自:https://www.cnblogs.com/shuiyi/p/5305435.html 对于新人来说,JavaScript的原型是一个很让人头疼的事情,一来prototype容易与__pro ...

随机推荐

  1. APP开发过程的优惠券设计及流程

    在整个APP开发产品发展的整个周期中,运营活动必不可少,而发放优惠券已成为运营活动的一种基本形式,而关于优惠券设计的整体流程尤为重要.接下来,常州开发APP公司专家分享一下自己的经验,希望对大家有帮助 ...

  2. C# 5.0新加特性

    1. 异步编程 在.Net 4.5中,通过async和await两个关键字,引入了一种新的基于任务的异步编程模型(TAP).在这种方式下,可以通过类似同步方式编写异步代码,极大简化了异步编程模型.如下 ...

  3. Java入门第一季——从此投身Java??

    找工作告一段落. 最后的工作呢,和java紧密相关,也是阴差阳错,不过都是软件开发,都好了,不过以后侧重点肯定是在java这边,php有机会还是一直学下去的,那么美的说~ Java开发第一季  一.简 ...

  4. 07 --C语言字符串函数

    1)字符串操作  复制 strcpy(p, p1)      复制字符串 strncpy(p, p1, n)  复制指定长度字符串 strdup(char *str)      将串拷贝到新建的位置处 ...

  5. Java常用类(I)-时间和日期

    java开发中,常涉及到时间的处理,这里就做一个总结,同样也是一个笔记. 相关类及概念 1. java.util.Date:表示特定的瞬间,精确到毫秒.由于API 不易于实现国际化,日期和时间字段之间 ...

  6. 【转】DBSCAN密度聚类算法

    DBSCAN(Density-Based Spatial Clustering of Applications with Noise,具有噪声的基于密度的聚类方法)是一种很典型的密度聚类算法,和K-M ...

  7. java中String和int的互相转化

    1. String 转 int 方式1:Integer.parseInt(); 方式2: Integer.valueOf(myStr).intValue(); 2.  int 转String 方式1: ...

  8. ubuntu重置root密码(转载自https://zhinan.sogou.com/guide/detail/?id=316512881651)

    ubuntu忘记root密码怎么办?如果普通用户忘记了怎么办 ### 第一种方法:无论你是否申请了root帐号,或是普通账号密码忘记了都没有问题的! 1. 重启ubuntu,随即长按shift进入gr ...

  9. P1828 香甜的黄油 Sweet Butter (spfa)

    题目描述 农夫John发现做出全威斯康辛州最甜的黄油的方法:糖.把糖放在一片牧场上,他知道N(1<=N<=500)只奶牛会过来舔它,这样就能做出能卖好价钱的超甜黄油.当然,他将付出额外的费 ...

  10. 4.1、Ansible模块

    ansible-doc -l    列出所有模块 ansible-doc 模块名    查看模块的help说明 ansible-doc -s  module_name:获取指定模块的使用信息 ***文 ...