在第一章中,我们使用构造函数和原型的方式在JavaScript的世界中实现了类和继承, 但是存在很多问题。这一章我们将会逐一分析这些问题,并给出解决方案。

注:本章中的jClass的实现参考了Simple JavaScript Inheritance的做法。

首先让我们来回顾一下第一章中介绍的例子:

function Person(name) {
this.name = name;
}
Person.prototype = {
getName: function() {
return this.name;
}
} function Employee(name, employeeID) {
this.name = name;
this.employeeID = employeeID;
}
Employee.prototype = new Person();
Employee.prototype.getEmployeeID = function() {
return this.employeeID;
}; var zhang = new Employee("ZhangSan", "1234");
console.log(zhang.getName()); // "ZhangSan"

修正constructor的指向错误

从上一篇文章中关于constructor的描述,我们知道Employee实例的constructor会有一个指向错误,如下所示:

var zhang = new Employee("ZhangSan", "1234");
console.log(zhang.constructor === Employee); // false
console.log(zhang.constructor === Object); // true

我们需要简单的修正:

function Employee(name, employeeID) {
this.name = name;
this.employeeID = employeeID;
}
Employee.prototype = new Person();
Employee.prototype.constructor = Employee;
Employee.prototype.getEmployeeID = function() {
return this.employeeID;
};
var zhang = new Employee("ZhangSan", "1234");
console.log(zhang.constructor === Employee); // true
console.log(zhang.constructor === Object); // false

创建Employee类时实例化Person是不合适的

但另一方面,我们又必须依赖于这种机制来实现继承。 解决办法是不在构造函数中初始化数据,而是提供一个原型方法(比如init)来初始化数据。

// 空的构造函数
function Person() {
} Person.prototype = {
init: function(name) {
this.name = name;
}, getName: function() {
return this.name;
}
}
// 空的构造函数
function Employee() {
}
// 创建类的阶段不会初始化父类的数据,因为Person是一个空的构造函数
Employee.prototype = new Person();
Employee.prototype.constructor = Employee;
Employee.prototype.init = function(name, employeeID) {
this.name = name;
this.employeeID = employeeID;
};
Employee.prototype.getEmployeeID = function() {
return this.employeeID;
};

这种方式下,必须在实例化一个对象后手工调用init函数,如下:

var zhang = new Employee();
zhang.init("ZhangSan", "1234");
console.log(zhang.getName()); // "ZhangSan"

如何自动调用init函数?

必须达到两个效果,构造类时不要调用init函数和实例化对象时自动调用init函数。看来我们需要在调用空的构造函数时有一个状态标示。

// 创建一个全局的状态标示 - 当前是否处于类的构造阶段
var initializing = false; function Person() {
if (!initializing) {
this.init.apply(this, arguments);
}
} Person.prototype = {
init: function(name) {
this.name = name;
},
getName: function() {
return this.name;
}
} function Employee() {
if (!initializing) {
this.init.apply(this, arguments);
}
} // 标示当前进入类的创建阶段,不会调用init函数 initializing = true;
Employee.prototype = new Person();
Employee.prototype.constructor = Employee;
initializing = false;
Employee.prototype.init = function(name, employeeID) {
this.name = name;
this.employeeID = employeeID;
};
Employee.prototype.getEmployeeID = function() {
return this.employeeID;
};
// 初始化类实例时,自动调用类的原型函数init,并向init中传递参数
var zhang = new Employee("ZhangSan", "1234");
console.log(zhang.getName()); // "ZhangSan"

但是这样就必须引入全局变量,这是一个不好的信号。

如何避免引入全局变量initializing?

我们需要引入一个全局的函数来简化类的创建过程,同时封装内部细节避免引入全局变量。

// 当前是否处于创建类的阶段
var initializing = false;
function jClass(baseClass, prop) {
// 只接受一个参数的情况 - jClass(prop)
if (typeof (baseClass) === "object") {
prop = baseClass;
baseClass = null;
}
// 本次调用所创建的类(构造函数)
function F() {
// 如果当前处于实例化类的阶段,则调用init原型函数
if (!initializing) {
this.init.apply(this, arguments);
}
}
// 如果此类需要从其它类扩展
if (baseClass) {
initializing = true;
F.prototype = new baseClass();
F.prototype.constructor = F;
initializing = false;
}
// 覆盖父类的同名函数
for (var name in prop) {
if (prop.hasOwnProperty(name)) {
F.prototype[name] = prop[name];
}
}
return F;
};

使用jClass函数来创建类和继承类的方法:

var Person = jClass({
init: function(name) {
this.name = name;
},
getName: function() {
return this.name;
}
}); var Employee = jClass(Person, {
init: function(name, employeeID) {
this.name = name;
this.employeeID = employeeID;
},
getEmployeeID: function() {
return this.employeeID;
}
}); var zhang = new Employee("ZhangSan", "1234");
console.log(zhang.getName()); // "ZhangSan"

OK,现在创建类和实例化类的方式看起来优雅多了。 但是这里面还存在明显的瑕疵,Employee的初始化函数init无法调用父类的同名方法。

