再谈javascript面向对象编程
前言:虽有陈皓《Javascript 面向对象编程》珠玉在前,但是我还是忍不住再画蛇添足的补上一篇文章,主要是因为 javascript 这门语言魅力。另外这篇文章是一篇入门文章,我也是才开始学习 Javascript,有一点心得,才想写一篇这样文章,文章中难免有错误的地方,还请各位不吝吐槽指正
吐槽 Javascript
初次接触 Javascript,这门语言的确会让很多正规军感到诸多的不适,这种不适来自于 Javascript 的语法的简练和不严谨,这种不适也来自 Javascript 这个悲催的名称,我在想网景公司的 Javascript 设计者在给他起名称那天一定是脑壳进水了,让 Javascript 这么多年来受了这么多不白之冤,人们都认为他是 Java 的附属物,一个 WEB 玩具语言。因此才会有些人会对 Javascript 不屑,认为 Javascript 不是一门真正的语言,但是这此他们真的错了。Javascript 不仅是一门语言,是一门真真正正的语言,而且他还是一门里程碑式的语言,他独创多种新的编程模式原型继承,闭包,对后来的动态语言产生了巨大的影响。做为 当今最流行的语言(没有之一),看看 git 上提交的最多的语言类型就能明白。随着 HTML5 的登场,浏览器将在个人电脑上将大显身手,完全有替换 OS 的趋势的时候,Javascript 做为浏览器上的一门唯一真真的语言,如同C之于 unix/linux,java 之于 JVM,Cobol 之于 MainFrame,我们也需要来重新的认真地认识和审视这门语言。另外 Javascript 的正式名称是:ECMAScript,这个名字明显比 Javascript 帅太多了!
言归正传,我们切入主题——Javascript 的面向对象编程。要谈 Javascript 的面向对象编程,我们第一步要做的事情就是忘记我们所学的面向对象编程。传统 C++ 或 Java 的面向对象思维来学习 Javascript 的面向对象会给你带来不少困惑,让我们先忘记我们所学的,从新开始学习这门特殊的面向对象编程。既然是 OO 编程,要如何来理解 OO 编程呢,记得以前学C++,学了很久都不入门,后来有幸读了《Inside The C++ Object Model》这本大作,顿时豁然开朗,因此本文也将以对象模型的方式来探讨的 Javascript 的 OO 编程。因为 Javascript 对象模型的特殊性,所以使得 Javascript 的继承和传统的继承非常不一样,同时也因为 Javascript 里面没有类,这意味着 Javascript 里面没有 extends,implements。那么 Javascript 到底是如何来实现 OO 编程的呢?好吧,让我们开始吧,一起在 Javascript 的 OO 世界里来一次漫游
首先,我们需要先看看 Javascript 如何定义一个对象。下面是我们的一个对象定义:
var o = {};
还可以这样定义一个对象
function f () { }
对,你们没有看错,在 Javascript 里面,函数也是对象。
当然还可以
var array1= [ 1,2,3];
数组也是一个对象。
其他关于对象的基本的概念的描述,还是请各位亲们参见陈皓《Javascript 面向对象编程》文章。
对象都有了,唯一没有的就是 class,因为在 Javascript 里面是没有 class 关键字的,算好还有 function,function 的存在让我们可以变通的定义类,在扩展这个主题前,我们还需要了解一个 Javascript 对象最重要的属性,__proto__成员。
__proto__成员
严格的说这个成员不应该叫这个名字,__proto__是 Firefox 中的称呼,__proto__只有在 Firefox 浏览器中才能被访问到。做 为一个对象,当你访问其中的一个成员或方法的时候,如果这个对象中没有这个方法或成员,那么 Javascript 引擎将会访问这个对象的__proto__成员所指向的另外的一个对象,并在那个对象中查找指定的方法或成员,如果不能找到,那就会继续通过那个对象的 __proto__成员指向的对象进行递归查找,直到这个链表结束。
好了,让我们举一个例子。
比如上上面定义的数组对象 array1。当我们创建出 array1 这个对象的时候,array1实际在 Javascript 引擎中的对象模型如下:
array1对象具有一个 length 属性值为3,但是我们可以通过如下的方法来为 array1 增加元素:
array1.push (4);
push 这个方法来自于 array1 的__proto__成员指向对象的一个方法(Array.prototye.push ())。正是因为所有的数组对象(通过[]来创建的)都包含有一个指向同一个具有 push,reverse 等方法对象(Array.prototype)的__proto__成员,才使得这些数组对象可以使用 push,reverse 等方法。
那么这个__proto__这个属性就相当于面向对象中的”has a”关系,这样的的话,只要我们有一个模板对象比如 Array.prototype 这个对象,然后把其他的对象__proto__属性指向这个对象的话就完成了一种继承的模式。不错!我们完全可以这么干。但是别高兴的太早,这个属性只在 FireFox 中有效,其他的浏览器虽然也有属性,但是不能通过__proto__来访问,只能通过 getPrototypeOf 方法进行访问,而且这个属性是只读的。看来我们要在 Javascript 实现继承并不是很容易的事情啊。
函数对象 prototype 成员
首先我们先来看一段函数 prototype 成员的定义,
When a function object is created, it is given a prototype member which is an object containing a constructor member which is a reference to the function object
当一个函数对象被创建时,这个函数对象就具有一个 prototype 成员,这个成员是一个对象,这个对象包含了一个构造子成员,这个构造子成员会指向这个函数对象。
例如:
function Base () { this.id = "base" }
Base 这个函数对象就具有一个 prototype 成员,关于构造子其实 Base 函数对象自身,为什么我们将这类函数称为构造子呢?原因是因为这类函数设计来和 new 操作符一起使用的。为了和一般的函数对象有所区别,这类函数的首字母一般都大写。构造子的主要作用就是来创建一类相似的对象。
上面这段代码在 Javascript 引擎的对象模型是这样的
new 操作符
在有上面的基础概念的介绍之后,在加上 new 操作符,我们就能完成传统面向对象的 class + new 的方式创建对象,在 Javascript 中,我们将这类方式成为 Pseudoclassical。
基于上面的例子,我们执行如下代码
var obj = new Base ();
这样代码的结果是什么,我们在 Javascript 引擎中看到的对象模型是:
new 操作符具体干了什么呢?其实很简单,就干了三件事情。
var obj = {}; obj.__proto__ = Base.prototype; Base.call (obj);
第一行,我们创建了一个空对象 obj
第二行,我们将这个空对象的__proto__成员指向了 Base 函数对象 prototype 成员对象
第三行,我们将 Base 函数对象的 this 指针替换成 obj,然后再调用 Base 函数,于是我们就给 obj 对象赋值了一个 id 成员变量,这个成员变量的值是”base”,关于 call 函数的用法,请参看陈皓《Javascript 面向对象编程》文章
如果我们给 Base.prototype 的对象添加一些函数会有什么效果呢?
例如代码如下:
Base.prototype.toString = function() { return this.id; }
那么当我们使用 new 创建一个新对象的时候,根据__proto__的特性,toString 这个方法也可以做新对象的方法被访问到。于是我们看到了:
构造子中,我们来设置‘类’的成员变量(例如:例子中的 id),构造子对象 prototype 中我们来设置‘类’的公共方法。于是通过函数对象和 Javascript 特有的__proto__与 prototype 成员及 new 操作符,模拟出类和类实例化的效果。
Pseudoclassical 继承
我们模拟类,那么继承又该怎么做呢?其实很简单,我们只要将构造子的 prototype 指向父类即可。例如我们设计一个 Derive 类。如下
function Derive (id) { this.id = id; } Derive.prototype = new Base (); Derive.prototype.test = function(id){ return this.id === id; } var newObj = new Derive ("derive");
这段代码执行后的对象模型又是怎么样的呢?根据之前的推导,应该是如下的对象模型
这样我们的 newObj 也继承了基类 Base 的 toString 方法,并且具有自身的成员 id。关于这个对象模型是如何被推导出来的就留给各位同学了,参照前面的描述,推导这个对象模型应该不难。
Pseudoclassical 继承会让学过C++/Java 的同学略微的感受到一点舒服,特别是 new 关键字,看到都特亲切,不过两者虽然相似,但是机理完全不同。当然不关什么样继承都是不能离不开__proto__成员的。
Prototypal 继承
这是 Javascript 的另外一种继承方式,这个继承也就是之前陈皓文章《Javascript 面向对象编程》中 create 函数,非常可惜的是这个是 ECMAScript V5 的标准,支持 V5 的浏览器目前看来也就是 IE9,Chrome 最新版本和 Firefox。虽然看着多,但是做为 IE6 的重灾区的中国,我建议各位还是避免使用 create 函数。好在没有 create 函数之前,Javascript 的使用者已经设计出了等同于这个函数的。例如:我们看看 Douglas Crockford 的 object 函数。
function object (old) { function F () {}; F.prototype = old; return new F (); } var newObj = object (oldObject);
例如如下代码段
var base ={ id:"base", toString:function(){ return this.id; } }; var derive = object (base);
上面函数的执行后的对象模型是:
如何形成这样的对象模型,原理也很简单,只要把 object 这个函数扩展一下,就能画出这个模型,怎么画留给读者自己去画吧。
这样的继承方式被称为原型继承。相对来说要比 Pseudoclassical 继承来的简单方便。ECMAScript V5 正是因为这原因也才增加 create 函数,让开发者可以快速的实现原型继承。
上述两种继承方式是 Javascript 中最常用的继承方式。通过本文的讲解,你应该对 Javascript 的 OO 编程有了一些‘原理’级的了解了吧
参考:
《Prototypes and Inheritance in JavaScript Prototypes and Inheritance in JavaScript》
Advance Javascript (Douglas Crockford 大神的视频,一定要看啊)
题外话:
web2.0后,web 应用可谓飞速发展,如今在 HTML5 发布之际,浏览器的功能被大大强化,我感觉 Browser 远远在不是一个 Browser 那么简单了。记得 C++ 之父曾经这样说过 JAVA,JAVA 不是跨平台,JAVA 本身就是一个平台。如今的 Browser 也本身就是一个平台了,好在这个平台是基于标准的。如果 Browser 是平台,由于 Browser 安全沙箱的限制,个人电脑的资源被使用的很少,感觉 Browser 就是一个 NC(Network Computer)?我们居然又回到了 Sun 最初提出的构想,Sun 是不是太强大了些?