慎用 javascript 的 closure
javascript 的对象有一个不足的地方是没有提供公有和私有成员的区分,不过通过 javascript 提供的 closure,弄出私有的效果不算太难,但是使用 closure 却有可能带来效率的降低。我弄了一个简单的测试:
closure_test.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 25 26 27 | var with_closure = (function() { var i = 0; var looper = function(loop) { var j; var start = (new Date()).getTime(); for (j = 0; j < loop; ++j) { i = i + 1; } var end = (new Date()).getTime(); return end - start; } return looper; })(); var without_closure = function (loop) { var i = 0; var j; var start = (new Date()).getTime(); for (j = 0; j < loop; ++j) { i = i + 1; } var end = (new Date()).getTime(); return end - start; } |
在 FF6 中,loop = 50000000的情况下:
With closure: 506 ms
Without closure: 269 ms
在 Chrome 15中,loop = 50000000的情况下:
With closure: 161 ms
Without closure: 78 ms
原因……估计大致是,内层函数需要先搜寻本函数内是否存在关于此变量的约束,如果不存在再去搜寻 closure 中的约束,这样的话自然就会使得变量的搜寻时间变长了。我没研究过 ECMAScript 的标准……不知道上面的猜测是否正确,就算正确也不知道是否是因为实现方式而导致的。
测试结果: closure 确实会带来性能的降低。虽然 closure 并非只是用来实现 javascript 中的私有,但如果仅仅是把 closure 用于实现私有,可能就有点滥用了。
javascript里面的this
昨天的思考中遇到了this的问题。ECMA-262中,this是在执行上下文(Execution Contexts)中被首次提及的,所以或许我应该先了解一下什么是执行上下文。
Excution Contexts(我简称EC吧),当程序的控制流进入一段ECMAScript可执行代码时,就会进入一个EC。活动EC在逻辑上会形成一个栈,栈顶EC就是当前运行的EC。
ECMAScript可执行代码?
ECMA-262中定义了三类这样的可执行代码:
- 全局代码:用C的概念类比的话,就是main函数里面的所有语句。javascript程序的源代码,除了函数体以外,都可以看成是全局代码。
我好像一直在混淆javascript和ECMAScript的概念了?其实ECMAScript可以认为是javascript和jscript的泛化,是一个标准化的规范。虽然我不知道javascript是不是完全兼容于ECMA的规范,不过ECMA中定义的大部分概念,对javascript应该都是适用的。
- eval代码:eval,但凡解释性语言基本都有的东西。当然apply也是。eval/apply可谓亲密无间的伙伴,不过在这里它不是主角。eval代码就是应用到内建的eval函数的那些字符串。性质类似于全局代码。
- 函数代码:全局代码里面并没有包括函数体的源代码,函数体的源代码属于函数代码的一部分。当然,函数体的源代码中如果包含嵌套的函数体,嵌套的那部分则是属于另一段的函数代码的。上一篇文章里面说过函数也可以通过new Function来创建,所以new Function的最后一个参数,也就是函数体,也会被看作是一段函数代码。
我猜想EC的并非以一种对象实现的。ECMA-262里面从头到尾都没有提起过所谓execution context object,甚至连context object的概念也没有看见。会有不少对象和引用附加在一个EC上,大概可以把EC理解成一个小型集合吧。
那么this到底和EC有什么关系?
this是EC上附加的一个……引用。this的值与调用者(主要对函数而言)和被执行的代码有关,并且值在进入EC的时候就已经定下来了。this是不可修改的。
this是跟可执行代码类型有关的:
- 对于全局代码,this引用到全局对象。
什么是全局对象?
全局对象是javascript的一类原生对象Global Object,在进入EC之前就已经被创建。它有几个特点:
- 不能通过new constructor构造。
- 不能用作函数调用。
- Global Object的类型以及prototype是跟具体的javascript的实现细节相关的。
javascript提供的内建值(NaN,Infinity和undefined)、内建函数(eval,parseInt等)以及其他原生对象的构造函数(Object,Function等)都是Global Object的提供的属性。可以向Global Object中添加各种其他的属性。
Global Object处于作用域链的最高层。至于什么是作用域链……再说吧,现在已经离题很远了。
根据这些,现在可以猜想到的一点就是,在javascript里面这样的调用:
eval("alert('hello');");
其实是等价于下面这样的伪代码:
global_object.eval("alert('hello');");
回到this。因为对于全局代码,this引用到了全局对象,所以在全局代码里面使用this,跟没使用效果是一样的orz:
this.a="hello";
this.alert(a===this.a); - eval代码
当进入的是eval代码的EC时,上一个EC,被称作调用上下文。eval代码里面的this,与调用上下文里面的this是一样的。如果没有调用上下文,那么eval代码会做与全局代码一样的处理。
function test() {
eval("this.a = 'hello';");
alert(a);
}
test();不过实际上,这里的this用了跟没有用也是一样的。
- 函数代码
this的值由调用者提供。当调用者不是对象时,this将引用到全局对象上。
看到这里,虽然还没弄清楚到底调用者怎样提供this的值,不过可以猜想,如果调用者是对象的话,那么this将会引用到调用者自身。这就可以解释为什么new constructor的时候,this是指向实例化的对象了。如果作为普通函数调用,按照之前对全局代码得出的结论,应该跟global_object.a_function的调用是一样的。这样想的话,调用者就是全局对象了。
那样的话,总算明白了普通函数里面的this有什么意思了……
function test_class () {
this.v1 = '1';
}/* new constructor方式调用,function code的
调用者为新实例化的对象,this引用到这个对象上。*/
alert((new test_class()).v1);alert(v1); // 调用失败,v1未定义
test_class(); // 普通函数调用,this为全局对象
alert(v1); // 经过上面的调用,全局对象里面有了v1的定义
不过让ECMA-262里面提到的一点我不太明白:eval代码可能会没有调用上下文。那会是什么样的情况呢?是指eval语句是第一句的情况吗?还是说只有eval一句的情况呢?
说到这里仍然是有很多不明不白,唯一清晰了的就是this在普通函数里面有什么含意……嘛既然标题是关于this的,我也不去想先了。留给后一篇post吧。
Javascript中prototype的菜鸟级思考
进入主题之前不得不承认,一直以来我都把javascript看成是java的缩水版,同一个公司出品。我一直不太喜欢java那笨重的身体,于是,javascript就也被我放进了不喜欢的PL的队列里面,javascript在我脑子里面大概都有4、5年没有更新过了。直到上个月着手为xtheukn做些小修小改的时候开始,我对javascript的理解才有了质的变化。
虽然挺想认真地再学一下这个有趣又强大的小玩意,不过时间是最大的问题啊……所以下面写的东西,可能会很肤浅也很多错误……
今天特意认真的看了看有关javascript的prototype部分,算是把之前不太懂的弄懂了一点了吧。
不过,大概还是停留在很低的水平上……
或许是我搜索能力问题,也或许是我脑子不太灵活的问题,自己在网上搜了不少关于prototype的讲解,但看了又看还是不太懂。后来找到了ECMA-262,仔细读了读,总算明白了什么是prototype了。
按照ECMA-262的定义,prototype是一个对象。利用prototype,可以在ECMAScript(javascript和jscript的泛化)中实现继承,它被构造函数(constructor)所持用的,可以通过constructor.prototype引用。
那么什么是构造函数?
构造函数其实跟普通的函数,单从函数的定义上面来说,可以说是完全一样的。唯一区分构造函数和普通函数的,大概就只有new操作符了。当new操作符作用在一个函数的时候,将会产生一个对象,然后在这个对象上调用这个函数,初始化这个对象里面的成员。
function BaseClass () {
this.var1 = 'Base';
}
var baseobj = new BaseClass();
然后baseobj.var1就会包含值'Base'了。javascript里面的所有对象,都可以通过new constructor(...)来实例化。
假如没有了new,那么对BaseClass的调用就是普通的函数调用,这时的baseobj就不是一个BaseClass的对象了,而仅仅是BaseClass这个函数的返回值。
当然,这样的话,对this的理解就不能停留在C++的层面上了。有空再慢慢看看这个this的含义吧。
在函数定义好以后它就持有一个prototype对象的引用,也就是constructor.prototype。这很正常,因为函数在javascript也是一个对象,到prototype对象的引用其实就是这个函数对象上的一个成员变量。constructor代表一个具体的构造函数名,比如说在上面的代码片段,constructor对应了BaseClass,引用BaseClass的prototype对象应通过BaseClass.prototype来引用。
函数对象
把函数也作为一种对象在解释语言里面很常见。在javascript里面,函数对象(Function Object)是一种原生的对象,也可以通过new constructor来实例化:
var func = new Function("v1", "v2", "return v1+v2;");
上面的语句定义了一个函数func,等价于:
function func(v1, v2) {
return v1+v2;
}
而当一个对象被成功实例化以后,它就会隐式的引用到构造函数持有的prototype对象上面。一个对象总会有一个隐式引用的prototype对象。
如果BaseClass的对象baseobj要引用一个变量var1,那么首先当然会去查找当前的baseobj对象上面有没有这个var1变量,如果有,取其值;如果没有,则通过这个隐式的引用回溯到BaseClass的prototype对象上面继续查找。很容易可以想到,如果在这个prototype对象上找不到var1,那么,因为这个prototype对象也是一个对象,它也有隐式引用的prototype对象,所以将会继续回溯到BaseClass.prototype对象所引用的prototype对象上继续进行查找,直到找到为止,否则就返回undefined。这种prototype之间的引用也称为prototype链。
需要记住的是prototype只能通过constructor.prototype来进行显式的引用,被构造函数实例化的对象只有隐式的引用,没办法显式的访问,所以类似BaseClass.prototype.prototype这样的调用是会失败的,除非BaseClass.prototype引用到的是一个函数对象。不过似乎构筑这种情况没什么实用性可言。
通过prototype链,javascript就可以实现继承了:
1 function BaseClass() {
2 this.var1 = 'Base';
3 }
4 function DerivedClass() {
5 this.var2 = 'Derived';
6 }
7 DerivedClass.prototype = new BaseClass();
8 var derobj = new DerivedClass();
这里关键的一句是第7行的语句。它把DerivedClass.prototype引用到一个BaseClass的实例(方便起见,记为baseobj)上面了。于是当第8行语句实例化一个DerivedClass的对象derobj时,derobj就会有一个对baseobj产生隐式引用,当在derobj访问一个不存在的变量时,就会通过隐式引用回溯到baseobj里查找。继承就是通过这样的方式实现了。
当然,因为baseobj是被DerivedClass所持有的,所以所有DerivedClass实例化的实例,都会隐式引用到同一个对象上面,也就是说,当baseobj有所变动,DerivedClass的所有实例都会受到影响。
需要注意的是哪些对象之间存在引用,是何种引用。假设存在构造函数CF,CF持有的prototype对象CFp,以及CF实例化的两个对象CF1和CF2:
- CF1和CF2是CF实例化而得到的,所以CF1和CF2对CFp存在隐式的引用。
- CFp是一个对象,假设构造它的构造函数是CFpCF,被实例化时CFpCF持有的prototype对象是CFpCFp,那么CFp对CFpCFp就存在隐式的引用。
- CF是一个函数,函数也是对象,也是通过某个构造函数实例化而得的,假设为CFCF,那么CF对CFCF也存在隐式引用。
- 最后,CF是一个构造函数,所以它有一个对CFp的显式引用。
从上面可见,CF1、CF2和CF之间是没有引用被引用关系的。也就是说,CF1、CF2对CFp的引用,是不受CF干预的。也就是说当CF.prototype改变了以后,CF1和CF2对CFp的引用是不会变的。
会产生循环引用吗?
我曾经考虑过这样的语句序列:
function func1() {}
function func2() {}
func1.prototype = new func2();
func2.prototype = new func1();
var v1 = new func2();
alert(v1.vv1);
看上去好像当v1检索vv1变量的时候:v1中不存在vv1变量,到func2.prototype找-->到func1.prototype找-->到func2.prototype找-->...,似乎会变成一个循环。
定义一个函数(也就是创建一个函数对象)的过程中有以下几个重要的动作:
- 创建一个原生的Function对象F
- 创建一个原生的Object对象O
- F.prototype <- O
- 返回F
实际的过程比上面列出复杂得多,不过已经足可见一个构造函数持有的prototype对象,是在定义的时候就已经引用到一个Object对象了。
然后看看func1.prototype = new func2()时发生了什么:
- 一个func2的实例F2被创建,根据当前func2的prototype是引用到一个Object对象O上面的,所以F2也是引用到这个O上面。
- func1.prototype <- F2
前面提到,constructor和实例化的对象之间是没有引用关系的, constructor.prototype的改变并不会影响已经实例化的对象所引用的对象,所以,F2此时仍然指向Object对象O上。 然后当下一句被执行的时候,func1.prototype已经被改变而指向F2,所以new func1所创建的对象F1会指向F2,而func2.prototype指向func1.prototype已经改变以后再实例化的F1。 然后v1被实例化,指向func2.prototype即F1。
然后看看实际的回溯路径: v1-->F1-->F2-->O
并没有构成循环。
想到这里算了。好像写了不少没用的东西了……不过就体谅一下我这等菜鸟吧。如果你发现有错的,请务必告诉我。
然后又一天被我这样花掉了,sigh……