Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

谈谈闭包 #8

Open
fanyj1994 opened this issue Nov 26, 2019 · 0 comments
Open

谈谈闭包 #8

fanyj1994 opened this issue Nov 26, 2019 · 0 comments

Comments

@fanyj1994
Copy link
Owner

闭包一直是一个令广大 JS 开发者迷惑的概念,这一篇,我将一一走近作用域、作用域链、执行上下文,揭开它们的神秘面纱,以轻松理解闭包到底是个什么玩意。

开始学习闭包的时候,总是容易陷入到寻找精准定义的误区中去,但是,单一地为闭包下定义,是很难理解的,因为之所以会出现闭包这个东西,和前面我们提到的这些概念息息相关:执行上下文、作用域、作用域链,理解了这些东西,才能理解闭包。

作用域和作用域链

在任何编程语言中,作用域都是核心概念,变量在作用域内找到归属,可以在其中自由活动,可以说,作用域是变量的领地,领地之外的人无法干涉和改变其活动。JS基于词法作用域规则,每个变量的作用域在其定义时就被确定。函数是 JS 中唯一拥有局部块级作用域的容器,函数的参数和内部的变量在局部作用域内部可访问和操作。函数的作用域同样在其定义时就被确定。

当局部作用域出现与外层作用域同名变量时,会出现属性遮蔽的情况,会优先访问到局部作用域的变量。

const name = 'Alice'
function sayHi() {
  const name = 'Fan'
  console.log(name) // Fan
}
sayHi()

每一个函数都有自己的局部作用域,当多个函数嵌套定义时,他们的作用域也就会随之嵌套,形成一个作用域链。

JS 的变量查找,就是沿着作用域链进行。

const name = 'Fan'

function sayName() {
  console.log(name) // Fan
  const age = 25
  function sayAge() {
    console.log(age) // 25
  }
  sayAge()
}

sayName()

在上面这个例子中,两个函数sayAgesayName 在创建时,形成嵌套关系,它们都会创建自己的作用域,这些作用域形成一个作用域链:

sayAge -> sayName -> 全局作用域

sayAge 在查找 age 这个变量时,会从最里层的作用域找起,可以看到,自己的作用域内部并没有声明变量 age,它会沿着作用域链向上,到最近的 sayName 中查找,在这里得到了 age 变量。

可以看到,作用域链的查找方式和原型链的查找方式十分类似,区别在于,原型链查找不到时返回一个 undefined,而作用域链会报出 ReferenceError.

如果在 sayName 中也找不到,就会继续向上,到全局作用域,这里也找不到,就会返回一个 ReferenceError。

事实上,我们可以从技术上为理解闭包,在计算机科学中,闭包指的是这样一种机制:不管在哪里被调用,一个函数,可以访问到其作用域链上外层作用域中任意位置的变量。

明确作用域链的工作机制后,就很好理解函数的执行上下文了。

执行上下文

我们通过函数声明来定义一个函数,该函数会提升到所在作用域的最顶部,确保该函数在作用域内的任何位置都可以被调用:

sayHi('Fanyj') // hi, Fanyj

function sayHi(name) {
  console.log('hi, ' + name)
}

执行上面代码,会打印出“hi, Fanyj”。可以看到,我们在函数定义之前调用函数,函数成功执行。为什么会这样?

是因为,每一个函数在被执行的时候,会初始化一个执行上下文环境,这个环境里包含以下几个东西:

  • this 关键字
  • 函数声明
  • arguments:实参对象(被调用时传入的参数,未调用时为空)
  • 函数内部的变量、形参(未被赋值)等
  • 对当前作用域链的引用(包含访问到的外层作用域的变量)

注意上面的第5项内容,“对当前作用域的引用”,正是这个操作,完成了函数与外部作用域的链接,形成了作用域链。

我们讲,JS 中的所有函数都可以叫做闭包,因为它们都拥有对当前作用域链的引用。

到了这里,一切还很正常,闭包就是保留外部作用域链的引用,而使得闭包可以访问作用域链上的所有变量而已,但是,当这个特性被花样使用之时,它的强大才能被显示出来(同时也带来了迷惑)。

