JavaScript 继承详解及示例代码

有些知识当时实在看不懂的话,可以先暂且放下,留在以后再看也许就能看懂了。

几个月前,抱着《JavaScript 高级程序设计(第三版)》,啃完创建对象,就开始啃起了 继承 ,然而啃完 原型链 就实在是看不下去了,脑子越来越乱,然后就把它扔一边了,继续看后面的。现在利用这个暑假搞懂了这个继承,就把笔记整理一下啦。

原型链(Prototype Chaining)

先看一篇文章,文章作者讲的非常不错,并且还配高清套图哦。lol…

链接: [学习笔记] 小角度看JS原型链

从原文中小摘几句

  1. 构造函数通过 prototype 属性访问原型对象
  2. 实例对象通过 [[prototype]] 内部属性访问原型对象,浏览器实现了 proto 属性用于实例对象访问原型对象
  3. 一切对象都是Object的实例,一切函数都是Function的实例
  4. Object 是构造函数,既然是函数,那么就是Function的实例对象;Function是构造函数,但Function.prototype是对象,既然是对象,那么就是Object的实例对象

确定原型与实例的关系

有两种方法来检测原型与实例的关系:

instanceof :判断该对象是否为另一个对象的实例

instanceof 内部运算机理如下:

functioninstance_of(L, R){//L 表示左表达式,R 表示右表达式
varO = R.prototype;// 取 R 的显示原型
 L = L.__proto__; // 取 L 的隐式原型
while(true) {
if(L ===null)
returnfalse;
if(O === L)// 这里重点:当 O 严格等于 L 时,返回 true
returntrue;
 L = L.__proto__;
 }
}

上面代码摘自: JavaScript instanceof 运算符深入剖析

isPrototypeOf() :测试一个对象是否存在于另一个对象的原型链上
这两个方法的不同点请参看: JavaScript isPrototypeOf vs instanceof usage

只利用原型链实现继承

缺点:1. 引用类型值的原型属性会被实例共享; 2. 在创建子类型的实例时,不能向超类型的构造函数中传递参数

functionFather(){
this.name ="father";
this.friends = ['aaa','bbb'];
}
functionSon(){
}
Son.prototype = newFather();
Son.prototype.constructor = Son;

vars1 =newSon();
vars2 =newSon();

console.log(s1.name);// father
console.log(s2.name);// father
s1.name = "son";
console.log(s1.name);// son
console.log(s2.name);// father

console.log(s1.friends);// ["aaa", "bbb"]
console.log(s2.friends);// ["aaa", "bbb"]
s1.friends.push('ccc','ddd');
console.log(s1.friends);// ["aaa", "bbb", "ccc", "ddd"]
console.log(s2.friends);// ["aaa", "bbb", "ccc", "ddd"]

只利用构造函数实现继承

实现方法:在子类型构造函数的内部调用超类型构造函数(使用 apply() 和 call() 方法)

优点:解决了原型中引用类型属性的问题,并且子类可以向超类中传参

缺点:子类实例无法访问父类(超类)原型中定义的方法,所以函数复用就无从谈起了。

functionFather(name,friends){
this.name = name;
this.friends = friends;
}
Father.prototype.getName = function(){
returnthis.name;
};

functionSon(name){
// 注意: 为了确保 Father 构造函数不会重写 Son 构造函数的属性,请将调用 Father 构造函数的代码放在 Son 中定义的属性的前面。
 Father.call(this,name,['aaa','bbb']);

this.age =22;
}

vars1 =newSon('son1');
vars2 =newSon('son2');

console.log(s1.name);// son1
console.log(s2.name);// son2

s1.friends.push('ccc','ddd');
console.log(s1.friends);// ["aaa", "bbb", "ccc", "ddd"]
console.log(s2.friends);// ["aaa", "bbb"]

// 子类实例无法访问父类原型中的方法
s1.getName(); // TypeError: s1.getName is not a function
s2.getName(); // TypeError: s2.getName is not a function

组合继承(Combination Inheritance)

实现方法:使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。

