22. 闭包函数
作用域和闭包是 JavaScript 最重要的概念之一,如果想进一步深入学习Javascript,就必须理解 JavaScript 作用域和闭包的工作原理。
作用域
前面已经有简答介绍过变量作用域,在这里我们再深入了解。和其他大多数编程语言一样,Javascript也采用词法作用域(lexical scoping),也就是说,函数的执行依赖于变量作用域,这个作用域是在函数定义时决定的,而不是函数调用时决定的。
一个变量的作用域(scope)是程序源代码中定义这个变量的区域。作用域控制着变量与函数的可见性和生命周期。在 JavaScript 中,变量的作用域有全局作用域和局部作用域两种。
全局作用域(Global Scope)
全局变量拥有全局作用域,在javascript代码中任何地方都是有定义的。换句话说就是在代码中任何地方都能访问到的。一般来说以下几种情形拥有全局作用域:
1. 所有在最外层定义(非函数体内定义)的变量都拥有全局作用域
<xmp>
var scope = "global"; // 声明一个全局变量
function checkscope(){
console.log(scope);
}
checkscope(); // "global"
console.log(scope) // "global"
</xmp>
2. 所有末定义直接赋值的变量,系统会自动声明为拥有全局作用域的变量; 这种情况要特别注意,很容易造成程序BUG
<xmp>
function checkscope(){
scope = "global"; // scope变成了一个全局变量
console.log(scope);
}
checkscope(); // "global"
console.log(scope) // "global"
</xmp>
3. 所有window对象的属性拥有全局作用域
一般情况下,window 对象的内置属性都拥有全局作用域,例如 window.name、window.location、window.top 等等。
局部作用域(Local Scope)
局部作用域一般只在固定的代码片段内可访问到,最常见的例如函数内部。在函数内声明的变量只在函数体内有定义,它们是局部变量,作业域是局部性的。函数参数也是局部变量,它们只在函数体内有定义。
<xmp>
function checkscope() {
var local = "local"; // 显式声明一个局部变量
return local; // 返回全局变量的值
}
console.log(checkscope()); // "local"
console.log(local); // error: local is not defined.
</xmp>
上面代码中,在函数体内定义了变量 local,在函数体内是可以访问了,在函数外访问就报错了。
在函数体内,局部变量的优先级高于同名的全局变量。如果在函数内声明的一个局部变量或函数参数中带有的变量和全局变量重名,那么全局变量就会被局部变量所遮盖。
<xmp>
var scope = "global"; // 声明一个全局变量
function checkscope(){
var scope = "local"; // 声明一个同名的局部变量
return scope; // 返回局部变量的值,而不是全局变量的值
}
checkscope(); // “local”
</xmp>
函数作用域和声明提前
Javascript的函数作用域是指在函数内声明的所有变量在函数体内始终是可见的。这意味着变量在声明之前甚至已经可用。Javascript的这个特性被非正式的称为声明提前(histing),即Javascript函数里声明的所有变量(但不涉及赋值)都被提前至函数体的顶部。
作用域链
当代码在一个环境中执行时,会创建变量对象的一个作用域链(scope chain)。作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。
每一段Javascript代码(全局代码或函数)都有一个与之关联的作用域链(scope chain)。这个作用域链是一个对象列表或链表,这组对象定义了这段代码“作用域中”的变量。当Javascript需要查找变量x的值时(这个过程称为“变量解析”(variable resolution)),它会从链中的第一个对象开始查找,如果这个对象有一个名为x的属性,则会直接使用这个属性的值,如果第一个对象中不存在,则继续查找链的下一个对象,以此类推,一直找到最后一个对象。如果作用链上没有任何一个对象含有属性x,那么就认为这段代码的作业域链上不存在x,并最终抛出一个引用错误(ReferenceError)异常。
标识符解析是沿着作用域链一级一级地搜索标识符的过程。搜索过程始终从作用域链的前端开始,然后逐级地向后回溯,直至找到标识符为止(如果找不到标识符,通常会导致错误发生)。
<xmp>
function fn_sum(num1, num2){
var sum = num1 + num2
return sum ;
}
</xmp>
在函数fn_sum创建的时候它的作用域链填入全局对象,全局对象中有所有全局变量
<xmp>
var result = fn_sum(2,3);
</xmp>
如果执行环境是函数,那么将其活动对象(activation object)作为作用域链第一个对象,第二个对象是包含环境,下一个是包含环境的包含环境。。。。。
闭包
闭包是指有权访问另一个函数作用域中的变量的函数。可以简单理解成定义在函数内部的函数。
闭包可以用在许多地方。它的最大用处有两个,一个是可以读取函数内部的变量(作用域链),另一个就是让这些变量的值始终保持在内存中。
<xmp>
function fun() {
var n = 1;
add = function() {
n += 1
}
function fun2(){
console.log(n);
}
return fun2;
}
var result = fun();
result(); // 1
add();
result(); // 2
</xmp>
在这段代码中,result 实际上就是函数 fun2。它一共运行了两次,第一次的值是 1,然后执行add(),n变成了2,第二次的值是 2。这证明了,函数 fun 中的局部变量 n 一直保存在内存中,并没有在 fun 调用后被自动清除。
这段代码中另一个值得注意的地方,就是 add = function() { n += 1 } 这一行。首先,变量 add 前面没有使用 var 关键字,因此 add 是一个全局变量,而不是局部变量。add 的值是一个匿名函数(anonymous function),而这个匿名函数本身也是一个闭包。
由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在 IE 中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除或设置为 null,断开变量和内存的联系。
<xmp>
//创建函数
var compareNames = createComparisonFunction("name");
//调用函数
var result = compareNames({ name: "Nicholas" }, { name: "Greg" });
//解除对匿名函数的引用(以便释放内存)
compareNames = null;
</xmp>
通过将 compareNames 设置为等于 null解除该函数的引用,就等于通知垃圾回收例程将其清除。随着匿名函数的作用域链被销毁,其他作用域(除了全局作用域)也都可以安全地销毁了。
下图展示了调用 compareNames() 的过程中产生的作用域链之间的关系。