但在 JS 中,谈起闭包的时候,更偏向于这样一种现象,指由于函数保留了对于作用域链的引用,所以,在任何位置调用函数,都可以访问其作用域中的变量,哪怕这个变量表面上是私有的。

闭包到底复杂在哪里?

先看一个例子:

const name = 'Alice'
function bar() {
  const name = 'Fan'
  function sayName() {
    console.log(name)
  }

  return sayName
}

const func = bar()
func()

试思考,func 被调用时,会输出什么呢?

上面这种情况,把一个内部函数作为另一个函数的返回值返回出去,并在外部进行调用,这正是闭包的一个经典用法。这种情况下,很容易会认为,既然函数在全局环境被调用,其访问的 name 也将会在全局作用域内查找,返回值将是 Alice。

但在上面的例子中,神奇的事情发生了,我们在全局作用域访问到了位于 bar 函数局部作用域的变量 name,所以答案仍然是:Fan。发生了什么?不是说,变量只可以在作用域范围内被访问吗,那 bar 中的变量 name,为什么可以在全局作用域访问呢?

如果你理解前面关于作用域的描述,就能够很容易判断出正确结果,在前面我们提到:

变量的作用域在其定义时就被确定

也就是说,在函数内部的变量,它会根据定义时产生的作用域链进行查找确定,和它在何处调用没有任何关系。

我们换个角度来理解。在正常情况下,之所以在作用域外部访问不到内部的变量,是因为函数返回的时候,如果没有其他引用,函数执行上下文中对于作用域的引用会被垃圾回收机制回收(防止内存滥用)。所以,很多人的疑惑点在这里出现,当 bar 执行完毕,其作用域被回收,那变量 name 也就自然被回收了,那为什么还能被访问到呢?

问题当然出现在返回值上,因为返回值 sayName 是一个函数,该函数拥有外层到全局作用域的全部引用,正是这个引用,阻止了外层作用域被回收。

我们提到过,每一个函数每次被调用的时候,都会初始化一个新的执行上下文环境,这个环境中,包含了对作用域链的引用,作用域链是在函数定义时就确定的。也就是说,内部嵌套函数的执行环境和外部函数的执行环境是没有关系的,它们都拥有独立的执行上下文。哪怕同一个函数被多次调用,同样会创建多个独立的执行上下文。所以,外部函数执行完毕,其打算回收 name,但内部嵌套函数进行变量查找时,其走的路线是自己的上下文对象中对于作用域链的引用。

所以说,最常见的对于闭包的描述是这样的:闭包指函数可以访问其所在词法作用域,保留着对上层作用域的引用,哪怕它在作用域外执行

再如果对闭包有疑惑,请默念以下三句话:

  1. 作用域和作用域链在函数定义时就确定了
  2. 每个函数在每一次调用时,都会创建属于自己的执行上下文
  3. 执行上下文中包含了对于变量作用域链的引用

事实上,只要函数在其定义的作用域外被调用(常见的形式有两种:函数作为返回值,函数作为参数传递),闭包就出现了。

所以说,哪怕你根本没法解释闭包,但我相信在你的代码中,闭包已经悄悄出现很多次了。

闭包在一些 JS 库中大规模应用,例如最典型的用法是 JS 模块,在一个 JS 模块中,其所有的功能被封装在内部,只向外部暴露一个接口,外界可以通过这个接口访问模块内部的数据

闭包的性能问题和安全问题

很明显,闭包阻止了内存回收机制对于作用域的回收,保持着对于作用域内活动对象的引用,其必定会带来多余的内存开销。所以,要避免大量使用闭包。

同时,由于闭包的存在,可以在作用域外访问内部的变量,本应该销毁的变量仍然存在,这就导致了内存泄露风险。

以上就是我对于闭包的理解。

参考资料

  1. 【推荐】深入理解javascript原型和闭包
  2. 【Book】《JavaScript权威指南》
  3. 【Book】《你不知道的JavaScript》
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant