本文介绍无类别面向对象class-free OOP的编程方式,通过编写函数化构造器来创建对象, 该方式相对比传统伪类 pesudoclassical的方式(使用new调用构造器函数),更为简单易懂,并且有许多优势。

这个方法,我是从 Douglas Crockfont 那儿得知的。Douglas是一名JavaScript专家, 写有 JavaScript: The Good Parts , 中译本名为 JavaScript语言精粹

一本非常好的书,实际上我的这篇文章中很多观点以及示例就是来自该书,以及该函数化构造器书中也有详尽介绍。

伪类有什么问题

那么伪类是什么 以及 伪类有什么不足的地方?

首先从起源说起,早期创造JavaScript时,原型 prototype参考了Lua这门语言的原型系统。 偏离当今编程语言的主流风格,当今大多数语言都是基于类 class-based的语言。 尽管原型继承有其自己的表现力(对象可以直接从其他对象继承属性,无类型),但它并未被广泛理解。
另外,JavaScript提供了一套类似 基于类语言的对象构建方法。 有类型化语言编程经验的程序员很少有愿意接受原型,并认为借鉴类型化的语言模糊了JavaScript真实的原型本质,结果两边都不讨好。

这种语法类似类型化语言,但本质还是原型,我们在JavaScript中称其为伪类 pesudoclassical,一般类似代码如下:

JavaScript constructor invocation
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
2
3
4
5
6
7
8
9
10
function Pseudo(){
this.name = "dangerous";
};

// 忘记加上 new
var obj = Pseudo();

// 全局变量里面多了 名为 name 的变量
console.log(name); // 输出结果 "dangerous"
console.log(window.name); // 输出结果 "dangerous"

上述代码我们创建对象的时候,没有加上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
13
var constructor = function(spec){
var that, other; // 私有实例 和 变量

that = {}; // 创建一个新的对象

// 添加给这个对象的公开变量 和方法
that.open_variable = "value";
that.open_function = function(){

};

return that; // 返回这个对象
}

步骤是:

  1. 创建一个新的对象,可以是通过对象字面量,也可以是new和伪类构造器创建的对象, 也可以是Object.create根据已经存在实例来创建的对象,包括别些产生对象的方式。
    虽然这篇文章是介绍比伪类更加清晰方便的函数化构造器,但“新”构造器并不阻止通过伪类来创建对象, 甚至几乎任何创建对象方式都可以接受。
  2. 定义所需要私有变量和方法,没有任何特殊的,仅通过var语法定义的普通变量。
    对于这个对象的私有方法,其能够访问到构造器传入的参数argument, that和其他私有成员。
  3. 给这个对象扩充其属性 以及 方法 method, 这些也可以引用之前定义的私有变量和方法。

上述便是函数化构造器的初步介绍,没有难于理解的prototype,拥有私有变量, 而且实现没有依赖一些特定语法,都是JS常用基础的语法。

这便是前面引文提及的class-free,即pesudoclassical free。通过函数的特性来实现。

更安全地定义对象成员方法 method

扩充对象that方法的时候,可以如之前代码一样,分配一个新的函数作为成员方法methodthat.method1 = function(){};

或者更安全地,可以先定义一个函数(私有的),然后再把他们分配个that

1
2
3
4
var methodical = function(){
return "safer method.";
}
that.methodical = methodical;

分两步优势是,其他需要调用methodical的地方, 可以直接调用methodical()而不是that.methodical()。 如果创建的对象实例的that.methodical哪怕被替换了,通过methodical调用的方法会同样继续工作, 因为直接调用的是定义的私有函数,不受实例修改影响。
另一方面,你也可以相反操作,通过that来调用, 以便用户修改的实例方法来覆盖 override 原方法并改变结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
var constructor = function(){
var a = 1, that = {}; // create new object
var fun1 = function (){ // the method will be changed in instance
return "OK"
};
var fun2 = function (){
return "I'm " + fun1() + " " + a;
};

that.a = a;
that.fun1 = fun1;
that.fun2 = fun2;

return that;
};

var obj = constructor();
// 修改实例
obj.a = 100
obj.fun1 = function() {
return "changed";
}
document.writeln( obj.a ) // 100
document.writeln( obj.fun1() ); // changed

document.writeln( obj.fun2() ); // I'm OK 1

上述示例中:obj.fun2()调用方法,由于我们并不通过调用实例that的方法, 而是调用相应私有变量以及fun1函数,所以其输出结果依旧不受影响。 由于更改实例成员,也是常见编程行为操作,这种分两步定义保证代码结果与预期一致, 又不会限制用户操作实例的自由。

继承

关于继承,其实上述函数化构造器是已经包含继承思想了。 在创建对象时,如果不是一个空的对象字面值,而是创建一个存在的对象,这实际就是继承, 后续可以对that进行一些更改,产生差异化。

All is class-free.

只基于对象,摒弃类的观点,这也是纯粹的原型模型,没有模仿类的语法。 比基于类的继承理念更加简单,只熟悉基于类语言的程序员可能感到陌生,但实际是更容易理解。

superior方法

super以及superior习惯翻译成父类方法,但这里可能"父方法"或"父对象方法"更为准确些。

我们的函数式构造器,有办法来调用superior方法。首先我们构造一个superior的方法, 它通过方法名取得需要调用的目标函数,之后执行原来的方法,应用参数进行apply调用。

1
2
3
4
5
6
7
Object.method("superior", function(name){
var that = this;
var method = that[name];
return function (){
return method.apply(that, arguments);
};
});

原生JavaScript的Object不存在method方法,method方法作用是给对象增加方法, 并隐藏了prototype的实现细节。所以要让上述代码生效,需要先执行如下代码:

1
2
3
4
5
6
Function.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.methodObject.superior的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
var mammal = function(spec) {
var that = {};
that.get_name = function() {
return spec.name;
};
that.say = function() {
return spec.saying || "";
};
return that;
}

var cat = function(spec) {
spec.saying = spec.saying || "meow"; // default "meow"
var that = mammal(spec);
that.get_name = function() {
return that.say() + " " + spec.name + " " + that.say();
};
return that;
};

var coolcat = function(spec) {
var that = cat(spec),
super_get_name = that.superior("get_name");
that.get_name = function() {
return "like " + super_get_name() + " baby";
};
return that;
};

var cooldog = function(spec) {
spec.saying = spec.saying || "Woof!"
var that = coolcat(spec);
return that
}

var mimi_cat = coolcat({name: "Mimi"});
var wang_dog = cooldog({name: "Wang Cai"});

document.writeln(mimi_cat.get_name()); // like meow Mimi meow baby
document.writeln(wang_dog.get_name()); // like Woof! Wang Cai Woof! baby

上面示例代码应该算简单明确,不做过多解释,其中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,有些理念是语言相同的。


  1. 华盛顿大学 Dan Grossman教授的 cousera课程。非常推荐学习, 课程专注于程序语言背后共性,并不是仅介绍如何使用某类语言的语法课程(虽说这部分内容也是很多), 而是更深入的思考。
    会涉及ml,ruby,racket这三门语言,学完你会发现,市面上很多新语言新特性实际上可能并不那么新, 甚至某些特性可以称得上历史悠久,仅仅是一直没有成为主流。