JavaScript继承详解(三)

在第一章中,我们使用构造函数和原型的方式在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
27
1
(请您对文章做出评价)
« 上一篇: 奇怪-正则匹配的test函数
» 下一篇: JavaScript继承详解(四)
posted @  2009-07-09 14:53  Sanshi(Asp.Net控件) 阅读( 15751) 评论( 27编辑  收藏

  
#1楼   2009-07-09 16:17 |  lass_name   
  
#2楼   2009-07-09 17:32 |  ziqiu.zhang   
和"悟透javascript"一书中介绍的很相似. 不知道楼主是否看过此书.博客园系列书籍. 或者也是从这书中学到的?
  
#3楼 [ 楼主2009-07-09 21:22 |  Sanshi(Asp.Net控件)   
引用 ziqiu.zhang:和"悟透javascript"一书中介绍的很相似. 不知道楼主是否看过此书.博客园系列书籍. 或者也是从这书中学到的?

没看过,不过这些知识都是相通的,我只是用自己的语言重新描述了一遍
  
#4楼   2009-07-10 15:38 |  可乐虫   
楼主,能把mootools的Class给提出来不.
  
#5楼   2009-07-10 15:38 |  可乐虫   
楼主,能把mootools的Class给提出来不.
  
#6楼   2009-08-24 11:11 |  love4J   
如何调用父类的同名方法?中的调用方式中的
var Employee = jClass(Person, {

init: function(name, employeeID) {

// 调用父类的原型函数init,注意使用apply函数修改init的this指向

this.base.init.apply(this, [name]);

this.employeeID = employeeID;

}
[name]是什么意思?为什么用方括号括起来?
  
#7楼 [ 楼主2009-08-24 11:15 |  Sanshi(Asp.Net控件)   
引用 love4J:
如何调用父类的同名方法?中的调用方式中的
var Employee = jClass(Person, {

init: function(name, employeeID) {

// 调用父类的原型函数init,注意使用apply函数修改init的this指向

this.base.init.apply(this, [name]);

this.employeeID = employeeID;

}
[name]是什么意思?为什么用方括号括起来?

name是ini函数传递过来的第一个参数,用中括号括起来表示这是一个数组,数组中只有一项。这是 apply 函数的语法。
  
#8楼   2009-08-24 14:51 |  love4J   
三生石上,问一个ext的问题
A=function(config){
this.id=config.id;
}
A.prototype.sayHello=function(){
alert("Hello, world!");
}
B=function(config){
this.name=config.name;
}
通过ext的方式让B继承A,B=Ext.extend(B,A),这样得到的B没有A的id属性,怎样让B有A的id?
  
#9楼 [ 楼主2009-08-24 16:47 |  Sanshi(Asp.Net控件)   
引用 love4J:
三生石上,问一个ext的问题
A=function(config){
this.id=config.id;
}
A.prototype.sayHello=function(){
alert("Hello, world!");
}
B=function(config){
this.name=config.name;
}
通过ext的方式让B继承A,B=Ext.extend(B,A),这样得到的B没有A的id属性,怎样让B有A的id?

可以这样写:
A = function(config) {
this.id=config.id;
};

A.prototype.sayHello = function(){
alert("Hello, world!");
};

B = Ext.extend(A, {
constructor: function(config){
B.superclass.constructor.apply(this, arguments);
this.name=config.name;
}
});

var b = new B({
id: "1002",
name: "ZhangSan"
});
alert(b.id);
b.sayHello();
  
#10楼 [ 楼主2009-08-24 16:48 |  Sanshi(Asp.Net控件)   
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
A =  function (config) {
this .id=config.id;
};
 
A.prototype.sayHello =  function (){
alert( "Hello, world!" );
};
 
B = Ext.extend(A, {
constructor:  function (config){
B.superclass.constructor.apply( this , arguments);
this .name=config.name;
}
});
 
var  b =  new  B({
id:  "1002" ,
name:  "ZhangSan"
});
alert(b.id);
b.sayHello();
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值