functionFather(name,friends){
this.name = name;
this.friends = friends;
}
Father.prototype.money = "100k $";
Father.prototype.getName = function(){
console.log(this.name);
};

functionSon(name,age){
// 继承父类的属性
 Father.call(this,name,['aaa','bbb']);

this.age = age;
}

// 继承父类原型中的属性和方法
Son.prototype = newFather();
Son.prototype.constructor = Son;

Son.prototype.getAge = function(){
console.log(this.age);
};

vars1 =newSon('son1',12);
s1.friends.push('ccc');
console.log(s1.friends);// ["aaa", "bbb", "ccc"]
console.log(s1.money);// 100k $
s1.getName(); // son1
s1.getAge(); // 12

vars2 =newSon('son2',24);
console.log(s2.friends);// ["aaa", "bbb"]
console.log(s2.money);// 100k $
s2.getName(); // son2
s2.getAge(); // 24

组合继承避免了单方面使用原型链或构造函数来实现继承的缺陷,融合了它们的优点,成为 JavaScript 中最常用的继承模式,但是它也是有缺陷的,组合继承的缺陷会在后面专门提到。

原型式继承(Prototypal Inheritance)

实现思路:借助原型基于已有的对象创建新对象,同时不必因此而创建自定义类型。

为了达到这个目的,引入了下面的函数(obj)

functionobj(o){
functionF(){}
 F.prototype = o;
returnnewF();
}
varperson1 = {
 name: "percy",
 friends: ['aaa','bbb']
};
varperson2 = obj(person1);
person2.name = "zyj";
person2.friends.push('ccc');

console.log(person1.name);// percy
console.log(person2.name);// zyj
console.log(person1.friends);// ["aaa", "bbb", "ccc"]
console.log(person2.friends);// ["aaa", "bbb", "ccc"]
ECMAScript 5 通过新增 Object.create() 方法规范化了原型式继承。在传入一个参数的情况下, Object.create() 和 obj() 方法的行为相同。

varperson1 = {
 name: "percy",
 friends: ['aaa','bbb']
};
varperson2 =Object.create(person1);
person2.name = "zyj";
person2.friends.push('ccc');

console.log(person1.name);// percy
console.log(person2.name);// zyj
console.log(person1.friends);// ["aaa", "bbb", "ccc"]
console.log(person2.friends);// ["aaa", "bbb", "ccc"]

在没有必要兴师动众地创建构造函数,而只想让一个对象与另一个对象保持类似的情况下,可以选择使用这种继承。

寄生式继承(Parasitic Inheritance)

寄生式继承是与原型式继承紧密相关的一种思路。

实现思路:创建一个仅仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再返回对象。

functionobj(o){
functionF(){}
 F.prototype = o;
returnnewF();
}
functioncreatePerson(original){// 封装继承过程
varclone = obj(original);// 创建对象
 clone.showSomething = function(){// 增强对象
console.log("Hello world!");
 };
returnclone;// 返回对象
}

varperson = {
 name: "percy"
};
varperson1 = createPerson(person);
console.log(person1.name);// percy
person1.showSomething(); // Hello world!

寄生组合式继承(Parasitic Combination Inheritance)

先来说说我们前面的组合继承的缺陷。 组合继承最大的问题就是无论什么情况下,都会调用两次父类的构造函数:一次是创建子类的原型的时候,另一次是在调用子类构造函数的时候,在子类构造函数内部又调用了父类的构造函数。

functionFather(name,friends){
this.name = name;
this.friends = friends;
}
Father.prototype.money = "100k $";
Father.prototype.getName = function(){
console.log(this.name);
};

functionSon(name,age){
// 继承父类的属性
 Father.call(this,name,['aaa','bbb']);// 第二次调用 Father() , 实际是在 new Son() 时才会调用

this.age = age;
}

// 继承父类原型中的属性和方法
Son.prototype = newFather();// 第一次调用 Father()
Son.prototype.constructor = Son;

