深入理解JavaScript闭包(1)

这边文章主要是Javascript Closures这篇文章的学习笔记,可能包含一定的原文翻译,以及一些自己的理解,希望通过这些学习能够更加深入地理解JavaScript中闭包的概念。

下面是The Execution Context一节的翻译与理解。

执行上下文 (The Execution Context)

执行上下文是ECMAScript规范(ECMA 262 3rd edition)中定义的有关ECMAScript实现的相关行为要求。虽然规范没有具体规定执行上下文的实现,但是根据规范中对其所定义的数据结构(包含一些列相关是属性),因此执行上下文可以理解为(甚至按照)对象的方式来实现,尽管其包含的属性不是公共的。

所有的JavaScript代码都是在执行上下文中所执行的。全局代码(包含已执行的行内代码,JS文件,或者内潜入HTML的JS代码)都是在全局执行上下文中执行的,并且对于每一个函数(包含构造函数)都有一个与之相关联的执行上下文。使用eval函数执行的代码也是包含一个独特的执行上下文,但是由于其在JavaScript中的特殊性,这里就不再多考虑了。有关执行上下文的具体细则规范可以在ECMA 262 (3rd edition) 10.2节中找到。

当一个JavaScript函数被调用的时候,它就进入到一个执行上下文中。如果其中又有其他函数被调用(或者自身的递归)都会创建一个新的执行上下文,然后函数调用进入到那个执行上下文中,直到那个函数执行结束(译者注:这不就类似盗梦空间中的进入一层层梦境,直到该层梦境的时间结束,由此看来盗梦空间真是值得广大程序员好好研究的佳片啊=,=||)。因此,由于这种现象就在代码执行中形成了一个执行上下文堆栈。

当一个执行上下文被创建的时候会依次经历以下几个阶段。首先,在函数的执行上下文中创建一个Activation(激活)对象(貌似犀牛书中就是这样翻译的~)。Activation对象实际是规范中又一机制。由于它拥有一些可访问的具名属性,因此可以将它作为一个对象来看待,但比较特殊的是,它没有原型对象(至少没有一个定义的原型对象),同时Activation对象也不能在代码中直接引用。

Activation对象创建完毕后,下一步就是为函数调用创建arguments对象,众所周知,arguments对象是一个类数组的对象,使用整数对其成员索引,且其排列顺序与函数调用传递的参数顺序一致。同时arguments对象还拥有lengthcallee两个成员(译者注:实际还应该有一个caller成员,不过这个成员已经建议弃用了),不过这里与主题无关就不做详细讨论了。这时候会在Activation对象上创建一个名为"arguments"的属性,且指向arguments对象。

接下来,执行上下文需要给作用域赋值。这里的作用域就是包含一些列的对象,也就是我们常说的作用域链,注意对“链”的理解。每一个函数对象在内部都拥有一个[[scope]]的属性(稍候会详细介绍),它也包含一个链式的对象。那么执行上下文的作用域主要包括Activation对象,且其处于作用域链的最顶端,之后就是函数对象对应的[[scope]]对象。

下一步称为变量实例化(variable instantiation),这时候使用到一个对象,在ECMA 262中称为Variable对象。然后Activation对象实际就是当作Variable对象使用(这里需要注意的是,其实两者指的是同一个对象)。在这个阶段里,会为函数的每一个形参在Variable对象上创建一个具名属性,如果函数调用传入的参数与形参一致的话,那么就会将参数的值逐一赋值给Variable对象上的那些属性(否则的话赋值为undefined)。之后对于调用函数的内部函数声明,同样首先会给这些函数创建函数对象,之后以函数名称为Variable对象的属性名,然后添加对应的函数对象引用。变量实例化的最后一个阶段就是处理调用函数的具备变量。同样的,将所有调用函数内的具备变量声明添加到Variable对象中。(译者注:此时的Variable对象就完整地包含了形参,内嵌函数以及局部变量)

这里需要注意的是在变量实例化阶段中,为Variable对象添加局部变量时,所赋值的初值均为undefined(译者注:因为我们知道JS引擎会将以var声明的具备变量提前解析,这就是刚刚所提到的变量实例化阶段所做的事儿,因此提前之后它们的初值都是undefined),而真正的局部变量实例化(赋值)直到在执行代码体的表达式时才会完成。

事实上,带有arguments的Activation对象与带有函数局部变量的Variable对象就是同一个对象,因此我们也可以把arguments标识符当作一个函数局部对象。

最后一个步骤就是对this关键字赋值。如果this关键字的值为一个对象的话,那么以this关键字为前缀的属性访问均是指向那个对象;但如果this关键字赋值为null的话,那么this关键字就指向全局对象。

