JavaScript与大部分客户端语言有几点明显的不同:

JS是 动态解释性语言,没有编译过程,它在程序运行过程中被逐行解释执行
JS是 弱类型语言,它的变量没有严格类型限制
JS是面向对象语言,但 没有明确的类的概念(虽然有class关键字,然而目前并没有什么卵用)
JS虽然没有类,但可以通过一些方法来模拟类以及实现类的继承。
一切皆对象,还先从对象说起。

1、对象(Object)


ECMA-262对对象的定义是:无序属性的集合,其属性可以包含基本值、对象或者函数。
直观点描述,就是由多个键值对组成的散列表。

JS创建对象的方法和其它语言大同小异:

// 通过构造函数创建
var zhangsan = new Object();
zhangsan.name = "张三";
zhangsan.age = 20;
zhangsan.sayHi = function() {
alert("Hi, I'm " + this.name);
}; // 通过对象字面量创建
var lisi = {
name: "李四",
age: 21,
sayHi: function() {
alert("Hi, I'm " + this.name);
}
};

当需要大量创建相同结构的对象时,可以使用 对象工厂(Object Factory)

// 对象工厂
function createPerson(name, age) {
return {
name: name,
age: age,
sayHi: function() {
alert("Hi, I'm " + this.name);
}
};
} var zhangsan = createPerson("张三", 20);
var lisi = createPerson("李四", 21);

但通过这种方式创建出来的实例,不能解决类型识别问题,只知道它是一个对象,但具体什么?无法判断:

zhangsan instanceof ?
lisi.constructor = ?

这时,“类”就登场了。

2、类(Class)


2.1、构造函数模式

事实上,JS中每个函数(function)本身就是一个构造函数(constructor),就是一个类:

// 构造函数模式
function Person(name, age) {
this.name = name;
this.age = age;
this.sayHi = function() {
alert("Hi, I'm " + this.name);
};
} var zhangsan = new Person("张三", 20);
var lisi = new Person("李四", 21);
alert(zhangsan instanceof Person); // true
alert(lisi.constructor === Person); // true

这里面其实有个问题:

alert(zhangsan.sayHi === lisi.sayHi); // false

多个实例中的同名方法并不相等,也就是说存在多个副本。而这些行为是相同的,应该指向同一个引用才对。

为了解决这个问题,JS为每个函数分配了一个 prototype(原型)属性,该属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。

2.2、原型模式

原型(Prototype):指向一个对象,作为所有实例的基引用(base reference)。

// 构造函数+原型组合模式
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.sayHi = function() {
alert("Hi, I'm " + this.name);
}; var zhangsan = new Person("张三", 20);
var lisi = new Person("李四", 21);
alert(zhangsan.sayHi === lisi.sayHi); // true

在Person中,sayHi 是 原型成员(prototype),name 和 age 是 特权成员(privileged),它们都是 公共成员(public)

注:“特权”是道格拉斯提出的名词。道格拉斯·克罗克福德(Douglas Crockford),Web界人称道爷,JSON创立者,《JavaScript语言精粹》作者,JSLint、JSMin、ADsafe开发者。

类的原型带有一个 constructor 属性,指向该类的构造函数(如果重新分配了原型指针,需要手动添加 constructor 属性);类的实例上会自动生成一个属性指向该类原型(在Chrome上可以通过“__proto__”访问到该对象,而IE上该属性则是不可见的)。
Person、Person的原型、Person的实例间的关系如下:

需要注意的是,原型成员保存引用类型值时需谨慎:

Person.prototype.friends = [];
zhangsan.friends.push("王五");
alert(lisi.friends); // ["王五"]

张三的基友莫名其妙就变成李四的基友了,所以 friends 应该添加为特权成员,而不是原型成员。

2.3、类的结构

综上所述,JS中的类的结构大致如下:

  • 类由构造函数和原型组成
  • 构造函数中可以声明私有成员和添加特权成员
  • 原型中可以添加原型成员
  • 私有成员可以被特权成员访问而对原型成员不可见
  • 特权成员和原型成员都是公共成员

3、继承(Inherit)


在JS中继承是如何实现的呢?

3.1、拷贝继承

最简单直接的方式莫过于 属性拷贝

// 拷贝继承
function extend(destination, source) {
for (var property in source) {
destination[property] = source[property];
}
}
extend(SubClass.prototype, SuperClass.prototype);

这种方式虽然实现了原型属性的继承,但有一个非常明显的缺陷:子类实例无法通过父类的 instanceof 验证,换句话说,子类的实例不是父类的实例。

3.2、原型继承

在 Chrome 的控制台中查看 HTMLElement 的原型,大致如下:

可以清晰看到,HTMLElement 的原型是 Element 的实例,而 Element 的原型又是 Node 的实例,从而形成了一条 原型链(Prototype-chain),JS的原生对象就是通过原型链来实现继承。

这里顺道说下解释器对实例属性的查找过程:

  1. 在特权属性中查找
  2. 特权属性中没找到,再到原型属性中查找
  3. 原型属性中没找到,再到原型的原型属性中查找
  4. 直到根原型还没找到,返回 undefined

这就说明为什么我们自定义的类明明没有声明 toString() 方法,但仍然可以访问到,因为所有对象都继承自 Object。

因此,我们也可以通过原型链来实现继承:

// 原型链继承
function User(name, age, password) {
// 继承特权成员
Person.call(this, name, age);
this.password = password;
}
// 继承原型
User.prototype = new Person();
// 修改了原型指针,需重新设置 constructor 属性
User.prototype.constructor = User; var zhangsan = new User("张三", 20, "123456");
zhangsan.sayHi(); // Hi, I'm 张三

运行正常,貌似没什么问题,但其实里面还是有些坑:

父类的构造函数被执行了 2 次:继承特权成员时 1 次,继承原型时又 1 次。
父类初始化两次,这有时会导致一些问题,举个例子,父类构造函数中有个alert,那么创建子类实例时,会发现有两次弹框。
不仅如此,还导致了下面的问题。从控制台中查看子类的实例,结构如下:

可以看到子类的原型中也包含了父类的特权成员(直接创建了一个父类实例,当然会有特权成员),只不过因为解释器的属性查找机制,被子类的特权成员所覆盖,只要子类的特权成员被删除,原型中相应的成员就会暴露出来:

delete zhangsan.name;
alert(zhangsan.name); // 此时访问到的就是原型中的name

那怎么办呢?对此道爷提供了一个很实用的解决方案—— 原型式寄生组合继承

3.3、原型式寄生组合继承

我们的目的是子类原型只继承父类的原型,而不要特权成员,原理其实很简单:创建一个临时的类,让其原型指向父类原型,然后将子类原型指向该临时类的实例即可。实现如下:

function inheritPrototype(subClass, superClass) {
function Temp() {}
Temp.prototype = superClass.prototype;
subClass.prototype = new Temp();
subClass.prototype.constructor = subClass;
}
inheritPrototype(User, Person);

因为临时类的构造函数是空实现,子类在继承原型时自然不会执行到父类的初始化操作,也不会继承到一堆乱七八糟的特权成员。

再看下 zhangsan 的结构:

此时,子类实例的原型链大致如下:

总结


修改后的代码整理如下:

// 用于子类继承父类原型的工具函数
function inheritPrototype(subClass, superClass) {
function Temp() {}
Temp.prototype = superClass.prototype;
subClass.prototype = new Temp();
subClass.prototype.constructor = subClass;
} // 父类
function Person(name, age) {
// 特权成员(每个实例都有一份副本)
this.name = name;
this.age = age;
}
// 原型成员(所有实例共享)
Person.prototype.sayHi = function() {
alert("Hi, I'm " + this.name);
}; // 子类
function User(name, age, password) {
// 继承父类特权成员(在子类中执行父类的初始化操作)
Person.call(this, name, age);
// 添加新的特权成员
this.password = password;
}
// 继承父类原型
inheritPrototype(User, Person);
// 重写父类原型方法
User.prototype.sayHi = function() {
alert("Hi, my name is " + this.name);
};
// 扩展子类原型
User.prototype.getPassword = function() {
return this.password;
};

到此为止,我们已经比较完美地实现了类和类的继承。

但每个类、每个子类、每个子类的子类,都要这么分几步写,也是很蛋疼的。对象有对象工厂,类当然也可以搞个 类工厂(Class Factory),江湖上已有不少现成的类工厂,让我们可以从统一规范的入口来生成自定义类。(见:JavaScript几种类工厂实现原理剖析

JavaScript里的类和继承的更多相关文章

  1. JavaScript里的类和继承(转)

    转自: http://www.h5cn.com/js/jishu/2016/0121/17634.html js与大部分客户端语言有几点明显的不同: JS是 动态解释性语言,没有编译过程,它在程序运行 ...

  2. JavaScript中的类式继承和原型式继承

    最近在看<JavaScript设计模式>这本书,虽然内容比较晦涩,但是细品才发现此书内容的强大.刚看完第四章--继承,来做下笔记. 书中介绍了三种继承方式,类式继承.原型式继承和掺元类继承 ...

  3. 在JavaScript里写类层次结构?别那么做!

    从理论上讲,JavaScript并没有类.在实践中,下面的代码片段被广泛认为是JavaScript“类”的一个例子: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 fu ...

  4. es6 javascript的Class 类的继承

    原文链接:https://blog.csdn.net/qq_30100043/article/details/53542531 1 基本用法 Class 之间可以通过extends关键字实现继承, 这 ...

  5. 详谈Javascript类与继承

    本文将从以下几方面介绍类与继承 类的声明与实例化 如何实现继承 继承的几种方式 类的声明与实例化 类的声明一般有两种方式 //类的声明 var Animal = function () { this. ...

  6. javascript 和 CoffeeScript 里的类

    javascript不是面向对象的语言,它用函数来模拟类和继承. javascript里,提供一个类并不难: var Person,l4, z3; Person = function(name) { ...

  7. Java编程里类的继承

    今天,我们将要讨论的内容是Java里面类的继承的相关概念. 说到继承,我相信大家都不陌生.生活中,子承父业,子女继承父母的财产,这就是继承.实际上,Java里的继承也是如此.对于一个类来说,它的数据成 ...

  8. JavaScript 类式继承与原型继承

    交叉着写Java和Javascript都有2年多了,今天来总结下自己所了解的Javascript类与继承. Javascript本身没有类似Java的面向对象的类与继承术语,但其基于原型对象的思想却可 ...

  9. Javascript学习6 - 类、对象、继承

    原文:Javascript学习6 - 类.对象.继承 Javasciprt并不像C++一样支持真正的类,也不是用class关键字来定义类.Javascript定义类也是使用function关键字来完成 ...

随机推荐

  1. PXE安装linux系统

    利用网络安装系统流程:1.配置dhcp,让客户端能够自动获取ip,在dhcp配置中添加pxelinux.0配置,使客户端连接tftp文件2.复制光盘镜像的isolinux文件夹下面的所有文件到tftp ...

  2. Linux FTP服务安装和远程登录失败

    问题:本机VPlayer安装pure-ftpd  ftp服务,通过flashfxp从windows连接出现以下错误: [左] 正在连接到 vmare -> IP=192.168.174.133 ...

  3. 关于ligerUi的ligertree的初始化默认选中指定项目的方法

    LigerUi中ligerTree官方示例代码片段: var parm = function (data) { return data.text.indexOf('节点1.3') == 0; }; t ...

  4. mysql 之权限介绍

    转自:http://tech.it168.com/a2010/0114/837/000000837456_all.shtml 一.MySQL授权表概述首先从全局开始,如果全局的是允许的,即在 user ...

  5. WPF 渐隐渐现切换背景图片

    最近学习WPF,尝试着自己做一些小玩意,也遇到了一些问题,于是整理记录以便日后查阅. 我们都知道WPF可以实现一些很炫的效果,然而有时候为达到这个目的却并不是一件很容易的事情.比如:在软件中我希望能够 ...

  6. 【BZOJ】1046 : [HAOI2007]上升序列

    1046: [HAOI2007]上升序列 题意:给定S={a1,a2,a3,…,an}问是否存在P={ax1,ax2,ax3,…,axm},满足(x1 < x2 < … < xm)且 ...

  7. 信号量 sem_undo设置

    一 为什么要使用信号量 为了防止出现因多个程序同时访问一个共享资源而引发的一系列问题,我们需要一种方法,它可以通过生成并使用令牌来授权,在任一时刻只能有一个执行线程访问 代码的临界区域.临界区域是指执 ...

  8. Oracle 插入数据

    6个柜面交易 打印修改--050101 delete from tran_prints where tran_id = (select id from tran where code='050101' ...

  9. C#中的lock关键字(初识)

    http://kb.cnblogs.com/page/88513/ 首先给出MSDN的定义: lock 关键字可以用来确保代码块完成运行,而不会被其他线程中断.这是通过在代码块运行期间为给定对象获取互 ...

  10. Unity3d Shader开发(三)Pass(Culling & Depth Testing)

    剔除是一种通过避免渲染背对观察者的几何体面来提高性能的优化措施.所有几何体都包含正面和反面.剔除基于大多数对象都是封闭的事实:如果你有一个立方体,你不会看到背离你的那一面(总是只有一面在你的前方),因 ...