第一次调用使的子类的原型成了父类的一个实例,从而子类的原型得到了父类的实例属性;第二次调用会使得子类的实例也得到了父类的实例属性;而子类的实例属性默认会屏蔽掉子类原型中与其重名的属性。所以,经过这两次调用, 子类原型中出现了多余的的属性 ,从而引进了寄生组合式继承来解决这个问题。

寄生组合式继承的背后思路是: 不必为了指定子类的原型而调用父类的构造函数,我们所需要的无非就是父类原型的一个副本而已 。

本质上,就是使用寄生式继承来继承父类的原型,然后将结果返回给子类的原型。

functionobj(o){
functionF(){}
 F.prototype = o;
returnnewF();
}
functioninheritPrototype(son,father){
varprototype = obj(father.prototype);// 创建对象
 prototype.constructor = son; // 增强对象
 son.prototype = prototype; // 返回对象
}
functionFather(name,friends){
this.name = name;
this.friends = friends;
}
Father.prototype.money = "100k $";
Father.prototype.getName = function(){
console.log(this.name);
};

functionSon(name,age){
// 继承父类的属性
 Father.call(this,name,['aaa','bbb']);

this.age = age;
}

// 使用寄生式继承继承父类原型中的属性和方法
inheritPrototype(Son,Father);

Son.prototype.getAge = function(){
console.log(this.age);
};

vars1 =newSon('son1',12);
s1.friends.push('ccc');
console.log(s1.friends);// ["aaa", "bbb", "ccc"]
console.log(s1.money);// 100k $
s1.getName(); // son1
s1.getAge(); // 12

vars2 =newSon('son2',24);
console.log(s2.friends);// ["aaa", "bbb"]
console.log(s2.money);// 100k $
s2.getName(); // son2
s2.getAge(); // 24

优点:使子类原型避免了继承父类中不必要的实例属性。

开发人员普遍认为寄生组合式继承是实现基于类型继承的最理想的继承方式。

最后

最后,强烈推荐两篇很硬的文章

Javascript – How Prototypal Inheritance really works
JavaScript's Pseudo Classical Inheritance diagram (需要翻墙)

摘第二篇文章的一张硬图过来:

看完之后,秒懂原型链,有木有?

以上就是对JavaScript 继承的资料整理,后续继续补充相关资料谢谢大家对本站的支持!

(0)

