本文介绍无类别面向对象class-free OOP
的编程方式,通过编写函数化构造器来创建对象, 该方式相对比传统伪类 pesudoclassical
的方式(使用new
调用构造器函数),更为简单易懂,并且有许多优势。
这个方法,我是从 Douglas Crockfont 那儿得知的。Douglas是一名JavaScript专家, 写有 JavaScript: The Good Parts , 中译本名为 JavaScript语言精粹。
一本非常好的书,实际上我的这篇文章中很多观点以及示例就是来自该书,以及该函数化构造器书中也有详尽介绍。
伪类有什么问题
那么伪类是什么 以及 伪类有什么不足的地方?
首先从起源说起,早期创造JavaScript时,原型 prototype
参考了Lua这门语言的原型系统。 偏离当今编程语言的主流风格,当今大多数语言都是基于类 class-based
的语言。 尽管原型继承有其自己的表现力(对象可以直接从其他对象继承属性,无类型),但它并未被广泛理解。
另外,JavaScript提供了一套类似 基于类语言的对象构建方法。 有类型化语言编程经验的程序员很少有愿意接受原型,并认为借鉴类型化的语言模糊了JavaScript真实的原型本质,结果两边都不讨好。
这种语法类似类型化语言,但本质还是原型,我们在JavaScript中称其为伪类 pesudoclassical
,一般类似代码如下: 1
2
3
4
5
6
7
8
9
10
11
12
13
14// 创造名为Que的构造器函数,并带有一个 status 属性。
var Que = function(str){
this.status = str;
};
// 通过prototype来给Que所有实例增加一个 get_status 的公共方法。
Que.prototype.get_status = function(){
return this.status;
};
// 创建一个Que实例。
var myQue = new Que("confused");
document.writeln( myQue.get_status() ); // 打印输出结果 "confused"。
函数如果其目的就是要配合new
来一起使用,这种函数一般称作构造器函数
。 使用构造器函数来构造对象,这几乎是JavaScript程序员的常用方法,比如大部分JavaScript Libraries,但普遍并不代表完美。
没有加new,会有问题
构造器函数依旧是函数,为了与普通函数区分,约定俗成用大写首字母的变量名。
但是调用构造器函数的时候,如果没有加new
可能会发生非常糟糕的事情,既没有编译时报错, 也没有运行时报错。
而且忘记加new
,有一个严重危害,this不会绑定在新对象之上。 更加悲剧的是,this会绑定到全局对象。 所以你不但没有正确创建对象,反而破坏了全局变量。
1 | function Pseudo(){ |
上述代码我们创建对象的时候,没有加上new
,全局变量里面多了name
变量,这不是该函数的本意。
其他"伪类"的问题
首先,伪类创建的实例中,所有属性都是公开的,没有私有环境。
再者,在使用伪类时扩展原型以及继承,本质是操作prototype, 伪类本意是靠近面向对象,但实际看起来格格不入。 这方面详情可以参照 JavaScript语言精粹 第五章的第一节伪类,这方面不是本文重点不做详述。
这种prototype操作不直观,导致代码实现丑陋, 这也许是新手学习prototype困难的原因(至少我当年刚接触prototype时很疑惑)。 即使隐藏那些无谓的prototype的实现细节,伪类也还是由之前提及的问题。
在基于类的语言中,类继承是代码重用的唯一方式,而JavaScript有着更多且更好的选择。
无类别 与 函数化
前面提及,创造JavaScript时prototype参考Lua。但当年JavaScript还参考诸多语言, 其函数function
部分深受Lisp这类函数化语言影响。
我直接给出函数化functional
构造器的代码模板: 1
2
3
4
5
6
7
8
9
10
11
12
13var constructor = function(spec){
var that, other; // 私有实例 和 变量
that = {}; // 创建一个新的对象
// 添加给这个对象的公开变量 和方法
that.open_variable = "value";
that.open_function = function(){
};
return that; // 返回这个对象
}
步骤是:
- 创建一个新的对象,可以是通过对象字面量,也可以是new和伪类构造器创建的对象, 也可以是
Object.create
根据已经存在实例来创建的对象,包括别些产生对象的方式。
虽然这篇文章是介绍比伪类更加清晰方便的函数化构造器,但“新”构造器并不阻止通过伪类来创建对象, 甚至几乎任何创建对象方式都可以接受。 - 定义所需要私有变量和方法,没有任何特殊的,仅通过
var
语法定义的普通变量。
对于这个对象的私有方法,其能够访问到构造器传入的参数argument
,that
和其他私有成员。 - 给这个对象扩充其属性 以及
方法 method
, 这些也可以引用之前定义的私有变量和方法。
上述便是函数化构造器的初步介绍,没有难于理解的prototype
,拥有私有变量, 而且实现没有依赖一些特定语法,都是JS常用基础的语法。
这便是前面引文提及的class-free
,即pesudoclassical free
。通过函数的特性来实现。
更安全地定义对象成员方法 method
扩充对象that
方法的时候,可以如之前代码一样,分配一个新的函数作为成员方法method
: that.method1 = function(){};
。
或者更安全地,可以先定义一个函数(私有的),然后再把他们分配个that
1
2
3
4var methodical = function(){
return "safer method.";
}
that.methodical = methodical;
分两步优势是,其他需要调用methodical
的地方, 可以直接调用methodical()
而不是that.methodical()
。 如果创建的对象实例的that.methodical
哪怕被替换了,通过methodical
调用的方法会同样继续工作, 因为直接调用的是定义的私有函数,不受实例修改影响。
另一方面,你也可以相反操作,通过that
来调用, 以便用户修改的实例方法来覆盖 override 原方法并改变结果。
1 | var constructor = function(){ |
上述示例中:obj.fun2()
调用方法,由于我们并不通过调用实例that
的方法, 而是调用相应私有变量以及fun1
函数,所以其输出结果依旧不受影响。 由于更改实例成员,也是常见编程行为操作,这种分两步定义保证代码结果与预期一致, 又不会限制用户操作实例的自由。
继承
关于继承,其实上述函数化构造器是已经包含继承思想了。 在创建对象时,如果不是一个空的对象字面值,而是创建一个存在的对象,这实际就是继承, 后续可以对that
进行一些更改,产生差异化。
All is class-free.
只基于对象,摒弃类的观点,这也是纯粹的原型模型,没有模仿类的语法。 比基于类的继承理念更加简单,只熟悉基于类语言的程序员可能感到陌生,但实际是更容易理解。
superior
方法
super
以及superior
习惯翻译成父类方法,但这里可能"父方法"或"父对象方法"更为准确些。
我们的函数式构造器,有办法来调用superior方法。首先我们构造一个superior
的方法, 它通过方法名取得需要调用的目标函数,之后执行原来的方法,应用参数进行apply
调用。
1 | Object.method("superior", function(name){ |
原生JavaScript的Object
不存在method
方法,method
方法作用是给对象增加方法, 并隐藏了prototype
的实现细节。所以要让上述代码生效,需要先执行如下代码: 1
2
3
4
5
6Function.prototype.method = function(name, func) {
if (!this.prototype[name]) { // 对于基本类型,不存在该方法,才进行添加。
this.prototype[name] = func;
}
return this;
};
上述代码中对
this
的操作var that = this;
, 由于JavaScript函数调用function invocation
时,this会被绑定到全局对象上, 这是语言设计的一个错误。
如果设计正确,内部函数被调用时,this
应该仍被绑定在外部函数的this
上, 理想设计下,内部函数就能够对外部函数对象的访问权。很可惜,并不处在理想情况下。
但解决方案也很简单,定义一个变量并把this
赋值给该变量,内部函数便可通过该变量访问到this
, 变量名约定俗称为that
。
—— JavaScript语言精粹 第四章函数 函数调用模式小节
示例场景
要尝试示例代码的话,不要忘记先执行上一小节的Function.prototype.method
和Object.superior
的代码。
1 | var mammal = function(spec) { |
上面示例代码应该算简单明确,不做过多解释,其中coolcat
通过superior
调用的父对象的方法。
关于函数化,详细部分可参考 JavaScript语言精粹 第五章继承 Inheritance 的 函数化 Functional 小节。
函数化构造器也不是完美的
抛开类的感念,专注于对象,再结合函数化,这种理念直观容易理解。 可是没有绝对完美的事物,不光这样,函数式构造器的“缺陷”也是来自于带来优点的特征。
我们的方法不涉及操作prototype
,带来便利的同时, 我们失去一些JavaScript设计的一些prototype的特性:
通过prototype继承(prototype chain原型链),可以复用父类成员,可以节省一些内存。 而函数化构造器每次创建新对象实例,每个都是独立内存空间的。
诸如
instanceof
,这种本质通过prototype实现的方法,对于函数化创建的实例是无效的。
但上述两点“缺陷”,其实也不是难以接受,下面我分别给出解释。
我们现在硬件水准,足够允许我们能够不节省这些空间, 一般来说,属性成员所占内存空间太微不足道了。
正常情况下,很难因为属性没复用导致性能问题。退一步,真的遇到有个变量达到令人发指的大小,最简单解决方法,提取出来作为一个普通变量, 不作为成员属性。另外不想直接暴露在公共对象的话,利用函数将其限定期望的
作用域scope
内。最后再考虑下,有没有比直接作为变量更好的方式。
instanceof
还是类似 基于类语言的语法。在无类型语言中,鸭子类型 duck type
也能实现。举例来说,我项目涉及JavaScript(比如这个博客网站),我不使用JQuery, 我自己写了一个轻量级的操作DOM元素 DOM Element 的library,名为domutil。 类似JQuery,创建一个wrapper包装对象,其中一个成员名为
el
存放便是原生DOM元素。 其中有些工具方法,是需要操作DOM的,同时为了为了更易用,其接受参数值,可能是wrapper对象 也能就是原生DOM。这时候我只需要判断接受对象是否有
el
成员,如果有通过el
取得DOM元素 如果没有则认为对象就是DOM元素。两种情况最终都获得DOM,然后再进行后续处理。(可参考wrap
方法,其作用是给一个DOM包裹到另一个DOM之中) 这样就无需类型系统中判断类别的处理。
总结
对于只熟悉基于类语言的程序员(一般使用面向对象OOP
语言),可能对本文有些观点感到陌生。(包括刚做程序员的我,当时我仅熟悉Java) 对此,借用我上过的一门公开课1 的老师的话:
"函数化实际上是比OOP简单的,你感觉相反,是因为有先入为主的影响(先学的基于类的语言),实际上函数化更为简单,OOP更为复杂。"
另外,本文也不是争论OOP与函数化那个更优秀,两种方式是程序语言设计两条方向, 如今现代语言也是两者兼备融合,即使严重偏向一方的语言也开始融合另一方的特性, 这种趋势就我认为就是说明,没有孰优孰劣之分。
当今函数化受众可能没有类型系统(其中大头Java C++)多, 所以也希望更多人能体会到函数化的优点,并且也不仅局限于JavaScript,有些理念是语言相同的。