对于全局执行上下文来说,处理的方式会有些许差异,这主要是因为其没有参数,因此就不需要定义Activation对象。同样的,全局执行上下文需要作用域,且它的作用域只包含一个对象——全局对象;全局执行上下文也需要经历变量实例化,它内部的函数自然就成为了最顶层的函数声明,包含了大量JavaScript代码;全局对象被当作Variable对象使用,因此其中的变量声明、函数声明自然就成为了全局对象的成员(译者注:这也就是为什么我们建议在全局代码中用一个匿名函数包裹内部代码,防止全局对象的污染)。

同时,全局执行上下文中的this对象指向的是全局对象。

最后以一张图片来结尾。

execution_context

- EOF -

KISSY深入研究(3)——loader.js

由于国庆大假的原因,加之最近工作上时间比较零散,没有相对较为完整的时间学习,因此KISSY源码分析停滞了很久,今天要分析的是在KISSY 1.1.5中才正式引入的loader概念。

Loader 背景

首先需要了解一些JavaScript Loader的相关知识,Loader这个概念主要是出现于页面加载时对于JavaScript代码的加载管理、依赖关系处理等。如果本身页面较为简单,可能Loader就并不适用于此。豆瓣的这篇文章《攻城利器之微型框架 - Do》正是介绍了它们的微型框架对于页面JavaScript代码的加载管理,这个框架的主要用途并不像普通JavaScript框架,它的主要用途是页面中JavaScript代码的管理者和组织者,具体介绍可以详细看原文已经整个框架代码。同时还有这篇文章《KISSY loader 的设计 》,是KISSY loader机制的开发者之一@拔赤介绍的相关设计理念,强烈推荐仔细阅读。

Loader 使用

有了对于Loader的基本概念以及相关的设计理念后,我们先从比较直观的方面介绍一下Loader。有关Loader的具体API文档可以猛击这里

我们从一个实例具体理解一下Loader的使用吧。在淘宝网首页中存在如下一段JavaScript代码:

<script src="http://a.tbcdn.cn/??s/kissy/1.1.3/kissy-min.js,p/header/header-v8-min.js?t=20100908.js"></script> 
<script> 
KISSY.app('FP');
FP.add({
    'direct-promo': {
        fullpath: 'http://a.tbcdn.cn/p/fp/2010c/js/fp-direct-promo-min.js'
    },
    'fp-mods': {
        fullpath: 'http://a.tbcdn.cn/??s/kissy/1.1.3/suggest/suggest-pkg-min.js,s/kissy/1.1.3/datalazyload/datalazyload-pkg-min.js,s/kissy/1.1.3/switchable/switchable-pkg-min.js,s/kissy/1.1.3/flash/flash-pkg-min.js,p/fp/2010c/js/fp-alimama-ecpm-min.js,p/fp/2010c/js/fp-alimama-ecpm-min.js,p/fp/2010c/js/fp-hubble-monitor-min.js,p/fp/2010c/js/fp-p4p-min.js,p/fp/2010c/js/fp-init-min.js?t=20100915.js'
    }
});
FP.use('direct-promo', function(F) {
    F.DirectPromo.request([48, 49, 75, 51, 52, 53, 68, 73, 74], 1);
});
</script> 

可以很清楚地看出这里使用了Loader技术。根据前面学习到的知识,在第二段JavaScript代码块中第一行首先是创建了一个FP的应用。之后我们往FP这个应用上添加了两个模块'direct-promo'以及'fp-mods',而且这两个模块的添加方式都是使用文件引用,当然我们也可以直接将代码写在这里,同时还可以加上一些配置,具体写法还是参照API文档的说明。接下来就开始在FP这个应用是使用我们添加的模块做事儿了,这里指的就是use这个方法。

那么这种写法(用法)有什么好处呢?与最原始的方式又有什么不同呢?我想通过背景里面的两篇文章大家一定都能有一些概念了,下面我们看下这样加载方式的Timeline

taobao-index-loader

是的,我们只需预先把种子脚本载入(种子脚本相对较小,但是因为首页具体情况,这里没有使用seed.js种子脚本),然后其后再按需add模块代码,use模块,就能保证我们这里的业务脚本无阻加载,这样不仅仅使代码管理起来更加集中、规范,同时也能大大提高页面性能。有时我们没办法做到不加载,但是通过延时加载,合理分配任务,让繁重工作分时完成,同样能够达到提高页面性能、用户响应的目的,增强网站可访问性。

Loader 概述

