Javascript是基于词法作用域的语言:通过阅读包含变量定义在内的数行源代码就能知道变量的作用域。全局变量在程序中始终有定义,局部变量在声明它的函数体内以及所嵌套的函数内始终有定义。
首先,有几个概念是需要先弄明白的。
执行上下文
什么是”上下文“?在计算机领域,这个名词比较抽象,简单理解,就是程序在执行的时候,程序当前的状态,一般可以简单理解成”当前能直接或间接访问到的程序变量“。
javascript在执行的时候,可以分成 1)顶层代码,和 2) 函数内代码。顶层代码的含义就是不包含在任意函数定义内的代码。
顶层代码的执行上下文,可以认为就是全局对象(glabal),在浏览器中就是window对象。
至于函数内的代码,只有执行的时候,才会有”执行上下文“。
可以简单将函数的执行上下文理解成类似下面的对象:
var executionContext = { activeObject: {...}, // 活动对象。什么是活动对象? scopeChain: [activeObject, ...], // 作用域链 this: {...} // this关键字};
当一个函数执行的时候,会首先构造一个执行上下文,步骤如下:
a. 创建活动对象
b. 构造作用域链
c. 构造this对象
下面详细说明各个步骤:
活动对象
我们可以把参数、局部变量都看作是一个自定义实现的对象的属性,这个自定义的对象我们称为”活动对象“。
作用域链
为说明方便,我们把顶层的代码看做运行在一个不可见的、全局的匿名函数里。
作用域链是不能脱离函数来说明的,它跟函数紧密相连,只有与函数一块时,作用域链才有意义。
函数:
1) 在定义的时候,每个函数对象都有一个内部的属性:[[Scope]],这个属性的值,就是当前正在执行的函数的作用域链
2) 在执行的时候,会构造一个活动对象,并且将活动对象和[[Scope]]合并在一起组成作用域链(scope chain),而活动对象在作用域链的最前面
假设我们在顶层代码定义了函数:
function add(num1, num2) { return num1 + num2;}
那在定义的时候,函数add对象就有一个内部属性[[Scope]]了,它是一个数组或链表,只有一个元素,指向全局对象。如图:
当调用add方法的时候,会构造一个执行上下文(execution context),上下文包含了scopeChain属性,它实际上是活动对象和add.[[Scope]]的合并,如下图:
var Total = add(5, 10);
事实上,我们可以把变量名的解析看作是从作用域链中,从头到尾遍历作用域链中的对象,看这些对象中有没有同名的属性。
with语句
了解了作用域链的概念后,就很容易理解with语句的含义:with语句,就是简单粗暴地将一个对象插入作用域链的头部。
使用局部变量提高性能
在jquery源代码中,可以看到有类似的代码:
(function(window) { var preferredDoc = window.document; // 这里直接使用window的局部变量,省了一次查上一个的作用域链对象 // 底下有许多地方直接用到了preferredDoc,这就避免了遍历全部的作用域链对象})(window);
闭包
作用域链理解透了,闭包也可以顺利理解。
什么是闭包?函数体的局部变量可以保存在它的作用域链中。
当函数有嵌套时,我们约定:被嵌套的函数叫 内部函数(inner function),外层的函数称为外部函数(outer function)。
那么,当inner function运行的时候,inner function的作用域链,也会包含outer function的作用域链:inner function的作用域链=inner function的活动对象+outer function的作用域链。
这种情况比较神奇,尤其在inner function被保存在outer function之外的某个变量或属性时,会发生一些神奇的现象。
function outer() {
var foo = 1;
function inner() {
foo++;
alert(foo);
}
return inner;
}
var f = outer();
f();
f();
var k = outer();
k();