理解函数作用域与闭包
前言
但凡读书,或者学一门技术,都要问自己以下几个问题。
- 它是什么?
- 它有什么用?/发明它是为了解决什么问题?
- 它有什么弊端?
我下面就试着从这几个方向来阐述闭包这个概念。
概念
在了解闭包之前,我们需要了解几个概念。本文在这里只做简单介绍,如需要进一步了解,请参考文章末尾的链接。
作用域
变量和函数的可作用范围,分为局部作用域和全局作用域。Javascript不具有块级作用域,而具有函数作用域。
执行环境(execuation context)
变量和函数有权访问的其他数据。
执行环境栈(execuation context stack)
每个函数在执行的时候,会把它的执行环境推入一个栈中,在函数执行完毕后执行环境出栈并被销毁。保存在其中的所有函数和比变量定义随之销毁,控制权返回到之前的执行环境中。全局的执行环境在应用程序退出(浏览器关闭)才会被销毁。
作用域链(scope chain)
作用域链用于保证对执行环境有权访问的变量和函数的有序访问。
什么是闭包?
闭包这个概念,在函数式编程里很常见,简单的说,就是使内部函数可以访问定义在外部函数中的变量。严格一点的定义是
在函数内声明另一个函数,并且返回这个函数。这个返回的函数和它的执行环境整体叫做闭包。
让我们来看一个例子:
function f1(){ var val = 10; } console.log(val); //Uncaught ReferenceError: val is not defined(…)
由于从函数外部无法访问函数内部的变量,所以报出了错误。那么如何能够访问到局部作用域的变量呢?
function f1(){ var val = 10; function f2(){ console.log(val); } return f2; } var f2 = f1(); f2(); // 10
在这段代码中,f2 函数和其执行环境构成了一整个闭包。对于常规的 f1() 方法, 在其内部的变量 val 应该在 f1() 方法执行完毕以后就被垃圾回收。但是 f1() 返回了一个新的方法 f2()。由于 f2() 访问了其外部函数的变量 val,val就构成了f2函数的执行环境。val 存在于f2的作用域链中,只要f2()方法没有被销毁,其作用域链中的变量和函数就不会被销毁, val 也就会一直存在。
闭包有什么用?
for循环变量无法保持的问题
for (var i = 0; i < 5; i++) { setTimeout(function () { console.log(i); }, 5); }
上面这个代码块会打印五个 5
出来,而我们预想的结果是打印 1 2 3 4 5。
之所以会这样,是因为 setTimeout 中的 i 是对外层 i 的引用。当 setTimeout 的代码被解析的时候,运行时只是记录了 i 的引用,而不是值。而当 setTimeout 被触发时,五个 setTimeout 中的 i 同时被取值,由于它们都指向了外层的同一个 i,而那个 i 的值在迭代完成时为 5,所以打印了五次 5
。
为了得到我们预想的结果,我们可以把 i 赋值成一个局部的变量,从而摆脱外层迭代的影响。
for (var i = 0; i < 5; i++) { (function (idx) { setTimeout(function () { console.log(idx); }, 5); })(i); }
制造函数构造器
假如我们要实现一系列的函数:add10,add20。我们为此构造了一个名为 adder 的构造器,如下:
var adder = function (x) { var base = x; return function (n) { return n + base; }; }; var add10 = adder(10); console.log(add10(5)); var add20 = adder(20); console.log(add20(5));
每次调用 adder 时,adder 都会返回一个函数给我们。我们传给 adder 的值,会保存在一个名为 base 的变量中。由于返回的函数在其中引用了 base 的值,于是 base 的引用计数被 +1。当返回函数不被垃圾回收时,则 base 也会一直存在。
闭包有什么弊端?
由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
面试题
面试题一
请定义这样一个函数
function repeat (func, times, wait) { } // 这个函数能返回一个新函数,比如这样用 // var repeatedFun = repeat(alert, 10, 5000) // 调用这个 repeatedFun ("helloworld") // 会alert十次 helloworld, 每次间隔5秒
代码参见:JS bin 闭包面试题一
面试题二
写一个函数stringconcat, 要求能
var result1 = stringconcat("a", "b") result1 = "a+b" var stringconcatWithPrefix = stringconcat.prefix("helloworld"); var result2 = stringconcatWithPrefix("a", "b") result2 = "helloworld+a+b"
代码参见:JS bin 闭包面试题二
参考:
学习Javascript闭包(Closure)
node-lessons/lesson11 at master · alsotang/node-lessons · GitHub
JavaScript作用域链