coffeescript是一门编译到javascript的子语言, 它采用了类似ruby/python的语法,增加了类支持,以及规避了javascript语言里面一堆的设计缺陷。 本文主要分析一下coffeescript是如何实现类机制的。
示例代码
首先我们给出一个coffeescript的示例代码,我们会分析这部分代码的编译结果,弄懂它是如何实现类的功能的。
1 2 3 4 5 6 7 8 9 10 11 12 |
|
上面的coffeescript代码中,People实现了hello方法,Programmer继承了People,并且重载了hello方法。
这里是生成的全部javascript代码,我们会一部分一部分地分析它到底做了什么事情:
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 |
|
类定义
我们首先弄清楚coffeescript的类是如何实现的。下面是Programmer的定义:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
如上面代码所示,Programmer其实就是一个闭包函数,在闭包里面生成了一个Programmer构造函数,
这样就可以通过p = new Programmer('halida');
来创建一个Programmer对象。
对于对象方法hello的创建,是在闭包里面给prototype赋值的方式来实现,
coffeescript里面可以用super
这个关键词来继承父类里面同样名称的方法:
1 2 3 4 5 |
|
然后是Programmer的构造函数,类似于ruby语言里面的initialize:
1 2 3 |
|
Programmer.__super__
是父类的构造函数(后面在讲__extends会提到是如何生成它的),
直接获取父类的构造函数constructor(这个是coffeescript缓存的, 下面会讲),
传给它本函数的参数arguments
,然后在this
这个环境里面执行它。
Programmer.hello
里面,也采用了同样的方式来继承父类的方法:
1
|
|
里面call(this)
是为了把当前环境切换到当前对象中去。
这样我们大致知道了类定义部分的代码到底发生了什么,
不过我们还是不清楚类继承是如何实现的,
魔法发生在__extends(Programmer, _super);
里面。
类继承的实现
首先看一下__extends
的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
我们来实际执行__extends(Programmer, _super);
,看看到底发生了什么,
在这里面,_super对应的值是父类People。
首先是第一个循环:
1 2 3 4 |
|
__hasProp.call(parent, key)
是用来判断key是否是parent本身定义的属性。
这段代码是循环People
里面所有的属性, 如果是People本身定义的, 就赋值到child里面去,
它的目的是继承父类的类方法和属性。 如果People.CLASS_NAME = "People";
,
那么结果就是Programmer.CLASS_NAME = People";
,
这样通过拷贝的方式,子类继承了父类的所有类方法。
然后是难懂的部分了, 如何继承父类的对象方法呢?
首先给child生成一个prototype对象构造函数,在里面还会缓存child的构造函数constructor, 这样child的child就可以通过调用它来执行父类的方法(实现了上面类定义部分的调用父类对象方法):
1 2 3 |
|
最顶层的父类People里面没有定义constructor,
是因为js里面返回函数的对象构造函数,它本身的prototype里面就有constructor,
console里面执行:People.prototype.constructor
,返回的是:
1 2 3 |
|
简单地说,child的prototype对象的prototype就是父类的prototype,
这样,子类对象找一个方法的时候,如果在它自己的prototype,也就是ctor里面找不到对应的方法,
就会在ctor的prototype里面寻找这个方法,然后就可以从父类里面找到了。
这就是为什么要用new ctor()
来创建一个prototype对象, 这样才能形成一个prototype调用链:
1 2 |
|
以及上面提到的, __super__
缓存了父类的prototype。
1
|
|
这部分概念比较难懂,你可以把上面的部分多看几遍,好好思考一下,或者继续往下看,一次实际的调用是如何做的。
走一遍
上面是对代码本身的分析,要弄懂,我们还需要模拟执行一下,理清思路。
我们创建一个对象:p = new Programmer('halida');
如果需要找Programmer里面定义的方法, 我们假设是coding
吧, 那么调用的过程是:
- 执行
p.coding()
。 - 在p对象里面找是否有coding。
- 在p的prototype
new ctor();
里面找是否有coding。定义Programmer的时候,添加的方法(比如上面示例代码的hello)都是塞到它里面去的。 - 在prototype的prototype:Programmer的prototype里面找是否有coding。
这个是对象方法的执行,还有类方法的执行,相对比较简单。例如Programmer.CLASS_NAME
:
- 在对象的prototype里面寻找CLASS_NAME。
- 在Programmer里面找是否有CLASS_NAME。
继承父类的时候,会拷贝出所有的父类方法,在子类定义的时候,如果定义了类方法,就会覆盖掉父类的类方法。