搞懂JavaScript之闭包

165 阅读5分钟

这是我参与8月更文挑战的第2天,活动详情查看: 8月更文挑战

1. 什么是闭包

《JavaScript高级程序设计》中给出的定义:闭包是有权限访问其他函数作用域内的变量的一个函数。

《你不知道的JavaScript》中给出的定义:当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。

我的理解是,闭包就是能够读取其他函数内部变量的函数。

闭包原理:利用js中的垃圾回收机制,在回收被销毁的变量和函数时,如果发现被销毁的变量的函数正在被另一个函数使用,那么使用中的变量和函数将不被释放,长期驻留在内存中,直到整个程序退出时才被释放。

闭包的作用:一个是可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。

function foo() { 
  var a = 2;
  function bar() { 
    console.log( a );
  }
  return bar; 
}
var baz = foo();
baz(); 

在上面这个例子中,bar()只是换了一种方法被调用,baz()输出2,此时bar()在它的词法作用域外面被执行了。

在 foo() 执行后,通常会期待 foo() 的整个内部作用域都被销毁,因为我们知道引擎有垃圾回收器用来释放不再使用的内存空间。由于看上去 foo() 的内容不会再被使用,所以很自然地会考虑对其进行回收。

而闭包的“神奇”之处正是可以阻止这件事情的发生。事实上内部作用域依然存在,因此没有被回收。谁在使用这个内部作用域?原来是 bar() 本身在使用。

拜 bar() 所声明的位置所赐,它拥有涵盖 foo() 内部作用域的闭包,使得该作用域能够一直存活,以供 bar() 在之后任何时间进行引用。

bar() 依然持有对该作用域的引用,而这个引用就叫作闭包。

释放闭包

function foo() { 
  var a = 2;
  function bar() { 
    console.log( a );
  }
  return bar; 
}
var baz = foo();
baz(); 
baz = null //释放闭包

无论使用何种方式对函数类型的值进行传递,当函数在别处被调用时都可以观察到闭包。

var fn;
function foo() {
  var a = 2;
  function baz() { 
    console.log( a );
  }
  fn = baz; // 将 baz 分配给全局变量 
}
function bar() 
{
	fn(); 
}
foo();
bar(); 

无论通过何种手段将内部函数传递到所在的词法作用域以外,它都会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。

闭包在我们平常写代码的过程中很常见

回调函数

function wait(message) {
   setTimeout( function timer() {
       console.log( message );
	}, 1000 ); 
}
wait( "Hello, closure!" );

模块

function CoolModule() {
  var something = "cool";
  var another = [1, 2, 3];
  function doSomething() { 
    console.log( something );
  }
  function doAnother() {
    console.log( another.join( " ! " ) );
  }
  return {
    doSomething: doSomething, 
    doAnother: doAnother
  }; 
}
var foo = CoolModule(); 
foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3

2. 思考题

  1. 利用循环分别输出数字 1~5,每秒一次,每次一个。
for (var i=1; i<=5; i++) { 
  setTimeout( function timer() {
     console.log( i );
  }, i*1000 );
}

这段代码在运行时会以每秒一次的频率输出五次 6。

首先解释 6 是从哪里来的。这个循环的终止条件是 i 不再 <=5。条件首次成立时 i 的值是6。因此,输出显示的是循环结束时 i 的最终值。

延迟函数的回调会在循环结束时才执行。事实上,当定时器运行时即使每个迭代中执行的是setTimeout(.., 0),所有的回调函数依然是在循环结束后才会被执行,因此会每次输出一个 6 出来。

缺陷是我们试图假设循环中的每个迭代在运行时都会给自己“捕获”一个 i 的副本。但是根据作用域的工作原理,实际情况是尽管循环中的五个函数是在各个迭代中分别定义的,但是它们都被封闭在一个共享的全局作用域中,因此实际上只有一个 i。

利用立即执行函数,形成一个封闭的作用域

for (var i=1; i<=5; i++) { 
  (function() {
		setTimeout( function timer() { 
      console.log( i );
    }, i*1000 );
  })();
}

这样也不行,i还是公用一个全局变量,IIFE 只是一个什么都没有的空作用域。它需要包含一点实质内容才能为我们所用。它需要有自己的变量,用来在每个迭代中储存 i 的值

for (var i=1; i<=5; i++) { 
  (function() {
    var j = i;
    setTimeout( function timer() {
      console.log( j );
    }, j*1000 );
  })(); 
}
for (var i=1; i<=5; i++) { 
  (function(j) {
		setTimeout( function timer() { 
      console.log( j );
    }, j*1000 );
  })( i );
}

在迭代内使用 IIFE 会为每个迭代都生成一个新的作用域,使得延迟函数的回调可以将新的作用域封闭在每个迭代内部,每个迭代中都会含有一个具有正确值的变量供我们访问。

for (let i=1; i<=5; i++) { 
  setTimeout( function timer() {
     console.log( i );
  }, i*1000 );
}

for 循环头部的 let 声明还会有一个特殊的行为。这个行为指出变量在循环过程中不止被声明一次,每次迭代都会声明。随后的每个迭代都会使用上一个迭代结束时的值来初始化这个变量。

2. 下面代码的输出结果是什么

function global(num){
	return function(){
		console.log(++num)
	}
}
var demo = global(1);
demo()
demo()
demo()

使用++i(前置++),i先将自身的值自增1,再将自增后的值赋值给变量a

使用i++(后置++),i先将自身的值赋值给变量a,然后再自增1

3. 下面代码的输出结果是什么

function makeAdder(x) {
  return function(y) {
    return x + y;
  };
}

var add5 = makeAdder(5);
var add10 = makeAdder(10);

console.log(add5(2));
console.log(add10(2));

4. 下面代码的输出结果是什么

function fun(n, o) { 
  console.log(o);
  return { 
    fun: function(m) { 
      return fun(m, n); 
    }
  };
}

// 1
var a = fun(0); 
a.fun(1); 
a.fun(2); 
a.fun(3); 

// 2
var b = fun(0).fun(1).fun(2).fun(3); 

// 3
var c = fun(0).fun(1);
c.fun(2);
c.fun(3); 

答案

undefined,0,0,0

undefined,0,1,2

undefined,0,1,1