【记】几个我并没有想好的问题
开门见山,记录三个问题,恰逢有人问我,仔细想想,发现原来真的没有想好,故重新思考后稍作记录。
【1.关于作用域和作用域链】
作用域的特性:自封闭,词法分区,调用对象冒泡,预声明。
我记得我曾在以前的一篇文章中提到了变量作用域一个预声明(hoisting)的特性,其中有个经典案例,也是好多公司用做面试的考题。
var a = 'global';
void function () {
alert(a);
var a = 'local';
}()
弹出的结果是undefined,即变量声明期和赋值期不宜样,这一点很多人都知道,我也不细说了。
全局变量是全局对象的一个属性,那么在上面的viod function 里面局部变量a是不是也应该是某个对象的属性呢?是滴,这里会有一个调用对象的概念。
每个javascript在他的执行环境中都会产生一个调用对象[activing Object],在此执行操作的变量可以看作此调用对象的一个属性。这么一来,正好契合了javascript语言本身基于对象的说法。
接下来,我们用调用对象属性查找方式来解释上面的预声明的情况时,应该是这样的。
上面几行代码,一行一行说:
var a = 'global'; 声明一个全局变量a,其值为'global'。其实细分来看,这句话其实有两个含义,var a 和 a = 'global';分别对应着在全局对象中创建一个堆,名为a, 然后第二个操作是把堆a对应的栈值置为’global‘。
为什么要分成两个操作来看呢,先不忙说,接着往下看...
第二行开始,一个没有返回值的function创建并调用。于是,一个新的调用对象产生了,这里有个词法划分的问题,我们所说的调用对象所包含的内容实际上和当前作用域所能访问到的内容是有区别的。这一点会在下面“调用对象冒泡”这个说法中说到。
刚才提到一个词法划分的问题,我说创建一个funcition,然后就有一个新的作用域划分出来,是的,作用域是通过词法来划分的,这里的词法指代的通常是function的定义。
那么再说到刚才的代码上,第二行开始到后面的function中,创建一个调用对象,这个调用对象中,又新建了一个属性a,注意这里是新建一个属性。但是按照刚才说的,声名和赋值分成两个操作,而且是发生在不同时期的。于是可以得出下面的代码:
var a;a = 'global';
void function () {var a;
alert(a);
a = 'local';
}()
如果看到这样的代码,那么答案就很明显了,但是有一点,即便说可以分拆成这个样子,那为何在那个void function 里面,var a;要在alert之前呢?
在js的运行机制里,声明期一定是在执行期之前的,所以在上面void function这个空函数的三句话里,还可以按照声明期和执行期分成两个阶段:
var a; 是属于声明期的。
而 alert(a);和 a='local',一个是调用,一个是赋值,都是属于执行期的,所以必然在声明期的后面。于是最终有了上面的结果。
然后呢...这仅仅讲到了调用对象的生成,再接着往下执行的时候,就会涉及到作用域链了,所谓的[[scope chain]],前一段讲讲什么‘变量预声明’,那还是‘作用域’的内容,那么所谓作用域链,这个【链】字体现在什么地方呢?
于是,又提到刚才提过的一个词,“调用对象冒泡”。听起来有些奇怪。但是可以用它来解释作用域链的工作原理。以一段代码为例:
var x = 1;
function f () {
var y = 2;
function g () {
var z = 3;
}
}
这段代码,牵涉到的作用域或者说调用对象有三个,分别是全局作用域,函数f划分的作用域(暂叫它为f),函数g划分的作用域(暂定为g),也涉及到三个对象,全局对象,调用对象f,调用对象g(暂且这么叫他们)。
其实,从上面代码的嵌套关系基本也可以看出这三个对象是有层级关系的。
全局对象在最外面,‘包含’着调用对象f,然后调用对象f好像又包含着调用对象g。(虽然说“包含”一词不准确,但是从形式上可以这么来看)。我们假定全局对象,调用对象f,调用对象g这三个对象间的层级关系後, 可以来说说为什么作用域g里面的内容能够访问f里面的内容,f能够方位全局对象中的内容,但是反过来就不行...
其实从刚才的描述中来看,所谓的“链式”就已经有所体现了,而且还是单向的链。至于为什么会是这样的结果。这里会涉及到这些独立的作用域是怎么形成链的具体情形。
在每个作用域的执行环境中,链的形成都是由内向外的。这也就是这条链单向的原因,回到上面说的三个作用域在执行期发生的事。可以这么来说:
每个作用域都是它所在的执行环境中的一部分,我这里说的执行环境其实就可以理解成作用域链。
于是,全局作用域,作用域f,作用域g,就组成了一条链。。。如果要转换成虚拟的调用对象的形式的话。可以画出类似下面的东东:
={x, f, window}=={y, g, f}=={z, g}=
我用大括号表示当前作用域划分,(但并不表示当前作用域范围),如果按照我上面写的这个方向的话,作用域这个单向链的可访问方向是从右向左的。
亦即从右向左的看,凡是在当前划分左边的,就都是当前划分能访问的范围(包括它自己)。比如,最右边划分g的访问范围就最大,整条链都可以。中间的划分区域可以访问的就是他自己和他左边的范围。 最左边的全局作用域里面的内容可访问区域就只有它本身,因为它已是最顶级的,左边没有任何划分区域了。
有了这些概念後,关于作用域内变量查找也就是类似的方式。比如一个作用域里要使用一个变量a的值,首先会查找自身所划分区域里有没有它的值,如果没有,就向左回溯,一直回溯到顶级的全局对象,如果还没有,那么就是undefined。
【2. 基于OO的和基于Functional的context有什么区别?】
首先说context,执行上下文对象。这个大家应该都清楚。和平时常用的关键字this有密切的联系。每段js在它的执行环境中都有其执行上下文对象。this就是它的指代。和调用对象不同的是,上下文对象通常是指调用当前作用域(函数方法)的对象,是有明确指定的。而调用对象更像是一个虚拟的静态存储,帮助维护作用域而已。
我们再回想很多公司经常会出些关于this指向的一些问题来考面试者。其实也就是想考context的判定。确实,上下文对象的判定在某些时候确实有一点难度,因为它不像作用域那样,是静态划分的,通过词法就能分离当前作用域。而context正如它的名字一样,是和语境,也就是和它的上下文密切相关的,同样一段代码,放不同的上下文里,context也是不同的。
一个简单的例子:
function test () {
console.log('context is '+this);
}
test()
假如是如下的情况:
function test () {
console.log('context is '+this);
}
new test()
我们可以用一小段代码表示一下new运算符的机制:
function _new (fn, args) {
var o = new Object();
if (fn && typeof fn == 'function') {
o.__proto__ = fn.prototype;
fn.apply(o, args);
return o;
}
}
new 运算符其实是新建了一个Object,然后分配了这个Object的__proto__属性,即原型,这是很重要的一步...最后返回出拥有当前构造器的prototype为属性的一个对象。即我们的实例。
言归正传,回到最开始的问题,基于OO和Functional的context有什么不同?
先说OO,接上面的new运算符,在OO的编程模式中,有个很重要的地方就是我们会写构造器,然后使用实例来干一些事,所以通常情况下,context都是在各种实例和window间徘徊。很好的帮助我们去获取不同实例的独立的属性和方法。然后必要的时候有call和apply来帮助我们转移本来的context。
关于Functional的context,其实我觉得说Functional的context没什么意义。。。为什么这么说呢,请看下面的例子: 比如一个典型的函数式的map函数:
map = function(fn, sequence, object) {
var len = sequence.length,
result = new Array(len);
for (var i = 0; i < len; i++)
result[i] = fn.apply(object, [sequence[i], i]);
return result;
}map(function (x) {return x+1}, [2, 3]) // [3, 4]
其实,context的概念在这里基本上完全无用武之地,函数式本身就是以函数为核心,以函数为第一型的编码方式,而context本身是基于对象的概念,要说Functional里面哪里有用context的概念的话,我觉得就只能剑走偏锋了,可能某些时候会写
Function.prototype.*** = function () {*** this ***}
Array.prototype.*** = function () {*** this ***}
...
可是,这完全不是我们想要的概念,当时我没想清楚,如果现在回头再说,我会觉得Functional谈context完全没有什么意义。
【3. 如果抽象出Class的概念,在原型继承里,多级继承怎么处理?】
说实话,这个确实是我之前没有认真想过的问题,如果抽像出独立的inherit方法,必然面临两个核心的问题:
1.如果基于prototype继承,如何在继承的时候,保证父类的构造函数不被执行?
2.既然是多级继承,怎么保存原型链,即继承所有继承链上的方法?
第一个问题,我觉得还好,现在主流的框架都有类似的解决方案,要保证被继承的父类不被执行,但又必须取到父类实例的方法,貌似只有一个办法,先把要继承的父类临时copy一份,然后用copy的那份来做该做的事情...
也就是需要一个空函数来顶替父类,将空函数的prototype指为父类prototype,然后使用这个临时的copy来获取你想要的东西。
YUI和EXT一直是这样做的,可以参考Kevin Lindsey发的一篇文章http://kevlindev.com/tutorials/javascript/inheritance/index.htm
我这里贴一个YUI比较老的一个版本:
extend: function(subc, superc, overrides) {
if (!superc||!subc) {
throw new Error("extend failed, please check that " +
"all dependencies are included.");
}
var F = function() {};
F.prototype=superc.prototype;
subc.prototype=new F();
subc.prototype.constructor=subc;
subc.superclass=superc.prototype;
if (superc.prototype.constructor == Object.prototype.constructor) {
superc.prototype.constructor=superc;
}
if (overrides) {
for (var i in overrides) {
if (L.hasOwnProperty(overrides, i)) {
subc.prototype[i]=overrides[i];
}
}
L._IEEnumFix(subc.prototype, overrides);
}
}
他们最开始就是用一个空函数的方式来解决不执行父类而获取到父类实例方法的问题。
但是这版本的YUI继承仍旧有个问题,就是不能保存原型链,超过一级继承的时候同样会出现问题。不过在后来的版本中也都陆续修复了。
解决方案还是利用闭包,然后用特定的变量将要多级继承的方法存储内存中。
John Resig的代码:
(function(){
var initializing = false, fnTest = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/;
// The base Class implementation (does nothing)
this.Class = function(){};
// Create a new Class that inherits from this class
Class.extend = function(prop) {
var _super = this.prototype;
// Instantiate a base class (but only create the instance,
// don't run the init constructor)
initializing = true;
var prototype = new this();
initializing = false;
// Copy the properties over onto the new prototype
for (var name in prop) {
// Check if we're overwriting an existing function
prototype[name] = typeof prop[name] == "function" &&
typeof _super[name] == "function" && fnTest.test(prop[name]) ?
(function(name, fn){
return function() {
var tmp = this._super;
// Add a new ._super() method that is the same method
// but on the super-class
this._super = _super[name];
// The method only need to be bound temporarily, so we
// remove it when we're done executing
var ret = fn.apply(this, arguments);
this._super = tmp;
return ret;
};
})(name, prop[name]) :
prop[name];
}
// The dummy class constructor
function Class() {
// All construction is actually done in the init method
if ( !initializing && this.init )
this.init.apply(this, arguments);
}
// Populate our constructed prototype object
Class.prototype = prototype;
// Enforce the constructor to be what we expect
Class.constructor = Class;
// And make this class extendable
Class.extend = arguments.callee;
return Class;
};
})();
由于this._super驻于内存,所以通过它是可以一级一级继承下去的。保持了父类的函数链。
恩。到这里,三个问题差不多了,大概为自己梳理一下思路,最后本来想在这个周末利用canvas做个动态的footer放自己blog的,可惜感觉有点卡,没放上去,就当个半成品放这里吧: