学习NodeJS第五天:JavaScript的继承

简介:     人们接触 JavaScript,都被他单纯的外表给骗了,殊不知,一下子又 FP 又 OO 又前台又跑到后台,活蹦乱跳。一旦你遇到某些障碍,面对的 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 的继承却是怎么的一种样子呢?咱们一起观察一下吧。

     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 单词性质(呵,真是无聊的俺)。君不见,许多的JS库都有专门针对继承的方法,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 * 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 } }); };

    看来 node.js 有点特殊,与 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 引擎就不能够让程序员接触得到。(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(一说 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; }

四、结语

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

参考:

目录
相关文章
|
JavaScript 前端开发 Java
深入JS面向对象(原型-继承)(一)
深入JS面向对象(原型-继承)
30 0
|
27天前
|
JavaScript 前端开发
js开发:请解释原型继承和类继承的区别。
JavaScript中的原型继承和类继承用于共享对象属性和方法。原型继承利用原型链查找属性,节省内存但不支持私有成员。类继承通过ES6的class和extends实现,支持私有成员但占用更多内存。两者各有优势,适用于不同场景。
18 0
|
3天前
|
JavaScript 前端开发 应用服务中间件
node.js之第一天学习
node.js之第一天学习
|
29天前
|
运维 JavaScript 前端开发
发现了一款宝藏学习项目,包含了Web全栈的知识体系,JS、Vue、React知识就靠它了!
发现了一款宝藏学习项目,包含了Web全栈的知识体系,JS、Vue、React知识就靠它了!
|
1月前
|
设计模式 JavaScript 前端开发
JavaScript中继承的优缺点
JavaScript中继承的优缺点
13 3
|
1月前
|
JavaScript 前端开发
如何在 JavaScript 中实现继承?
如何在 JavaScript 中实现继承?
11 2
|
1月前
|
JavaScript 前端开发
js继承的超详细讲解:原型链继承、构造函数继承、组合继承、原型式继承、寄生式继承、寄生组合式继承、class继承
js继承的超详细讲解:原型链继承、构造函数继承、组合继承、原型式继承、寄生式继承、寄生组合式继承、class继承
51 0
|
1月前
|
前端开发 搜索推荐 JavaScript
编程笔记 html5&css&js 001 学习编程从网页开始
编程笔记 html5&css&js 001 学习编程从网页开始
|
2月前
|
前端开发 JavaScript 算法
在 JavaScript 中,有哪些方式可以达到继承的效果?
在 JavaScript 中,有哪些方式可以达到继承的效果?
29 0
|
2月前
|
前端开发 JavaScript
从零开始学习前端开发:HTML、CSS、JavaScript入门指南
【2月更文挑战第1天】本文将带领读者从零开始学习前端开发,介绍HTML、CSS和JavaScript的基础知识与应用,帮助读者快速入门前端开发领域。
62 1