读源代码——def.js

缘起

前两天同事晓孟给我们做了一期 JS 框架的分享,秀了一段神奇的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def ("Person") ({
init: function(name){
this.name = name;
},

speak: function(text){
alert(text || "Hi, my name is " + this.name);
}
});

def ("Ninja") << Person ({
init: function(name){
this._super();
},

kick: function(){
this.speak("I kick u!");
}
});

var ninjy = new Ninja("JDD");

ninjy.speak();
ninjy.kick();

这段 JS 代码是在模拟 Ruby,定义了一个“类” Person 以及一个继承了 Person 的“类” Ninja。看到这段代码的时候,我最惊讶的是 11 行里面的 << 是怎么做到继承的,JS 里面是没有运算符重载的。这个神奇的效果吸引我在分享会后打开了 def.js 的代码库。

定义类

预备

在分析源代码之前,我们先复习一下 JS 中的两个概念,构造函数和原型链。

构造函数和原型链

在 JS 中,任何函数都可以成为构造函数;只要通过 new 关键字方式调用,就称其为构造函数。new关键字调用构造函数时会

  1. 创建一个对象
  2. 把构造函数内的this指向这个对象
  3. 把这个对象的 __proto__ 指向构造函数的prototype
  4. 这个对象就是new表达式的值

JS 中对象会沿着原型链,即__proto__查找属性,一直到原型链的顶部。

1
2
3
4
5
6
7
8
9
10
11
12
function Foo() {
this.value = 42;
}

Foo.prototype.method = function() {
this.value = this.value - 1;
}

var foo = new Foo();
foo.value; // 42
foo.method();
foo.value; // 41

上面代码中第 1 行到第 9 行就是 def.js 定义一个类的示例,重写如下

1
2
3
4
5
6
7
8
9
10
var P = def ("Person");
P ({
init: function(name){
this.name = name;
},

speak: function(text){
alert(text || "Hi, my name is " + this.name);
}
});

我特意把它分成两个部分,第一部分是第 1 行 def ("Person"), 其中 def 是 def.js 定义的一个函数(删除了部分代码方便理解)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
(function(global){
var deferred;
// ...
function def(context, klassName) {
klassName || (klassName = context, context = global);
// 定义一个构造函数
var Klass = context[klassName] = function Klass(){
return this.init && this.init.apply(this, arguments);
}
// make this class extendable
Klass.extend = extend;

// 设置属性
deferred = function(props){
return Klass.extend(props);
};

// ...
return deferred;
}
global.def = def;
}(this));

def 做了两件事

  1. 定义了构造函数,并把这个构造函数Klass赋给一个全局变量。
  2. 返回一个函数,这个函数输入一个定义各种属性的对象,用这些属性扩展(extend)构造函数Klassprototype
1
2
3
4
5
6
7
8
9
10
function extend(source) {
var prop, target = this.prototype;

for (var key in source)
if (source.hasOwnProperty(key)) {
prop = target[key] = source[key];
}
}
return this;
}

那么,def ("Person") 就是一个函数,我把它赋给变量P。同时,生成了一个全局变量Person,它是一个构造函数。

第二部分,调用函数P({...}),扩展了Person这个构造函数,把构造函数的prototype增添上了initspeak属性。

Person

继承一个类

接下来看看示例代码中最神奇的一段

1
2
3
4
5
6
7
8
9
def ("Ninja") << Person ({
init: function(name){
this._super();
},

kick: function(){
this.speak("I kick u!");
}
});

为了方便理解,把它重写成

1
2
3
4
5
6
7
8
9
10
11
12
13
var N = def ("Ninja");

var temp = Person ({
init: function(name){
this._super();
},

kick: function(){
this.speak("I kick u!");
}
});

N << temp;

先看第一段。 def ("Ninja")本身是一个函数,我把它赋给变量N;同时会生成一个全局的构造函数Nijia

再看第二段。这里把Person这个构造函数当做普通函数使用,会发生什么事情呢?还是再来复习一下 JS

预备

又是构造函数

构造函数直接调用时,没有新的对象被创建,而this会被指向global

1
2
3
4
5
function Foo() {
this.bla = 1; // 获取设置全局参数
}
Foo(); // undefined
bla; // 1

闭包

看下面的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

var fn = (function() {
var closure = {};
function f(name) {
return function() {
closure.pk = name;
}
}
return f;
})();

var a = fn('a');
a();
var b = fn('b');
b();

变量ab有共同的闭包closure,def.js 就是利用这一点传递属性。

closure

Object.prototype.valueOf()

在需要原生值(primitive value)的时候却遇到了对象,JS 就会自动调用valueOf方法把对象转换为原生值。

1
2
3
4
5
6
7
8
9
10
11
12
13

function MyNumber(value) {
this.value = value;
}
MyNumber.prototype.valueOf = function() {
console.log(this.value);
return this.value;
}

var num1 = new MyNumber(1);
var num2 = new MyNumber(2);

num1 + num2; // 调用 valueOf,会打印 1 和 2 到 console

再看看 def.js def 部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
(function(global){

// 闭包用来传递属性,继承关系
var deferred;
// ...
function def(context, klassName) {
klassName || (klassName = context, context = global);
// 定义一个构造函数
var Klass = context[klassName] = function Klass(){
if(context != this){
// called as a constructor
// allow init to return a different class/object
return this.init && this.init.apply(this, arguments);
}
// called as a function - defer setup of superclass and properties
deferred._super = Klass; // 继承
deferred._props = arguments[0] || { }; // 传递属性
}
// ...
}
global.def = def;
}(this));

当把Person这个构造函数当做普通函数使用的时候,contextthis 都是 global,会利用PersonNinja共同的闭包deferred做属性传递。

当示例代码执行到 N << temp 的时候,JS 会自动调用 def.js 内定义的valueOf方法,实现继承。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
deferred.valueOf = function(){
var Superclass = deferred._super;

// inherit from superclass
Subclass.prototype = Superclass.prototype;
var proto = Klass.prototype = new Subclass;
// reference base and superclass
Klass._class = Klass;
Klass._super = Superclass;

// enforce the constructor to be what we expect
proto.constructor = Klass;
// to call original methods in the superclass
proto._super = base;
// set properties
deferred(deferred._props);
};

最后

也没什么别的,多读读代码总有好处。欢迎大家推荐类似短小精悍的代码库:)