`
gui1401
  • 浏览: 49095 次
  • 性别: Icon_minigender_1
  • 来自: 济南
社区版块
存档分类
最新评论

JavaScript,Class,继承

    博客分类:
  • js
阅读更多

 

 

解开JavaScript生命的达芬奇密码

cleverpig 发表于 2006-12-26 16:31:42
作者:cleverpig     来源:Matrix
评论数:6 点击数:4,776     投票总得分:18 投票总人次:4
关键字:JavaScript,Class,继承
<!---->

摘要:

现在,Ajax技术发展势头迅猛,开发者已经建立了一个调用大量客户端JavaScript、不断增长的、复杂的系统。因此,在JavaScript上尝试OO技术便成为了管理复杂性的一种手段。只着眼于Class的定义方式,我认为它是JavaScript开发者尝试解决问题的首选。因此,你可以在互联网上找到许多不同的问题解决案例,但在我看过它们后不免有些失望——这些案例都是在某个场合下适用,而不是放之四海而皆准的通法...
<!---->
解开JavaScript生命的达芬奇密码
——如何使用JavaScript进行可靠的继承调用


作者:cleverpig




提出问题:


 
        几乎每位在开发JavaScript时尝试应用面向对象技术的开发者,或多或少都会问自己一个问题:“如何调用父类(super class)的方法?”在Ajax技术还没有目前这样炙手可热之前,这种问题很少出现,因为大多数开发者仅在进行客户端form验证或者简单的DHTML/DOM操作时使用JavaScript。在那些简单的解决方案中,函数式编程(functional programming)是很有意义的,面向对象编程则处在次之重要的位置。现在,Ajax技术发展势头迅猛,开发者已经建立了一个调用大量客户端JavaScript、不断增长的、复杂的系统。因此,在JavaScript上尝试OO技术便成为了管理复杂性的一种手段。在此过程中,多数开发者很快便认识到:JavaScript是一种原型化的(prototypical)语言,它缺少OO自身带来的多种便利。

        OO设计的主旨和关于它的一些话题谈起来很大,但只着眼于Class的定义方式,我认为它是JavaScript开发者尝试解决问题的首选。因此,你可以在互联网上找到许多不同的问题解决案例,但在我看过它们后不免有些失望——这些案例都是在某个场合下适用,而不是放之四海而皆准的通法。而我对这个话题的兴趣来自于我的team在开发ThinWire Ajax Framework的影响。由于这个框架生成出对客户端代码的需求,才使我们“被迫”去实现可靠的、支持父类方法调用的OO模式。通过父类调用,你可以进一步依靠类的继承特性来核心化通用代码,从而更易于减少重复代码,去掉客户端代码的坏味道

        下面罗列出了一些在我的研究过程中遇到的解决方式。最终,我没有从中找出一个可以接收的解决方案,于是我不得不实现一个自己的解决方案,你将在本文的结尾部分看到这个方案。

        然而父类调用在这里是最重要的OO机制,因此我需要一个相应的工作模式,也正是因为在我的观点中原型化方式是丑陋的,所以我更需要一种更加自然地使用JavaScript定义类的方法。

More Solutions:

        好吧,让我们进入讨论。正如开发者所察觉的那样,在JS中实现基本的继承是很容易的事,事实上有一些众所周知的方法:

丑陋的Solution:

        没有进行父类调用的简单继承:
// 提前写好的JavaScript Class定义和继承
// 当然,这种代码很丑陋,散发着代码的坏味道。
function BaseClass() {
    //BaseClass constructor code goes here 
}

BaseClass.prototype.getName = function() {
    return "BaseClass";
}

function SubClass() {
    //SubClass constructor code goes here 
}

//Inherit the methods of BaseClass
SubClass.prototype = new BaseClass();

//Override the parent's getName method
SubClass.prototype.getName = function() {
    return "SubClass";
}

//Alerts "SubClass"
alert(new SubClass().getName());


导致IE内存泄露的Solution:

        这种实现方式能够导致在IE中的内存泄漏,你应该尽量避免:
// 运行时的JavaScript Class 定义和继承
// 看上去很传统,但这些脚本会导致在Internet Explorer中的内存泄漏.
function BaseClass() {
    this.getName = function() {
        return "BaseClass";
    };    

    //BaseClass constructor code goes here 
}

function SubClass() {
    //在对象实例建立时重载父类的getName方法 
    this.getName = function() {
        return "SubClass";
    }

    //SubClass constructor code goes here 
}

//Inherit the methods of BaseClass
SubClass.prototype = new BaseClass();

//Alerts "SubClass"
alert(new SubClass().getName());
        

        就像我在第一个实现方法中所注释的那样,第一个实现方法有些丑陋,但它相比引起内存泄漏的第二种方式便是首选了。

        我把这两种方法放在这里的目的是指出你不应该使用它们。

硬性编码的Solution:

        让我们看一下第一个例子,它采用了标准的原型化方式,但问题是:它的子类方法如何调用父类(基类)方法?下面是一些开发者尝试并采用的方式:

        一种企图进行父类调用的“通病”:
function BaseClass() { }
BaseClass.prototype.getName = function() {
    return "BaseClass(" + this.getId() + ")";
}

BaseClass.prototype.getId = function() {
    return 1;
}

function SubClass() {}
SubClass.prototype = new BaseClass();
SubClass.prototype.getName = function() {
    //调用父类的getName()方法
    //哈哈,这是对父类调用的直接引用吗?
    return "SubClass(" + this.getId() + ") extends " +
        BaseClass.prototype.getName();
}

SubClass.prototype.getId = function() {
    return 2;
}

//输出结果:"SubClass(2) extends BaseClass(1)";
//这是正确的输出吗?
alert(new SubClass().getName());


        上面的代码是对第一段脚步进行修改后的版本,我去掉了一些注释和空格,使你能注意到新的getId()方法和对父类的调用。你一定急于知道通过这样对BaseClass的硬性编码引用(hard coded reference),它是否能进行正确地调用BaseClass的方法?

        一个正确的、多态的父类调用必做的事情是保证“this”引用指向当前对象实例和类方法。在这里,看上去和它应该输出的结果非常接近,看上去好像在SubClass中调用了BaseClass的getName()方法。你发现问题了吗?这个问题是非常细小的,但却很重要决不能忽视。通过使用上面的父类调用语法,BaseClass的getName()方法被调用,它返回一个字符串:包括类名和“this.getId()”的返回值。问题在于“this.getId()”应该返回2,而不是1。如果这和你所想的不同,你可以查看Java或者C#这类OO语言的多态性。

改进后的硬性编码Solution:

        你可以通过一个微小的改动来解决这个问题。

静态(硬编码)父类调用:
function BaseClass() { }
BaseClass.prototype.getName = function() {
    return "BaseClass(" + this.getId() + ")";
}

BaseClass.prototype.getId = function() {
    return 1;
}

function SubClass() {}
SubClass.prototype = new BaseClass();
SubClass.prototype.getName = function() {
    //一点魔法加上多态性!
    //但很明显,这还是一个直接引用!    
    return "SubClass(" + this.getId() + ") extends " +
        BaseClass.prototype.getName.call(this);
}

SubClass.prototype.getId = function() {
    return 2;
}

//输出结果:"SubClass(2) extends BaseClass(2)";
//Hey, 我们得到了正确的输出!
alert(new SubClass().getName());


        在ECMA-262 JavaScript/EcmaScript标准中,Call()方法是所有Function实例的一个成员方法,这已经被所有的主流浏览器所支持。JavaScript把所有的function看作对象,因此每个function都具有方法和附着其上的属性。Call()方法允许你调用某个function,并在function的调用过程中确定“this”变量应该是什么。JavaScript的function没有被紧紧地绑定到它所在的对象上,所以如果你没有显式地使用call()方法,“this”变量将成为function所在的对象。

        另外一种方法是使用apply方法,它和call()方法类似,只在参数上存在不同:apply()方法接受参数的数组,而call()方法接受单个参数。

Douglas Crockford的Solution:

        现在回溯到上面的示例,在这个示例中唯一的问题就是父类引用是直接的、硬性编写的。它可以适用于小型的类继承环境,但对于具有较深层次的大型继承来讲,这些直接引用非常难于维护。

        那么,有解决方法吗?不幸的是这里没有简单的解决方案。

        JavaScript没有提供对通过“隐性引用”方式调用父类方法的支持,这里也没有在其它OO语言中使用的“super”变量的等价物。于是,一些开发者做出了自己的解决方案,但就像我前面提到的那样,每个解决方案都存在某种缺点。

        例如,下面列出的众多著名方法之一:JavaScript大师[ur=http://en.wikipedia.org/wiki/Douglas_Crockford]Douglas Crockford[/url]在他的《Classical Inheritance in JavaScript》中提出的方法。

        Douglas Crockford的方法在多数情况下可以正常工作:

一次性支持代码:
//Crockford的方法:给所有的function都增加'inherits' 方法、
//每个类都增加了'uber'方法来调用父类方法
Function.prototype.inherits = function(parent) {
    var d = 0, p = (this.prototype = new parent());
    
    this.prototype.uber = function(name) {
        var f, r, t = d, v = parent.prototype;
        if (t) {
            while (t) {
                v = v.constructor.prototype;
                t -= 1;
            }
            f = v[name];
        } else {
            f = p[name];
            if (f == this[name]) {
                f = v[name];
            }
        }
        d += 1;
        r = f.apply(this, Array.prototype.slice.apply(arguments, [1]));
        d -= 1;
        return r;
    };
};


运行示例:
function BaseClass() { }
BaseClass.prototype.getName = function() {
    return "BaseClass(" + this.getId() + ")";
}

BaseClass.prototype.getId = function() {
    return 1;
}

function SubClass() {}
SubClass.inherits(BaseClass);
SubClass.prototype.getName = function() {
    //这里看上去非常的清晰,它调用了BaseClass的getName()方法
return "SubClass(" + this.getId() + ") extends " +
        this.uber("getName");
}

SubClass.prototype.getId = function() {
    return 2;
}

function TopClass() {}
TopClass.inherits(SubClass);
TopClass.prototype.getName = function() {
    //这里看上去非常的清晰,它调用了SubClass的getName()方法
    return "TopClass(" + this.getId() + ") extends " +
        this.uber("getName");
}

TopClass.prototype.getId = function() {
    //Ok, 因此this.getId()应该总是
//返回调用SubClass的getId()方法的返回值(2)。
    return this.uber("getId");
}

//输出结果:"TopClass(2) extends SubClass(1) extends BaseClass(1)"
//嗯?后面的两次this.getId()调用都没有返回2.
//发生了什么? 
alert(new TopClass().getName());
        
        上面代码的第一部分包括了Crockford的“inherit”和“uber”方法代码。第二部分看上去和前面的示例很类似,除了我添加了用来演示Crockford方式所存在问题的第三层继承关系。诚然,Crockford这位JavaScript大师的方法是我所找到的最可靠的方法之一,我很敬佩他在JavaScript编程方面做出的贡献。但是,如果你使用三个依次继承的类来考核他的代码,你将从输出中发现这里存在着细微的问题。

        从输出结果看,第一次调用的this.getId()返回了TopClass当前的id值“2”,但在调用SubClass和BaseClass的getName()方法时返回了“1”而不是“2”。从代码上看, 在getName()方法中的父类调用行为是正确的,三个类的名字都被正确地显示出来。唯一的问题出现在this.uber("getId")这个父类调用被放入调用堆栈(call stack)时。因为此时当前对象是一个TopClass实例,而每次调用在调用堆栈中的this.getId()都应该返回调用TopClass的getId()方法后的返回值。

        而问题是TopClass的this.getId()方法通过this.uber("getId")执行了父类调用,这三次this.getId()调用中的后两次错误地调用了BaseClass的getId()方法,这样便在输出结果中显示了两次“1”。正确的行为应该是调用三次SubClass的getId()方法,在输出结果中显示三次“2”。大家可以通过FireFox的FireBug插件进行代码debug进行观察。

        这是十分难以描述的现象,我不能保证我能把它解释清楚。但是至少从上面的运行结果中可以看出它是错误的。

        另外,Crockford的方法和其它一些方法的劣势在于每个父类调用都需要一个额外的方法调用和额外的某种处理。这是否成为你所面临的问题,取决于你所使用的父类调用深度。在ThinWire项目的客户端代码中使用了大量的父类调用,因此父类调用的可靠性和快速性在项目中是很重要的。

我的初级Solution:

        面对这样的窘境——Crockford的方法出现问题、在互联网上没有找到符合要求的方法,我决定看看我自己是否可以发明一种可以满足要求的方法。这花掉了我近一周的时间来使代码工作并满足各种情况,但我对它的工作情况很有信心,并且很快把它与framework集成在一起,TinWire的beta和beta2两个版本中都使用了这些“初级设计”的代码。

        动态父类调用:

一次性支持代码:
//定义最顶级类
function Class() { }
Class.prototype.construct = function() {};
Class.__asMethod__ = function(func, superClass) {    
    return function() {
        var currentSuperClass = this.$;
        this.$ = superClass;
        var ret = func.apply(this, arguments);        
        this.$ = currentSuperClass;
        return ret;
    };
};

Class.extend = function(def) {
    var classDef = function() {
        if (arguments[0] !== Class) { this.construct.apply(this, arguments); }
    };
    
    var proto = new this(Class);
    var superClass = this.prototype;
    
    for (var n in def) {
        var item = def[n];                        
        
        if (item instanceof Function) {
            item = Class.__asMethod__(item, superClass);
        }
        
        proto[n] = item;
    }

    proto.$ = superClass;
    classDef.prototype = proto;
    
    //赋给这个新的子类同样的静态extend方法
    classDef.extend = this.extend;        
    return classDef;
};


运行示例:
//Hey, 注意一下这个类的定义方式
//看上去比其它方式要清楚些
var BaseClass = Class.extend({
    construct: function() { /* optional constructor method */ },
    
    getName: function() {
        return "BaseClass(" + this.getId() + ")";
    },
    
    getId: function() {
        return 1;
    }
});

var SubClass = BaseClass.extend({
    getName: function() {
        //调用BaseClass的getName()方法
        return "SubClass(" + this.getId() + ") extends " +
            this.$.getName.call(this);
    },
    
    getId: function() {
        return 2;
    }
});

var TopClass = SubClass.extend({
    getName: function() {
        //调用SubClass的getName()方法
        return "TopClass(" + this.getId() + ") extends " +
            this.$.getName.call(this);
    },
    
    getId: function() {
        //this.getId()总是返回调用父类的getId()方法的返回值(2)
        return this.$.getId.call(this);
    }
});

//输出结果:"TopClass(2) extends SubClass(2) extends BaseClass(2)"
//一切都正确!
alert(new TopClass().getName());

        这里是前面示例的,但是目前这种方式包括了通过“extend”方法实现的十分清晰的类定义模式和正确的父类调用语义。尤其是“extend”方法通过一个中间function封装了类定义中的每个方法,这个中间function在每次方法调用时首先把当前父类引用“$” 与正确的父类引用相互交换,然后把这个正确的父类引用传递给apply()进行方法调用,最后再将把当前父类引用“$” 与正确的父类引用交换回来。这种方式唯一的问题就是它需要一些中间function,它们会对性能产生不良影响。所以近来我重新审视了设计、完成了去掉了中间function了一种改良的方式。

改良后的Solution:

        动态父类调用快速版本:

一次性支持代码
//定义最顶级类
function Class() { }
Class.prototype.construct = function() {};
Class.extend = function(def) {
    var classDef = function() {
        if (arguments[0] !== Class) { this.construct.apply(this, arguments); }
    };
    
    var proto = new this(Class);
    var superClass = this.prototype;
    
    for (var n in def) {
        var item = def[n];                        
        if (item instanceof Function) item.$ = superClass;
        proto[n] = item;
    }

    classDef.prototype = proto;
    
    //赋给这个新的子类同样的静态extend方法 
    classDef.extend = this.extend;        
    return classDef;
};


运行示例:
//Hey, 注意一下这个类的定义方式
//看上去比其它方式要清楚些
var BaseClass = Class.extend({
    construct: function() { /* optional constructor method */ },
        
    getName: function() {
        return "BaseClass(" + this.getId() + ")";
    },
    
    getId: function() {
        return 1;
    }
});

var SubClass = BaseClass.extend({
    getName: function() {
        //调用BaseClass的getName()方法
        return "SubClass(" + this.getId() + ") extends " +
            arguments.callee.$.getName.call(this);
    },
    
    getId: function() {
        return 2;
    }
});

var TopClass = SubClass.extend({
    getName: function() {
        //调用SubClass的getName()方法
        return "TopClass(" + this.getId() + ") extends " +
            arguments.callee.$.getName.call(this);
    },
    
    getId: function() {
        // this.getId()总是返回调用父类的getId()方法的返回值(2) 
        return arguments.callee.$.getId.call(this);
    }
});

//输出结果:"TopClass(2) extends SubClass(2) extends BaseClass(2)"
//工作正常!而且没有任何中间function
alert(new TopClass().getName());

        这是最后的设计,它使用了JavaScript中一点鲜为人知的特性:callee。

        在任何方法执行过程中,你可以查看那些通过“arguments”数组传入的参数,这是众所周知的,但很少有人知道“arguments”数组包含一个名为“callee”的属性,它作为一个引用指向了当前正在被执行的function,而后通过“$”便可以方便的获得当前被执行function所在类的父类。这是非常重要的,因为它是获得此引用的唯一途径(通过“this”对象获得的function引用总是指向被子类重载的function,而后者并非全是正在被执行的function)。

原文作者附言:
 

        Ok,这便是相对彻底的问题解决方案了。但是我想通过把它的细节写成文档让每个人阅读,以致我可以从中找出漏洞不断地完善代码。欢迎对我的文章进行评论和建议!
                                                                                        --Joshua Gertzen


参考资源:

        ThinWire RIA Ajax GUI Framework

        原文作者Joshua Gertzen的《Object Oriented Super Class Method Calling with JavaScript》

        Douglas Crockford 的《Classical Inheritance in JavaScript》

        Kevin Lindsey的《JavaScript Tutorial》

        Tom Wright的《Super Simulation in JavaScript OOP》

        Mozilla's Core JavaScript 1.5 手册

        Jesse James Garrett的《Ajax: A New Approach to Web Applications》

        Microsoft's HTML & DHTML 手册

        W3C Document Object Model (DOM) 规范

        ECMA-262 EcmaScript (JavaScript / JScript) 规范

分享到:
评论

相关推荐

    JavaScript之class继承_动力节点Java学院整理

    主要介绍了JavaScript之class继承,新的关键字class从ES6开始正式被引入到JavaScript中。class的目的就是让定义类更简单,有兴趣的可以了解一下

    JavaScript Class/Object

    本文件(含有注释)精简的介绍了javascript类的属性、方法(包挂静态属性和方法)的应用,还有构造函数的实现,以及类的继承。代码很精简,相信大家都能看懂。下载了,记得给个评价哦~~有待我以后改进

    baseclass:精益JavaScript继承

    基类 精简而强大JavaScript继承。 $ bower install baseclassjs$ npm install baseclassjs BaseClass是一个快速,轻量级且非侵入式的继承库。 使用自然JavaScript编写代码,但现在具有简单继承的功能。 BaseClass...

    ES6 javascript中Class类继承用法实例详解

    主要介绍了ES6 javascript中Class类继承用法,结合实例形式较为详细的分析了ES6继承的基本用法、相关属性、方法与使用技巧,需要的朋友可以参考下

    Javascript编程 类的继承及封装:Class

    笔者历经多年javascript的开发,痛彻体会javascript面向对象编程的不便性,精心制作了一个类的定义与继承功能的js,实现了在javascript中对类的定义、继承、封装机制,主要功能特征包括:  一、 统一了类定义的语法...

    【JavaScript源代码】JavaScript继承的三种方法实例.docx

     继承 继承: 首先继承是一种关系,类(class)与类之间的关系,JS中没有类,但是可以通过构造函数模拟类,然后通过原型来实现继承。  继承也是为了数据共享,js中的继承也是为了实现数据共享  我们可以联想到原型...

    class:javascript类继承

    javascript类继承 var Person = Class({ constructor: function(fname, lname){ this.fname = fname; this.lname = lname; }, prototype : { introduce: function(){ return ['Hi my name is', this.fname,...

    在JavaScript中模拟类(class)及类的继承关系_.docx

    在JavaScript中模拟类(class)及类的继承关系_.docx

    JavaScript对象继承模型Fiber.js.zip

    Fiber.js 是来自 Linkedin 的一个轻量级的 JavaScript 对象继承模型。示例代码:// Animal base class var Animal = Fiber.extend(function() {  return {  // The `init` method serves as the ...

    es5生成class支持构造传参继承函数复用多继承

    es5 生成class,支持构造传参、继承、函数复用、多继承;

    JavaScript mixin实现多继承的方法详解

    本文实例讲述了JavaScript mixin实现多继承的方法。分享给大家供大家参考,具体如下: mixin简单通俗的讲就是把一个对象的方法和属性拷贝到另一个对象上,注意这个继承还是有区别的。js是一种只支持单继承的语言,...

    classy.js:一个简单的 Javascript 继承实现

    Classy 是一个 Javascript 继承实现。 这几乎就是你需要知道的一切。 您将获得一种在代码中实现 Javascript 类的简单方法以及一个有用的“包含”方法。 ##例子 ###Building.js Class ( "Building" , { //...

    【JavaScript源代码】深入JS继承.docx

    深入JS继承  目录 前言准备总结继承的n种方式原型式继承原型链式继承借用构造函数(类式继承...撇开ES6 class不谈,传统的继承方式你知道几种?每种实现原理是什么,优劣点能谈谈吗。这里就结合具体例子,按照渐进式的

    define-class:一个简单的 JavaScript 继承模块

    一个简单的 JavaScript 继承模块 ##用法 ####定义类 var DefineClass = require ( 'define-class' ) ; // Person Class var Person = DefineClass ( { // put all instance method/props here // constructor ...

    JavaScript中继承用法实例分析

    本文实例分析了JavaScript中继承的用法。分享给大家供大家参考。具体如下: // define the Person Class function Person() {} Person.prototype.walk = function(){ alert &#40;'I am walking!'&#41;; }; Person....

    Sky.Class.js:一个简单的 JavaScript 单继承框架

    这是一个简单的单一(原型)继承javascript框架,您可以使用该框架在运行时动态生成类。 用法 使用起来真的很简单,只需添加 Sky.Class.min.js 文件引用,然后像这样使用: var cls = new Class ( /*parentClass,...

    浅析JavaScript原型继承的陷阱

    因此,JavaScript通过扩展自身能模拟类式(class-based)继承。 JavaScript和其它面向对象语言一样,对象类型采用引用方式。持有对象的变量只是一个地址,而基本类型数据是值。当原型上存储对象时,就可能有一些陷阱。...

    JavaScript中类的继承(ES6、class、extends、super)

    Class 可以通过extends关键字实现继承 这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。如下: class Test{} class ColorTest extends Test{}  上面代码定义了一个ColorTest类,该类通过extends关键字,...

Global site tag (gtag.js) - Google Analytics