理解函数作用域与闭包

前言

但凡读书,或者学一门技术,都要问自己以下几个问题。

  • 它是什么?
  • 它有什么用?/发明它是为了解决什么问题?
  • 它有什么弊端?

我下面就试着从这几个方向来阐述闭包这个概念。

概念

在了解闭包之前,我们需要了解几个概念。本文在这里只做简单介绍,如需要进一步了解,请参考文章末尾的链接。

作用域

变量和函数的可作用范围,分为局部作用域和全局作用域。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作用域链
posted @ 2016-01-11 10:35  潘诗瑶  阅读(301)  评论(0编辑  收藏  举报