【转】学习NodeJS第六天:JavaScript的继承

人们接触 JavaScript,都被他单纯的外表给骗了,殊不知,一下子又 FP 又 OO 又前台又跑到后台,活蹦乱跳。一旦你遇到某些障碍,面对的JavaScript也表现得脾气好,你怎么弄它,改造它,它也不会生气,却太容易让人迷惑,造 成生气的居然是你或者我。真不知道是你玩 JS 还是变成 JS 玩你……

    许多人被 JS “蛊惑”过之后,深感不爽,立意要重新改造乃万恶的 JS,首当其冲抓住的是便是“原型继承(Prototypical Inherit)”。关于“原型继承”和“类继承(Class Inherit)”,JavaScript 业界教父、Yahoo!UI 架构师 Douglas Crockford(D.C.) 认为是派别的问题(School),就像FP函数式较之于OO,OO 蔚然成主流不等于 FP 便消退其光芒, 而难以能成为为一宗一派立论,否则便是非黑即白。

    如右图是 D.C 本人,老人家了,常言道,老马识途,呵呵。

    D.C 主要的意思是,学术上讨论“原型继承”向来占有一席位置,也有一批语言的思想亦立足于此“原型继承”,但是,当今人们之所以不认识或少见识“原型继承”的 OO方法论,本质里头受Java/C#一派的影响,造成熟悉“类继承”的人群就占绝大多数。而回到“原型继承”的问题上,“原型继承”肯定也有“原型继 承”的优点,有其可取的地方,不然也不能自成一派。至于具体是什么的优点?恕在下技浅、鲜知,大家有空问 Google 或 D.C 的文章当可,让小弟说也是重复 D.C 说过的话。不管怎么样,甚幸 D.C 如此 替JavaScript 的“原型继承”说话,尽管大家还不容易接受,然而那自然是一定无疑的——试问,你我眼中,类的概念已经普遍深入民心,根深蒂固,怎么可以说改就改?过于颠 覆了吧,于是你我继续改造 JavaScript 的继承,使之符合为自己一套的生产经验,去实践应用……

    随着 JavaScript 一路发展,现在已有几套可实现类的继承的方式或者途径呈现在大家面前,如今 NodeJS 的继承却是怎么的一种样子呢?咱们一起观察一下吧。

     NodeJS 的继承一方面没摒弃原型继承,一方面也大量应用类继承,一个类继承一个类,一个类继承一个类下去……sys.inherits() 即是继承任意两个类的方法。该方法支持传入两个 function 参数:sys.inherits(subFn, baseFn);,sunFn 是子类,baseFn 是父类。

一、process.inherits() v.s sys.inherits()

    值得稍作讨论的是继承方法所属的命名空间。原本 inherits() 是依存在 process 对象身上的,后来改为 sys 对象。如果用户键入 process.inherits(...) 旧方法,NodeJS 会提示你这个用法已经弃置了,改用 sys.inherits ,即源码中:

    ……  
    process.inherits = removed("process.inherits() has moved to sys.inherits.");  
    ……  

 新版 Nodejs 还有其他API命名的修改,inherits 只是其中的一项。显然作者Ry作修改有他自己的原因,才会有这样的决定,新版总是比旧版来的有改进,但有没有其他人建议他那样做却无从而知了:)。不过私 下判断,从语意上来说继承方法应该定义在语言核心层次的,至少在sys(System)上比在 process 的语意更为合适,更为贴切。要不然,进程 process 怎么会有继承的功能呢,感觉怪怪的,呵呵。不过话说回来,sys 必须要 require 一下才能用,而 process 却是默认的全局对象,不需要使用到 require 才能访问。

    再说说 inherits() 这个方法和本身这个 inherits 单词性质(呵,真是无聊的俺)。君不见,许多的JS库都有专门针对继承的方法,Prototype.js 的 extend 纯粹是拷贝对象,早期 jQuery 还尚未考虑所谓“继承”,还好留有余地,后来作者 John 把 JavaScript 继承堆到一个层次,连模仿 Java 的 super() 语句都有,实现了基于类 Class 的 JavaScript 继承。为此 John 还写了博文,特别是这篇博文,让好奇的我很受益,了解脱壳的 JS 的方法继承——当然,那些是后话了,不过不能不提的是 jQuery 的继承方法其命名可是“extend()”,而且再说 YUI/Ext 之流亦概莫如是,然而为啥这个 NodeJS 的继承方法管叫做 inherits 呢,并有意无意地还加上第三人称的 -s 的时态!话说 inherits 照译也都是“继承”的意思,跟足了 OO 的原意,但却好像没有 extend 好记好背,敲键盘时便知道……

    前面交待了一些背景后,目的只是想增加大家对继承 inherit 的兴趣,以便接着更深入主题。好了,真的进入主题,立马看看 sys.inherits 源码(exports.inherits,lib/sys.js第327行):

    /** 
     * Inherit the prototype methods from one constructor into another. 
     * 
     * The Function.prototype.inherits from lang.js rewritten as a standalone 
     * function (not on Function.prototype). NOTE: If this file is to be loaded 
     * during bootstrapping this function needs to be revritten using some native 
     * functions as prototype setup using normal JavaScript does not work as 
     * expected during bootstrapping (see mirror.js in r114903). 
     * 
     * @param {function} ctor Constructor function which needs to inherit the 
     *     prototype 
     * @param {function} superCtor Constructor function to inherit prototype from 
     */  
    exports.inherits = function (ctor, superCtor) {  
        ctor.super_ = superCtor;  
        ctor.prototype = Object.create(superCtor.prototype, {  
            constructor: {  
                value: ctor,  
                enumerable: false  
            }  
        });  
    };  

  看来 NodeJS 有点特殊,与 yui、ext 的实现不太一样。可是,究竟是什么道理令到这个继承方法与众不同呢?依据源码表述,比较关键的是,似乎在于 Object.create() 该方法之上。Object.create() 又是什么呢?要疱丁解牛,揭开谜底的答案,我们可以从“基于对象的继承”和“基于类的继承”的认识来入手。