既然已经对Loader有了大致了解,并且通过一个例子也算是比较直观地感受了Loader带来的好处,那么我们下面就深入到loader.js源码中具体研究研究这种设计是如何实现的。

我们用一段典型代码来说明整个Loader 的实现过程:

// Add mod1 module for KISSY
KISSY.add('mod1', {
    fullpath: 'http://ghsky.com/labs/foo.js'
});

// Add mod2 module for KISSY
KISSY.add('mod2', function() {
    // some code here
}, {
    requires: ['mod1']
});

// Use the mod2 to do something
KISSY.use('mod2', function() {
    // callback here
});

对于模块主要分为add以及use两个阶段,add阶段主要是添加模块相关配置信息到模块宿主环境中保存,以下是一张典型的宿主环境的模块存储结构:

KISSY.Env

同时添加模块的方式主要有两种,一种是直接以代码形式添加(代码模块),例如上面示例代码的'mod2',另外一种就是以文件资源形式添加(资源模块),例如上面示例代码的'mod1'。由此我们引出模块的5种主要状态,0(初始状态),LOADING(1),LOADED(2),ERROR(3),ATTACHED(4)。

0和LOADING状态只有资源模块会存在,而所有的代码的初始状态均为LOADED。处于LOADED状态的模块均表示外部资源已经读取或者不需要外部资源,此时与ATTACHED状态的唯一差别就是模块自身的代码栈未执行,这里提到模块自身的代码栈,主要指的还是代码模块,当然有些资源模块添加之后同样还可以为其添加一些代码,这样也就成为他们自身的代码栈了。只有当每个模块的依赖模块都加载完毕才会执行其自身代码栈,当然这些工作都完成后,模块也就是ATTACHED(可用)了!

可能你还注意到宿主环境里面还有一个_loadQueue的对象,其作用主要是用于保存资源文件的加载队列,后面会详细介绍。下面我们就大致看一下Loader模块的具体组成:

loader

暴露的公共API就是三个,getScript, add, use,三个方法的具体说明可以猛击这里查看。其他的方法均为内部使用,因此才去了私有方法的命名方式,这里先说一下,__mixMod & __mixMods 两个方法设计到了一些全局loader的高级的特性,因此这里就不具体研究。下面我们主要从add以及use两个方法入手来具体了解loader的实现机制。

Loader add()

正如上面示例所示,add方法仅为宿主环境添加所需的执行代码(或者资源文件)以及模块的配置信息,添加之后的模块只有在use时才能体现出其作用,因此要注意理解add与use的区别。首先大致了解一下add方法的处理流程:

Loader.add

具体解释一下上述的流程,add首先根据传递的参数进行规格化处理,然后判断添加的模块类型(代码模块或是资源模块)。若是资源模块,则很简单,目前只需简单地将模块的相关配置信息规格化后加入到宿主模块环境中;若是代码模块,则首先也要进行相关配置的规格化处理,之后需要判断模块是否有依赖模块,若有的话则检查各个依赖模块是否均已ATTACHED,若是的话就可以执行自身的代码栈,否则需要将代码压入代码栈等待后续执行,至此整个add过程就已经结束了。这里主要涉及到了__isAttached以及__attachMod两个方法。

__isAttached方法顾名思义,就是检查模块是否已经可用了(ATTACHED),其判断依据便是每个模块均有的状态标记,之前的“概述”里面已经提到了模块的状态。

__attachMod方法则是attach模块,也就是执行模块自身代码栈的方法。该方法的执行条件是依赖模块均已可用,这时候便可以执行模块自身的代码栈,顺利执行之后即可将模块的状态设置为ATTACHED(可用)。因此__attachMod方法可以保证代码模块是在依赖模块加载后才执行,因此可以保证代码执行安全。

正如示例所示,我们以外部资源方式添加了'mod1',以代码形式添加了'mod2',此时add方法对于'mod1',只简单规格化配置后就将其添加至宿主模块环境中,只是目前其状态字段为undefined,其所需的外部资源也未读取。对于'mod2',以代码形式添加,但是由于其依赖模块'mod1'在__isAttached检测中未通过,也就是'mod2'的代码还暂时无法执行,故只能将其压入其代码栈等待后续执行。

Loader use()

use方法相对add就要复杂一些,下面先大致看下基本流程:

Loader.use