如何调用父类的同名方法?

我们可以通过为实例化对象提供一个base的属性,来指向父类(构造函数)的原型,如下:

// 当前是否处于创建类的阶段
var initializing = false;
function jClass(baseClass, prop) {
// 只接受一个参数的情况 - jClass(prop)
if (typeof (baseClass) === "object") {
prop = baseClass;
baseClass = null;
}
// 本次调用所创建的类(构造函数)
function F() {
// 如果当前处于实例化类的阶段,则调用init原型函数
if (!initializing) {
// 如果父类存在,则实例对象的base指向父类的原型
// 这就提供了在实例对象中调用父类方法的途径
if (baseClass) {
this.base = baseClass.prototype;
}
this.init.apply(this, arguments);
}
} // 如果此类需要从其它类扩展
if (baseClass) {
initializing = true;
F.prototype = new baseClass();
F.prototype.constructor = F;
initializing = false;
} // 覆盖父类的同名函数
for (var name in prop) {
if (prop.hasOwnProperty(name)) {
F.prototype[name] = prop[name];
}
}
return F;
};

调用方式:

var Person = jClass({
init: function(name) {
this.name = name;
},
getName: function() {
return this.name;
}
});
var Employee = jClass(Person, {
init: function(name, employeeID) {
// 调用父类的原型函数init,注意使用apply函数修改init的this指向
this.base.init.apply(this, [name]);
this.employeeID = employeeID;
},
getEmployeeID: function() {
return this.employeeID;
},
getName: function() {
// 调用父类的原型函数getName
return "Employee name: " + this.base.getName.apply(this);
}
});
var zhang = new Employee("ZhangSan", "1234");
console.log(zhang.getName()); // "Employee name: ZhangSan"

目前为止,我们已经修正了在第一章手工实现继承的种种弊端。 通过我们自定义的jClass函数来创建类和子类,通过原型方法init初始化数据, 通过实例属性base来调用父类的原型函数。

唯一的缺憾是调用父类的代码太长,并且不好理解, 如果能够按照如下的方式调用岂不是更妙:

var Employee = jClass(Person, {
init: function(name, employeeID) {
// 如果能够这样调用,就再好不过了
this.base(name);
this.employeeID = employeeID;
}
});

优化jClass函数

// 当前是否处于创建类的阶段
var initializing = false;
function jClass(baseClass, prop) {
// 只接受一个参数的情况 - jClass(prop)
if (typeof (baseClass) === "object") {
prop = baseClass;
baseClass = null;
}
// 本次调用所创建的类(构造函数)
function F() {
// 如果当前处于实例化类的阶段,则调用init原型函数
if (!initializing) {
// 如果父类存在,则实例对象的baseprototype指向父类的原型
// 这就提供了在实例对象中调用父类方法的途径
if (baseClass) {
this.baseprototype = baseClass.prototype;
}
this.init.apply(this, arguments);
}
} // 如果此类需要从其它类扩展
if (baseClass) {
initializing = true;
F.prototype = new baseClass();
F.prototype.constructor = F;
initializing = false;
}
// 覆盖父类的同名函数
for (var name in prop) {
if (prop.hasOwnProperty(name)) {
// 如果此类继承自父类baseClass并且父类原型中存在同名函数name
if (baseClass &&
typeof (prop[name]) === "function" &&
typeof (F.prototype[name]) === "function") {
// 重定义函数name -
// 首先在函数上下文设置this.base指向父类原型中的同名函数
// 然后调用函数prop[name],返回函数结果
// 注意:这里的自执行函数创建了一个上下文,这个上下文返回另一个函数,
// 此函数中可以应用此上下文中的变量,这就是闭包(Closure)。
// 这是JavaScript框架开发中常用的技巧。
F.prototype[name] = (function(name, fn) {
return function() {
this.base = baseClass.prototype[name];
return fn.apply(this, arguments);
};
})(name, prop[name]);
} else {
F.prototype[name] = prop[name];
}
}
}
return F;
};

此时,创建类与子类以及调用方式都显得非常优雅,请看:

var Person = jClass({
init: function(name) {
this.name = name;
},
getName: function() {
return this.name;
}
});
var Employee = jClass(Person, {
init: function(name, employeeID) {
this.base(name);
this.employeeID = employeeID;
},
getEmployeeID: function() {
return this.employeeID;
},
getName: function() {
return "Employee name: " + this.base();
}
});
var zhang = new Employee("ZhangSan", "1234");
console.log(zhang.getName()); // "Employee name: ZhangSan"

至此,我们已经创建了一个完善的函数jClass, 帮助我们在JavaScript中以比较优雅的方式实现类和继承。

在以后的章节中,我们会陆续分析网上一些比较流行的JavaScript类和继承的实现。 不过万变不离其宗,那些实现也无非把我们这章中提到的概念颠来簸去的“炒作”, 为的就是一种更优雅的调用方式。

JavaScript继承详解(三)的更多相关文章

  1. [原创]JavaScript继承详解

    原文链接:http://www.cnblogs.com/sanshi/archive/2009/07/08/1519036.html 面向对象与基于对象 几乎每个开发人员都有面向对象语言(比如C++. ...

  2. JavaScript继承详解

    面向对象与基于对象 在传统面向对象的语言中,有两个非常重要的概念 - 类和实例. 类定义了一类事物公共的行为和方法:而实例则是类的一个具体实现. 我们还知道,面向对象编程有三个重要的概念 - 封装.继 ...

  3. JavaScript继承详解(五)

    在本章中,我们将分析John Resig关于JavaScript继承的一个实现 - Simple JavaScript Inheritance. John Resig作为jQuery的创始人而声名在外 ...

  4. JavaScript继承详解(四)

    在本章中,我们将分析Douglas Crockford关于JavaScript继承的一个实现 - Classical Inheritance in JavaScript. Crockford是Java ...

  5. JavaScript继承详解(一)

    面向对象与基于对象 几乎每个开发人员都有面向对象语言(比如C++.C#.Java)的开发经验. 在传统面向对象的语言中,有两个非常重要的概念 - 类和实例. 类定义了一类事物公共的行为和方法:而实例则 ...

  6. 【转载】JavaScript继承详解一

    面向对象与基于对象 几乎每个开发人员都有面向对象语言(比如C++.C#.Java)的开发经验. 在传统面向对象的语言中,有两个非常重要的概念 - 类和实例. 类定义了一类事物公共的行为和方法:而实例则 ...

  7. 【转载】JavaScript继承详解(二)

    这一章我们将会重点介绍JavaScript中几个重要的属性(this.constructor.prototype), 这些属性对于我们理解如何实现JavaScript中的类和继承起着至关重要的作用. ...

  8. JavaScript继承详解(二)

    这一章我们将会重点介绍JavaScript中几个重要的属性(this.constructor.prototype), 这些属性对于我们理解如何实现JavaScript中的类和继承起着至关重要的作用. ...

  9. javascript继承详解(待续)

    常见继承分两种,一种接口继承,继承方法签名:一种实现继承,继承实际方法.js只支持后一种. 1原型链 首先看原型.构造函数.实例的关系.如果我们让一个函数的原型对象等于另一个的实例,然后另一个的原型对 ...

随机推荐

  1. Android 测试之Monkey

    一.什么是Monkey Monkey是Android中的一个命令行工具,可以运行在模拟器里或实际设备中.它向系统发送伪随机的用户事件流(如按键输入.触摸屏输入.手势输入等),实现对正在开发的应用程序进 ...

  2. java后台面试知识点总结

    本文主要记录在准备面试过程中遇到的一些基本知识点(持续更新) 一.Java基础知识 1.抽象类和接口的区别 接口和抽象类中都可以定义变量,但是接口中定义的必须是公共的.静态的.Final的,抽象类中的 ...

  3. Linux内核设计与实现(chapter1/2)

    Linux内核简介 Unix从一个失败的多用户操作系统Multics中衍生来的. Unix强大的原因: 简洁 几乎所有的东西都被当做文件来对待,可以通过相同的系统调用接口来进行调用. 因为它是由c语言 ...

  4. 图文转化(Alpha)版使用说明

    图文转化使用说明 本软件是一款扫描图片上的文字转化成txt或doc格式存储的软件. 现在还只是初期简单的一个实现,软件暂时的界面显示如下: 简介:照片选取的是手机里的本地照片,拍照打开照相机进行拍照. ...

  5. 使用git进行代码的推送

    首先是对于锐捷墙的问题,登陆github有时可以有时又连不上,网络又非常慢,所以用了十足的耐心才fork完了代码库.链接https://github.com/niconiconiconi/hellow ...

  6. input 清空值。(转载)

    ref顾名思义我们知道,其实它就可以被看座是一个组件的参考,也可以说是一个标识.作为组件的属性,其属性值可以是一个字符串也可以是一个函数. 其实,ref的使用不是必须的.即使是在其适用的场景中也不是非 ...

  7. Redis 总结精讲

    本文围绕以下几点进行阐述 1.为什么使用redis2.使用redis有什么缺点3.单线程的redis为什么这么快4.redis的数据类型,以及每种数据类型的使用场景5.redis的过期策略以及内存淘汰 ...

  8. 【转帖】Git学习笔记 记录一下

    本文内容参考了廖雪峰老师的博文,并做了适当整理,方便大家查阅. 原帖地址 https://wangfanggang.com/Git/git/ 常用命令 仓库初始化 - git init 1 git i ...

  9. PHPSQL注入

    什么是SQL注入? 就是通过把SQL命令插入到Web表单递交或输入域名或页面请求的查询字符串,最终达到欺骗服务器执行恶意的SQL命令. 例如一个简单的登录表单(这里把密码写成明文方便说明):  当在表 ...

  10. 深入理解ajax系列第七篇——传递JSON

    前面的话 虽然ajax全称是asynchronous javascript and XML.但目前使用ajax技术时,传递JSON已经成为事实上的标准.因为相较于XML而言,JSON简单且方便.本文将 ...