`
gui1401
  • 浏览: 50112 次
  • 性别: 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实现继承的几种方式

    本篇文章将深入探讨JavaScript中实现继承的几种常见方式。 1. 原型链继承 JavaScript的原型(prototype)机制是实现继承的基础。每个函数都有一个prototype属性,这个属性指向一个对象,这个对象的属性和方法可以被...

    JavaScript学习之三 — JavaScript实现继承的7种方式

    通过`class`和`extends`关键字,子类可以继承父类的所有属性和方法,而且父类的静态属性和方法也能被继承。这提供了更清晰的语法,但底层仍然是原型链机制。 6. 寄生继承(Parasitic Inheritance) 寄生继承的思想...

    javascript控件开发之继承关系

    在这个主题中,“javascript控件开发之继承关系”主要探讨的是如何利用JavaScript的面向对象特性来构建和组织控件的层次结构,以及如何通过继承来实现代码的复用和模块化。 在JavaScript中,继承是基于原型...

    JavaScript继承机制研究.pdf

    Class Extend是一种非常强大和灵活的继承机制,它广泛应用于JavaScript开发中。 结论 JavaScript继承机制的实现方式多样化,每种继承机制都有其优缺点。通过深入理解JavaScript继承机制,可以更好地理解JavaScript...

    JavaScript继承与多继承实例分析.docx

    JavaScript的继承机制主要基于原型链,本文将深入探讨JavaScript的继承与多继承,并通过实例进行分析。 1. **JavaScript继承** - **原理**:JavaScript的继承主要是通过原型链(prototype chain)来实现的。每个...

    javascript的prototype继承

    JavaScript的原型继承是其面向对象编程的一大特性,它基于原型链机制实现,允许一个对象可以从另一个对象继承属性和方法。这种继承方式不同于类继承,而是通过将子类的原型对象设置为父类的一个实例来实现。 在...

    详解Javascript继承的实现

    随着语言的发展,新的语法特性(如ES6的`class`)和库的出现,使得JavaScript的继承机制更加完善,同时也提供了更多的灵活性和便利性。理解这些原理和实践,对于编写高效、可维护的JavaScript代码至关重要。

    JavaScript中继承原理与用法实例入门

    此外,JavaScript还提供了其他实现继承的方式,如使用`__proto__`直接修改原型,或者使用ES6中的`class`语法糖,这些方法在不同场景下各有优势。总的来说,理解JavaScript中的构造函数和原型是掌握继承的关键,而...

    JavaScript继承详解.doc

    JavaScript中的继承是面向对象编程的重要概念,它允许一个对象(子对象)获取另一个对象(父对象)的属性和方法,从而实现代码复用和多态性。JavaScript支持多种继承实现方式,包括以下四种: 1. **构造函数继承**...

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

    在JavaScript中,class继承通过extends关键字实现。当我们想创建一个PrimaryStudent类继承自Student类时,可以这样写: ```javascript class PrimaryStudent extends Student { constructor(name, grade) { super...

    JavaScript面向对象继承详解

    6. **ES6的类继承**:ES6引入了`class`语法糖,使得JavaScript的继承看起来更像传统的面向对象语言。实际上,`class`和`extends`关键字仍然基于原型链,但提供了更简洁的语法。 在JavaScript中,还有其他一些与继承...

    JavaScript 继承详解(六)

    在本章中,我们将分析Prototypejs中关于JavaScript继承的实现。 Prototypejs是最早的JavaScript类库,可以说是JavaScript类库的鼻祖。 我在几年前接触的第一个JavaScript类库就是这位,因此Prototypejs有着广泛的...

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

    ES6(ECMAScript 2015)的JavaScript提供了更简洁易懂的类和继承的语法,取代了ES5中复杂的原型链操作。ES6引入了Class关键字,允许开发者使用传统的面向对象编程方式来创建类和实现继承。下面详细介绍ES6中Class类...

    Javascript原型继承

    JavaScript原型继承是面向对象编程在JavaScript中的实现方式之一,它基于原型(Prototype)和对象的特性,使得一个对象可以继承另一个对象的属性和方法。在JavaScript中,每个对象都有一个特殊的内部属性`[...

    js javascript zInherit 对象 继承

    因此,在实际开发中,开发者可能还会结合其他继承策略,如ES6的`class`和`extends`关键字,或者使用`Object.create()`方法,以优化继承机制。 总的来说,`zInherit`是JavaScript对象继承的一种实现,它利用原型链...

    JavaScript Class/Object

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

    JavaScript中的继承之类继承_.docx

    ### JavaScript中的继承——类继承 #### 一、引言 JavaScript是一种动态的、弱类型的编程语言,它支持面向对象编程模式。与传统的面向对象语言如Java或C#不同,JavaScript的继承模型基于原型,这使得它的继承机制...

    baseclass:精益JavaScript继承

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

    javascript继承实例

    JavaScript提供了多种实现继承的方式,包括经典继承、原型链继承、构造函数继承、组合继承、原型式继承、寄生式继承、寄生组合式继承等。其中,最常见的可能是原型链继承和构造函数继承。 1. **原型链继承**:这是...

Global site tag (gtag.js) - Google Analytics