这么一看大致流程估计也够晕的,那么下面就顺序解释一下。首先声明一下,use的参数中可以传递一个global的参数,但由于其使用较少,笔者基本没有使用经验,所以这里就避而不谈了。use方法支持同时加载多个模块,其方式是以“,”分隔各模块名。方法首先将需要use的模块建立成数组形式,然后调用__isAttached方法检查是否均已可用,若都加载完毕可用,那么就直接执行回调函数退出即可。对于只要存在一个不可用的模块,则比较对其加载,因此需要遍历这个模块数组,排除那些已经可用的模块,只用处理还未可用的模块,同时use方法支持按序执行,也就是如果需要use多个模块,且需要按序加载他们,这里use方法便会利用一个小技巧,将当前模块之前的一个模块(也就是保证按序加载)放入到当前模块的依赖中,这样由于依赖模块均在当前模块前加载可用,因此变相实现了按序加载,对于这种方式会破坏原始的依赖模块数组因此需要做一个备份,当前模块加载可用后再做一次还原即可。同时按序加载中还需要防止循环依赖的情况出现。

目前已经将当前模块的配置处理完毕,包括可能出现的按序加载处理,之后便需要调用__attach方法加载当前模块。这个方法也是利用回调机制,执行完毕后回调处理函数。我们这里的回调处理函数实际就相当于要执行use方法的回调,只是这里需要保证的是整个use列表的模块都已经加载完毕之后才可执行,因此这里需要简单的判断。进入__attach方法,如果存在依赖模块则需要递归地先对依赖模块加载,之后如果当前模块为资源模块的话便需要处理资源加载的问题,首先需要建立资源的绝对路径(处理模块配置中使用的相对路径,同时这里如果存在模块的样式资源文件引用,也需要处理其加载路径),完成后在模块上形成一个'fullpath'(可能还存在'csspath')这样的属性指定加载资源的绝对路径。资源文件路径都准备好了之后就需要进入__load方法,读取模块资源。

在__load方法中,还记得我们最开始说到的在KISSY这个宿主环境中有一个_loadQueue的对象么,这里我们用其来缓存每一个需要加载的外部资源。首先获取_loadQueue中当前资源的信息,然后初始化当前模块的状态(注意对于那些代码模块,在add方法中我们已经初始化过其模块状态为LOADED,这里便不会覆盖),同样对于已经存在于_loadQueue中的资源同样,判断其是正在加载或者已经加载完毕来初始化当前模块的状态。如果存在css资源加载的话,直接通过getScript方法加载资源即可,同样,对于那些还未加载过的资源,设置模块状态为LOADING,然后使用getScript进行加载并注册相关回调来修改模块状态,对于那些已经被之前模块申请加载中的模块,只需要再注册上一个回调函数即可。内嵌代码则直接执行__attach中注册的回调函数。

loader_callbacks

刚刚提到的几个方法都存在比较复杂的回调关系,从上图可以重新理清一遍思路。这个use方法使用回调机制于包括加载模块、读取资源、资源载入几个部分有机地联系起来。

最后再啰嗦一下,前面我们一直提到宿主环境这个词,前面也讲到多次,那么到底什么算一个宿主环境呢?其实简单说就是KISSY以及用KISSY创建的APP都算一个宿主环境,在其上都存在add, use等方法,且对于各个宿主环境的模块是彼此独立的,因此在宿主环境的模块对象中是彼此相互不同的,但是对于_loadQueue这个对象比较特殊,因为__load方法对其的操作均是在KISSY这个宿主环境中,而非其衍生的宿主环境。为什么这么处理呢?因为刚刚提到每个宿主环境的模块是彼此独立管理的,但是可能某些不同的模块使用了同样的外部JavaScript资源,那么如果我们不用_loadQueue进行管理便可会导致外部JavaScript资源重复载入两遍,因为JavaScript每次载入均会执行,因此可能导致未知异常,需要将其异步载入的节点加入_loadQueue中进行状态判定,但是对于CSS样式资源文件变不会存在这种执行问题且其载入也是同步的,因此不用在_loadQueue中保存载入的节点,只需记录其读取状态为LOADED即可(当然若此时由于一些未知因此导致的载入失败,状态也是无法判定的)。

getScript方法是典型的异步脚本载入方式,实现也是很简单的,同时其还支持CSS资源的载入,原理也是大同小异,这里也就不赘述了。

结束

至此从Loader机制的设计理念到具体实现方式已经大致介绍完毕,从篇幅上便可知其复杂程度,主要是由于其函数间依赖关系比较复杂,因此需要理清思路才能顺利理解。其实很多问题也都是在实现或者使用过程中才遇到的,从源代码中可见开发者是基于实际开发经验以及众多类库相关机制理解后才能够实现得如此周密。因此对于我们个人学习或练习实践中大可在思路理清之后形成一个大致程序框架,明确相关关系,而具体可能的细节问题不用在构架中过于纠结,逐渐尝试和调整,总之不可能一步登天,做到如此周详的考虑。

- EOF -