学习NodeJS第五天:JavaScript的继承

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

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

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

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

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

     Node.js 的继承一方面没摒弃原型继承,一方面也大量应用类继承,一个类继承一个类,一个类继承一个类下去……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.");
……

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

    再说说 inherits() 这个方法和本身这个 inherits 单词性质(呵,真是无聊的俺)。君不见,许多的 JavaScript 库都有专门针对继承的方法,Prototype.js 的 extend 纯粹是拷贝对象,早期 jQuery 还尚未考虑所谓“继承”,还好留有余地,后来作者 John 把 JavaScript 继承推到一个层次,连模仿 Java 的 super() 语句都有,实现了基于类 Class 的 JavaScript 继承。为此 John 还写了博文,特别是这篇博文,让好奇的我很受益,了解脱壳的 JS 的方法继承——当然,那些是后话了,不过不能不提的是 jQuery 的继承方法其命名可是“extend()”,而且再说 YUI/Ext JS 之流亦概莫如是,然而为啥这个 node.js 的继承方法管叫做 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
 *     prot

    看来 node.js 有点特殊,与 YUI、Ext.js 的实现不太一样。可是,究竟是什么道理令到这个继承方法与众不同呢?依据源码表述,比较关键的是,似乎在于 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 引擎就不能够让程序员接触得到。(Thx to qinfanpeng)原因有多种多样。总之不能够这样直接使用——无法使用。

    也就是说不提倡 _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 也就是新版 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;
}

四、结语

今天说的大概可分后两部分,前部分打算从 node.js 的继承说开去,虽然不知道是不是废话较多,以致于后半部分衔接得不够好,但是还是说出了我的心底话,来来去去都是那几样事物,因为高度抽象,可能不易厘清,俺尽量也想说的圆,可不容易,他日有机会修正文章,这份读到的便当草稿吧,有缘人请将就过目,感觉有疑的话请尽管斧正,当然有窍门请解囊告知,多多提携!

参考:

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

sp42a

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值