JS闭包什么鬼

前言

闭包问题是js中比较困扰人的一个问题,现在我们来探究这个到底是什么鬼

问题起源

问题:循环异步调用

典型的JS初学者犯下错误

for (var i = 0; i < 10; i++) {
  setTimeout(function () {
    console.log(i)
  })
}

这个函数看上去应该输出0 , 1 , 2, 3, 4, 5, 6, 7, 8, 9,但是实际上执行会发现输出了10个10

为什么?


闭包

js闭包最简单的解释:函数x返回函数y,y中所有的外层变量(包括x的变量)形成闭包。在y中可以使用外层所有的变量,并且y引用的x中的变量将会与函数y共存亡,即使x已经运行完毕被销毁。

Example:

function inner

词法作用域

In languages with lexical scope (also called static scope), name resolution depends on the location in the source code and the lexical context, which is defined by where the named variable or function is defined 在使用词法作用域(静态作用域)的语言中,名字解析依赖于名字在代码中出现的位置以及词法上下文,而这是在命名的变量或函数处定义的。

Example:

function p () {
  print x
}

function q () {
  variable x = 1
  p()
}

以上函数示例演示的是动态作用域的闭包。这个在词法作用域的语言中将无法使用,因为在词法作用域中,名字解析是依赖于名字所在代码的位置以及词法上下文的。在函数p定义的时候,显然在他的词法上下文中并不能找到变量x的解析结果,因此这种情形在词法作用域中无效。

现代编程语言大多数使用词法作用域,其中包括我们日常使用的Python、Javascript。

初学者对词法作用域的"闭包"会非常疑惑,需要理解的是“词法作用域中的闭包,取决于闭包定义时候的上下文,而不是调用他的时候的上下文”。

Example: 有效闭包

function outer () {
  var value = 0
  return function inner () {
    return value++
  }
}

var count = outer()

count() // 0
count() // 1
count() // 2

Example:无效闭包

function inner () {
  return value++
}

function outer () {
  var value = 0
  return inner
}

var count = outer()

count() // Uncaught ReferenceError: value is not defined

函数作用域

要点:

  • 每个javascript的函数会产生一个新的作用域
  • 作用域决定变量的可访问性
  • 在一个函数内部定义的局部变量是不能在函数外访问的
  • 相反,在一个函数内定义的变量可以在函数内被访问

Example:

function f () {
  var x = 1
  // 函数内部可以访问到x
  function g () {
    var y = x  // 函数内部的函数也可以访问到变量
  }
  console.log(x)
}

var变量提升

注意var声明的变量具有变量提升的特性,在函数作用域中无论在何处声明,处处可使用。但是按照规范一般都先声明再进行使用。

Example:

function f () {
  // 变量提升,此处不是全局变量
  x = 1
  console.log(x)
  var x
}

var非块级作用域

看以下例子

function f () {
  var arr = [1,2,3,4,5]
  for (var x of arr) {
    // loop
  }
  console.log(x) // 5

  if (true) {
    var y = 5  // 不存在块级作用域
  }
  console.log(y) // 5
}

es6 let的块级作用域

解决问题

循环异步调用问题解决

使用var, 函数作用域

使用let, 块级作用域

内存泄漏问题


Further Reading