相关推荐

  • 深入学习JavaWeb中监听器(Listener)的使用方法

    一.监听域对象中属性的变更的监听器 域对象中属性的变更的事件监听器就是用来监听 ServletContext, HttpSession, HttpServletRequest 这三个对象中的属性变更信息事件的监听器. 这三个监听器接口分别是ServletContextAttributeListener, HttpSessionAttributeListener 和ServletRequestAttributeListener,这三个接口中都定义了三个方法来处理被监听对象中的属性的增加,删除和替换

  • Java 中的字符串常量池详解

    Java中的字符串常量池 Java中字符串对象创建有两种形式,一种为字面量形式,如String str = "droid";,另一种就是使用new这种标准的构造对象的方法,如String str = new String("droid");,这两种方式我们在代码编写时都经常使用,尤其是字面量的方式.然而这两种实现其实存在着一些性能和内存占用的差别.这一切都是源于JVM为了减少字符串对象的重复创建,其维护了一个特殊的内存,这段内存被成为字符串常量池或者字符串字面量池.

  • javaweb 国际化:DateFormat,NumberFormat,MessageFormat,ResourceBundle的使用

    Javaweb 国际化 DateFormat:格式化日期的工具类,本身是一个抽象类: NumberFormat:格式化 数字 到 数字字符串,或货币字符串的字符类; MessageFormat: 可以格式化模式字符串,模式字符串: 带占位符的字符串: "Date: {0}, Salary: {1}",可以通过 format 方法会模式字符串进行格式化 ResourceBundle:资源包类,在类路径(src)下需要有对应的资源文件: baseName.properties. 其中 ba

  • JavaScript中push(),join() 函数 实例详解

    定义和用法 push方法 可向数组的末尾添加一个或多个元素,并返回一个新的长度. join方法 用于把数组中所有元素添加到一个指定的字符串,元素是通过指定的分隔符进行分割的. 语法 arrayObject.push(newelement1,newelement2,....,newelementX) arrayObject.join(separator). 参数描述newelement1必需.要添加到数组的第一个元素.newelement2可选.要添加到数组的第二个元素.newelementX可选

  • 基于JavaScript实现购物网站商品放大镜效果

    大家几乎都有在网上购物的经验,有的网站会有一个商品放大镜功能,就是把鼠标移到图片上的时候,旁边会有另外一张大的图片展示,等同于放大镜效果,那这样的效果怎样实现的呢,我把代码发给大家,请大家参考. <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>放大镜</title> <style> *{mar

  • Java Socket编程详解及示例代码

    Socket,又称为套接字,Socket是计算机网络通信的基本的技术之一.如今大多数基于网络的软件,如浏览器,即时通讯工具甚至是P2P下载都是基于Socket实现的.本文会介绍一下基于TCP/IP的Socket编程,并且如何写一个客户端/服务器程序. 餐前甜点 Unix的输入输出(IO)系统遵循Open-Read-Write-Close这样的操作范本.当一个用户进程进行IO操作之前,它需要调用Open来指定并获取待操作文件或设备读取或写入的权限.一旦IO操作对象被打开,那么这个用户进程可以对这个

  • 快速学习JavaWeb中监听器(Listener)的使用方法

    一.监听器介绍 1.1.监听器的概念 监听器是一个专门用于对其他对象身上发生的事件或状态改变进行监听和相应处理的对象,当被监视的对象发生情况时,立即采取相应的行动.监听器其实就是一个实现特定接口的普通java程序,这个程序专门用于监听另一个java对象的方法调用或属性改变,当被监听对象发生上述事件后,监听器某个方法立即被执行. 1.2.监听器案例--监听window窗口的事件监听器 package me.gacl.listener.demo; import java.awt.Frame; imp

  • Java的Struts框架中配置国际化的资源存储的要点解析

    1.资源文件命名 国际化资源文件命名格式如下: basename_language_country.properties basename_language.properties basename.properties 其中basename为资源文件的基本名称,language和country必须是java支持的语言和国家.获取java支持的语言和国家代码如下: Locale[] locales = Locale.getAvailableLocales(); for(Locale locale:

  • java指纹识别以及谷歌图片识别技术源码

    本文实例为大家分享了java指纹识别和图片识别源代码,供大家参考,具体内容如下 主类: import java.awt.image.BufferedImage; import java.util.ArrayList; import java.util.List; public class SimilarImageSearch { /** * @param args */ public static void main(String[] args) { List<String> hashCode

  • java按字节截取带有汉字的字符串的解法(推荐)

    由于接口使用的oracle字段长度为固定字节数,然后传进来的字符串估计比数据库字段的总字节数要大,那么截取小于数据库字节数的字符串. 自己参考网上的例子,整了个递归调用就可以了,因为截取的字符字节长度必须小与数据库的字节长度,即如果最后一个字符为汉字,那么只能去掉往前截取. /** * 判断传进来的字符串,是否 * 大于指定的字节,如果大于递归调用 * 直到小于指定字节数 ,一定要指定字符编码,因为各个系统字符编码都不一样,字节数也不一样 * @param s * 原始字符串 * @param

  • Java的Struts框架中的主题模板和国际化设置

    主题模板 如果不指定一个主题,然后Struts2中会使用默认的XHTML主题.例如Struts 2中选择标签: <s:textfield name="name" label="Name" /> 生成HTML标记: <tr> <td class="tdLabel"> <label for="empinfo_name" class="label">Name:<

  • 详解Java中用于国际化的locale类

    1. Locale 介绍 Locale 表示地区.每一个Locale对象都代表了一个特定的地理.政治和文化地区. 在操作 Date, Calendar等表示日期/时间的对象时,经常会用到:因为不同的区域,时间表示方式都不同. 下面说说Locale对象的3种常用创建方式. (1)获取默认的Locale 使用方法: Locale locale = Locale.getDefault() (2) 直接使用Locale的静态对象 Locale.java中提供了以下静态对象 public static f

随机推荐