二、基于对象

    首先是“基于对象”的继承。“基于对象”继承的概念,是可以允许没有“类(Class)”的概念存在的。所有的对象都是对 Object 的继承。我要从一个父对象,得到一个新的子对象,例如,兔子可以由“动物”这一对象直接继承。我们在js中:

    // 定义父对象animal  
    var animal = new Object();  
    animal.age = new Number();  
    animal.eat = function(food){...}  
    // 定义子对象兔子rabbit  
    var rabbit = new Object();  
    rabbit.__proto__ = animal;  

  所以这里我们一律说“什么、什么对象”,而不出现“类”。Object就是最原始的“对象”,处于顶层的父对象。JavaScript中任何子对 象其终极的对象便是这个Object。“new Object”的意思是调用命令符new,执行Object构造函数。这是一个空的对象。animal.age = new Number();这一句是分配一个名叫age的属性予以animal对象,其类型是数字number;animal.eat = function(food){...}就是分配一个名叫eat的方法予以animal对象,其参数是food食物。这样,animal动物对象拥有了年 龄age的属性和吃eat的方法,形成一个标准的对象。

    接着,因为兔子肯定符合对象的意思,所以先声明一个空对象,赋予给rabbit变量。像这句话:var rabbit = new Object();然后注意了,rabbit.__proto__ = animal;就是建立继承关系的语句。_proto_是任何对象都有的属性(前提是在Firefox的JS Enginer运行下),也就是说每一个对象都有_proto_属性。改变_proto_的指向等于改变对象原型——也就是我们所说的“父对象”到底是哪 一个。没错就是这么简单完成的JavaScript的继承。

    但是有一个兼容性的问题。这么好用的_proto_居然只准在Firefox的JS引擎中开放,别家的JS引擎就不能够让程序员接触得到。原因有多种多样。总之不能够这样直接使用——无法使用。

    也就是说不提倡_proto_的用法。还好我们知道JavaScript作为动态语言,是支持晚绑定的特性的,就是可以让用户任意在某个对象上添加或删除某个成员。既然可以那样,我们就可以透过复制对象的成员达到派生新的对象的操作,如下例:

// 定义父对象animal  
var animal = new Object();  
animal.age = new Number();  
animal.eat = function(food){...}  
// 定义子对象兔子rabbit  
var rabbit = new Object();  
for(var i in animal){  
    rabbit[i] = animal[i];  
} 

     写一个for列出animal身上的所有成员,统统复制到rabbit这样原本空的对象身上。循环过后就算达到“继承”之目的了。再提炼一下,将for写成一个通用的apply()方法,如下:

Object.apply = function(superObject, sonObject){  
    for(var i in superObject)  
        sonObject[i] = sonObject[i];  
} 

 应当指出,上面的“复制成员理念”可以是可以,并且运行无误,但大家有没有留意到,apply()主要是一个for(...){...}循环。咱们 一想到“循环语句”便很容易联想到耗时、是否会引致死循环等的问题,都是不好的问题,所以看能不能使用这个for循环,总之可以避免循环就好。——问题的 深层次就涉及到代码是否优雅的问题:使用apply()被认为是不优雅的,尤其当越来越多使用apply()的时候,结果是遍布for(...) {...}。当然解决是否优雅最直接的方法是,JavaScript语言提供直接可以代替apply()。你们看,虽然那是如此小的问题,还是值得去修 正,看来一再提倡的,追求完美、追求极致、追求越来越好可不是空喊的一句口号。

   于是,ECMAScript v5.0(一说v3.1)也就是新版JavaScript规定了Object.create()方法,提供了一个由父对象派生子对象的方法。新用法如下:

    // 定义父对象animal  
    var animal = new Object();  
    animal.age = new Number();  
    animal.eat = function(food){...}  
    var rabbit = Object.create(animal);  

     非常直观是吧~一个create()搞掂了~实则回头看看也是表达要封装_proto_的这么一层意义,此处顺便给出实现的方法(唠叨一下,除Mozllia,V8写法亦如此,参见v8natives.js第694行):

    Object.create = function( proto) {  
       var obj = new Object();  
       obj.__proto__ = proto;  
        
       return obj;  
    };  

 当然,for的方法也等价的,

    Object.create = function( proto) {  
       var obj = new Object();  
       for(var i in proto)  
        obj[i] = proto[i];  
        
       return obj;  
    };  

    如果你偏要走捷径,仅仅理解es3.1的改变只是换了马甲的话,变为Object.create(),那只能说是“捷径”。其实它背后还有其他内容的(一 些过程、一些参数……若干原理),俺作了删减,但绝不影响主干意思。如来大家能够理解到这里,就不错了,留个机会大家发掘其他的内容,也省得我费舌 ^_^。(重点提示那个constructor,在第二参数)。

    到了这里已经完成了第一个派别“基于对象”的继承。我觉得,“基于对象继承”的说法可以说是多余的,因为对象就像一个框,什么都可往里在装。继承除了为对 象服务外总不会指别的的意思吧!?所以基于对象的说法,可以说,只为后来,出现更高明的其他思想与之相对,才有基于对象的说法。

    到这里,可以了解“原型继承(Prototypical Inherit)”是怎么一回事了。process.inherits它的原理,在揭开Object.create()神秘面纱后,大概已经呼之欲出了。

三、类

   前文里头卖了一个关子,所谓更高明的“思想”,就是类啦!表面上,类其实和对象没什么不同,也有方法、属性、事件等的概念,实际上,类就是对 象的模板。好,明确这点后,我们清楚“类”作为一种特殊的“事物”,当然也不是凭空而生的。下面的JS语句结果是一样的,我们可以通过两者对比理解一下由 “对象”到“类对象”的过程:

    // 定义一个JS类(类在js中表现为Function)  
    function Foo(){  
        // ……构造器过程  
    }  
    var o = new Object();  
    o.constructor = Foo;  
    Foo.call(o); // <---此步就是调用Foo(),更确切地说,是调用Foo构造函数。 其作用相当于var o = new Foo();  

 为什么要call()呢?因为new命令调用构造器function Foo(){},最后必然会返回this当前实例对象,即:

    funtion Foo(){  
       // ……构造器过程  
       // return this;  
    }  
 

    这个当前实例对象是啥呢?——在例子中便是o了。o事先声明罢了。这样我们看到对象到类的“升华”!

    是不是还是觉得不够透切呢?咱们还没说完咧~我们可以结合兔子的例子,同样是动物和兔子,写成类,从而诞生了JS中一种类的写法!

    animal = function(){}  
    animal.prototype.age = new Number();  
    animal.prototype.eat = function(food){  
    }  
    rabbit = function(){}  
    rabbit.prototype = new animal();  

 开玩笑了, 这种写法才是JS的地道写法,老早就有了。上述写法彻底告别一个对象一个对象去定义层次关系。简单说,其涵义就是通过函数的原型prototype,加 多一层function来确定父对象是什么。首先有个认识,就是比起“基于对象”的继承,我们现在可以加入了“构造函数”,例如animial = function(){}和rabbit = function(){}分别就是父类的构造函数和子类的构造函数。但是如果我不需要子类的构造函数,却又不行,因为不可能不写一个function,只 有function才可以有prototype属性去定义成员。前面我们不是说过_proto_是不开放的属性吗?惟独Function的_proto_ 就总是开放的,也就是说Function对象都有的_proto_的作用apply()和call()的作用,但是_proto_的名字就变为没有下划线 了,也就是Function.prototype。况且JS之中,借助Function定义对象的模板是经常的写法,new某个类就是建立对象,也让 prototype发挥定义继承链作用。

    既然Function.prototype总是开放的,那么用它代替_proto_也行吧?没错,借助一个空的构造函数就行了,原来Object.create也可以这样写的:

    Object.create = function (o) {  
            function F() {}  
            F.prototype = o;  
            return new F();  
     }  

 当然这个object方法又回归到“基于对象继承”的方法上了 呵呵。我们可以从D.C介绍过的方法看出一点源头,借助网络,这些渊源都是有迹可循的。详见参考网址http://javascript.crockford.com/prototypal.html 。实际上Object.create应该就从D.C方法来,好像他也是极力的推动者,不知道了……最后抄多个Extend代码帮助理解,原理没啥区别,关键胜在够简单清晰。

    extend = function (Klass, Zuper) {  
            Klass.prototype = Object.create(Zuper.prototype);  
            Klass.prototype.constructor = Klass;  
    }  
 

参考:

猜你喜欢

转载自andy0807.iteye.com/blog/1446781