一、作用域(Scope)
1、scope chain
每个JavaScript执行环境(某教程对于“执行环境”的解释是:Js解释器每次执行一个function时,就会为该function创建一个新的执行环境。)都关联一个scope chain。scope chain是由一组对象组成的链表,当Js代码需要查找变量x的值时,会首先从此链表的第一个对象查起,如果该对象含有名为x的属性,则取用该属性值,如果没有,则继续向下一个查找。
在顶层Js代码(比如,不包含在任何function中的代码)中,scope chain仅由一个对象组成,那就是global object;在一个非嵌套function中,scope chain由两个对象组成,第一个是function的call object,第二个是global object,所以当在该function内去某变量时,首先查找的是call object,其次是global object;在一个嵌套function(nested function,定义在其它function中的function,有时也成内部function)中,scope chain由三个以上的对象组成,第一个是该function的call object,其次是它上一层funciton的call object,以此类推,最后是global object。
2、function scope
JavaScript中的function都是lexically scoping(也称static scoping,与dynamically scoping对应)。也就是说,function的作用域在function定义时就被确定而非运行时。当一个function被定义时,当前环境的scope chain会被作为function内部状态的一部分保存起来。了解了这个机制,我们就来详细分析一下scope chain的构造过程。结合如下代码。
- var x = "global";
- function f() {
- var x = "local1";
- function g() {
- var x = "local2";
- function h() {
- alert(x);
- }
- h();
- }
- g();
- }
- f(); // Calling this function displays "local"
- var y = “global2”;
- 代码开始运行的时候,Js就建立了一个context,我们可以把它叫做global context,对应的就拥有了一个scope chain,该scope chain中只包含一个元素——global object。
- 代码运行至第3行,定义“f”函数,global context中的这个scope chain被保存进函数“f”的某个内部状态变量。
- 代码运行到第15行,执行“f”函数,此时Js解释器又为这个函数创建了一个执行环境,还有一个call object,“f”由状态变量中读出scope chain,并将自己的scope设置为该scope chain,然后把它的call object插入到chain的首位,此时函数f的scope chain中就包含了两个object。此时,f中的嵌套function g 执行真正的定义操作。
- g被定义时,定义所在环境中那个包含两个object的scope chain就被保存进函数“g”的某个内部状态变量。
- 运行到第12行代码,进行与函数f执行时的相同操作,nested function g的scope chain依次包含g的call object,f的call object和global object。
- 最后一直运行到nested function h,它的scope chain中就是h的call object,g的call object,f的call object和global object。
经分析可知,containing function的变量会随着call object传入nested function中,因此,nested function可以任意取用和修改containing function的变量和arguments。需要注意的是,虽然function的scope chain是固定的(即由哪些call object组成),但是那些call object中的properties却不是固定的,是动态改变的,也就是说,它所能访问到的所有properties的值都是在它被调用执行之时的实时value。如下例。因对动态实时概念的不了解而造成的错误常常出现在事件的绑定应用上,我在下一章节会详细讲述。
function f1() {
var sum = 0;
for (var i=0; i<arguments.length; i++) {
sum = sum + arguments[i];
}
return sum;
}
function bindArguments(f) {
var boundArgs = arguments;
var newfunc = function() {
var args = [];
for(var i = 1; i < boundArgs.length; i++) args.push(boundArgs[i]);
for(var i = 0; i < arguments.length; i++) args.push(arguments[i]);
return f.apply(this, args);
}
//change its value here
boundArgs = [f, 1, 2, 3];
return newfunc;
}
function test1() {
var result = bindArguments(f1,3,4,5,6);
document.write(result(1,2)); //print 9 instead of 21
}
二、闭包(closure)
1、什么是闭包
课本中的解释:闭包,是代码与其所运行的scope共同构成的联合体。因此,理论上讲,Js中的任何一个function都是一个闭包。但是,这个闭包的概念现在多被应用在涉及nested function的情况,有时,甚至将那些在定义域之外被调用的nested function称作一个闭包。如下例:
- function makefunc(x) {
- return function() { return x++; }
- }
- var a = [makefunc(1), makefunc(5), makefunc(9)];
- alert(a[0]()); // Displays 1
- alert(a[0]()); // Displays 2
- alert(a[1]()); // Displays 5
- alert(a[1]()); // Displays 6
- alert(a[2]()); // Displays 9
- alert(a[2]()); // Displays 10
这段代码中标绿的部分是一个nested function,并在第5、6、7行被调用,这个区域是在它的定义域之外的,因此,这个nested function是一个闭包。
我们来简单分析下这个代码产生如上结果的原因。
- Js解释器执行makefunc(1),产生一个对应的call object(内包含arguments:[1], x=1)
- 触发nested function的定义操作,将call object保存进nested function的内部状态变量,此call object将一直跟随这个新建的function,直到它被回收。
- 将新建的nested function返回,并指定a[0]做引用。
- 第一个a[0]()第一次执行nested function,修改call object中的变量x,返回 1。
- 第二个a[0]()第二次执行nested function,在已修改的call object基础上再次修改变量x, 返回2。
2、闭包应用
模拟私有静态变量。
有时,程序员可能希望拥有一种变量,这种变量在整个环境中只有一个,它的值可被某function修改,每次修改就都是基于上一次修改的值。它的特点类似于Java类中的static变量。实现这种效果,我们可以有三种方式:
- 将此变量设置成全局变量。但是这个变量只需要被一个function使用,设置为全局变量将污染全局命名空间
- 可以将该变量设置为某function的自有属性(注意,不是local变量)
- uniqueInteger.counter = 0;
function uniqueInteger() {
return uniqueInteger.counter++;
} - document.write(uniqueInteger()); //prints 0
- document.write(uniqueInteger()); //prints 1
- 应用闭包,将变量通过call object保存到被返回的nested function中
- uniqueID = (function() {
- var id = 0;
- return function() { return id++; };
- })();
- document.write(uniqueID()); //prints 0
- document.write(uniqueID()); //prints 1
后两种方式的区别在于,第一种,我们可以任意的在外部操作改变uniqueInteger.counter 的值,出于安全性的考虑这显然不合标准;而第二种,变量id无法用任何外部方法修改,效果类似成为一个叫做uniqueID的类的private变量。
其实,通过上面的例子,我们可以更形象的理解闭包的概念。包含nested function的containing function它起的一个作用就是一个空间,这个空间中包含了新定义的一些变量,这些变量的值都由空间中的nested function控制,且仅受这里面的nested function控制,因此算是个封闭的空间。containing function一旦执行,这个空间就成了一个空间实例,随着nested function被作为返回值赋给外面的某变量。以后,我们通过变量来执行nested function时,所有的操作结果都仅在那个空间实例中起作用。那个封闭的空间就是一个闭包。
我们之前研究的闭包例子都是一个空间中包含一个nested function,下面这个例子包含两个。结果会向我们展示,这两个function会分享这个空间中的资源,并能平等的操作。
- function makeProperty(o, name, predicate) {
- var value;
- o["get" + name] = function() { return value; }; //nested function 1
- o["set" + name] = function(v) { //nested function 2
if (predicate && !predicate(v)) - throw "set" + name + ": invalid value " + v;
- else
- value = v;
- };
- }
- var o = {};
- makeProperty(o, "Name", function(x) { return typeof x == "string"; });
- o.setName("Frank"); // manipulate variable value
- document.write(o.getName()); // Prints "Frank"
- o.setName(0); // throw an exception:setName: invalid value 0
事件绑定
在编写网页时,我们经常需要为不同的element绑定相同的事件,比如三个按钮,都绑定一个执行alert操作的onclick事件,有一种写法如下:
- function attachEvent() {
- var buttons = document.getElementsById("alertButton");
- for(var i=0; i<buttons.length; i++) {
- buttons[i].onclick = function(){
- alert(i);
- }
- }
- }
- attachEvent();
这段代码看似没有问题,但是执行起来你会发现,所有的按钮弹出的都是“2”。这是因为绑定给button的事件函数要在button真正被click时才会执行,而此时传入nested function的call object的变量i值已经更新到2,因此此时点击所有按钮都会产生同样的结果。
我们知道,我们之所以取到了i变化后的值,是因为我们的function是在i变化之后才执行的,但如果我们可以在取到"i"这个值时立即执行函数,那么这个值就固定了。然而,这种“立即执行”显然不适用于onclick事件的绑定函数上,但是,我们却可以把一个函数加在绑定函数的外面,把i变量的值传进这个函数中,并让它立刻执行,那这样,我们期望的效果就达到了。我们就是通过(function(){})()这种语法实现这种“立即执行”的。具体改进如下:
- function attachEvent() {
- var buttons = document.getElementsByTagName("input");
- for(var i=0; i<buttons.length; i++) {
- buttons[i].onclick = (function(){
- var j = i;
- return function(){
- alert(j);
- }
- })();
- }
- }
- attachEvent();
新function中的变量j负责获取i的值,此后j就与i没有关系了(因为在js里,premitive type的传递方式是值传递),并且只受这个function中的nested function控制。为了符合面向对象编程的可复用原则,上面那个方法还可以改进为:
- function alertAction(letter){
- return function(){
- alert(letter)
- }
- }
- function attachEvent() {
- var buttons = document.getElementsByTagName("input");
- for(var i=0; i<buttons.length; i++) {
- buttons[i].onclick = alertAction(i)
- }
- }
- attachEvent();