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

JavaScript 中的闭包 #3

Closed
Tracked by #6
swiftwind0405 opened this issue Jan 13, 2020 · 0 comments
Closed
Tracked by #6

JavaScript 中的闭包 #3

swiftwind0405 opened this issue Jan 13, 2020 · 0 comments

Comments

@swiftwind0405
Copy link
Owner

swiftwind0405 commented Jan 13, 2020

闭包的定义

闭包是指有权访问另一个函数作用域中的变量的函数,但是闭包不只是函数,还应该包括函数可访问的词法作用域(因为作用域链)

闭包有三个作用域范围:

  1. 有权访问自己的作用域:自己的大括号中定义的变量
  2. 有权访问外部函数的变量
  3. 有权访问全局变量

创建函数的父级上下文的数据是保存在函数的内部属性 [[Scope]] 中的。如果对 [[Scope]] 和作用域链的知识完全理解了的话,那对闭包也就完全理解了。

根据函数创建的算法,我们看到 在 ECMAScript 中,所有的函数都是闭包,因为它们都是在创建的时候就保存了上层上下文的作用域链(除开异常的情况) (不管这个函数后续是否会激活 —— [[Scope]] 在函数创建的时候就有了)。

所有对象都引用一个[[Scope]]:
这里还要注意的是:在 ECMAScript 中,同一个父上下文中创建的闭包是共用一个 [[Scope]] 属性的。也就是说,某个闭包对其中 [[Scope]] 的变量做修改会影响到其他闭包对其变量的读取。
这就是说:所有的内部函数都共享同一个父作用域。

因为作用域链,使得所有的函数都是闭包(与函数类型无关: 匿名函数,FE函数声明,NFE命名函数表达式,FD函数表达式都是闭包)。

这里只有一类函数除外,那就是通过 Function 构造器创建的函数,因为其[[Scope]]只包含全局对象。
为了更好的澄清该问题,我们对 ECMAScript 中的闭包给出2个正确的版本定义:

ECMAScript中,闭包指的是:

  1. 从理论角度:所有的函数。因为它们都在创建的时候就将上层上下文的数据保存起来了。哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问自由变量,这个时候使用最外层的作用域。
  2. 从实践角度:以下函数才算是闭包:
    1. 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
    2. 在代码中引用了自由变量

闭包是如何产生的

以下过程引用自极客时间的《浏览器工作原理与实践》的第12章
先看以下代码:

function foo() {
    var myName = "极客时间"
    let test1 = 1
    const test2 = 2
    var innerBar = { 
        setName:function(newName){
            myName = newName
        },
        getName:function(){
            console.log(test1)
            return myName
        }
    }
    return innerBar
}
var bar = foo()
bar.setName("极客邦")
bar.getName()
console.log(bar.getName())

当 foo 函数的执行上下文销毁时,由于 foo 函数产生了闭包,所以变量 myName 和 test1 并没有被销毁,而是保存在内存中,那么应该如何解释这个现象呢?
要解释这个现象,我们就得站在内存模型的角度来分析这段代码的执行流程:

  1. 当 JavaScript 引擎执行到 foo 函数时,首先会编译,并创建一个空执行上下文。
  2. 在编译过程中,遇到内部函数 setName,JavaScript 引擎还要对内部函数做一次快速的词法扫描,发现该内部函数引用了 foo 函数中的 myName 变量,由于是内部函数引用了外部函数的变量,所以 JavaScript 引擎判断这是一个闭包,于是在堆空间创建换一个“closure(foo)”的对象(这是一个内部对象,JavaScript 是无法访问的),用来保存 myName 变量。
  3. 接着继续扫描到 getName 方法时,发现该函数内部还引用变量 test1,于是 JavaScript 引擎又将 test1 添加到“closure(foo)”对象中。这时候堆中的“closure(foo)”对象中就包含了 myName 和 test1 两个变量了。
  4. 由于 test2 并没有被内部函数引用,所以 test2 依然保存在调用栈中。

通过上面的分析,我们可以画出执行到 foo 函数中“return innerBar”语句时的调用栈状态,如下图所示:

image

从上图你可以清晰地看出,当执行到 foo 函数时,闭包就产生了;当 foo 函数执行结束之后,返回的 getName 和 setName 方法都引用“clourse(foo)”对象,所以即使 foo 函数退出了,“clourse(foo)”依然被其内部的 getName 和 setName 方法引用。所以在下次调用bar.setName或者bar.getName时,创建的执行上下文中就包含了“clourse(foo)”。

总的来说,产生闭包的核心有两步:第一步是需要预扫描内部函数;第二步是把内部函数引用的外部变量保存到堆中

闭包的作用

  1. 能够访问函数定义时所在的词法作用域(阻止其被回收)。

  2. 私有化变量

function base() {
  let x = 10 // 私有变量
  return {
    getX: function() {
      return x
    },
  }
}

let obj = base()
obj.getX() // ==> 10
  1. 模拟块级作用域
var a = []
for (var i = 0; i < 10; i++) {
  a[i] = (function(j) {
    return function() {
      console.log(j)
    }
  })(i)
}

a[6]() // ==> 6
  1. 创建模块
function coolModule() {
  let name = "Mike"
  let age = 20
  function sayName() {
    console.log(name)
  }
  function sayAge() {
    console.log(age)
  }
  return {
    sayName,
    sayAge,
  }
}

let info = coolModule()
info.sayName() // ==> Mike

模块模式具有两个必备的条件(来自《你不知道的JavaScript》):

  • 必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块实例)
  • 封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态。

参考文章

@swiftwind0405 swiftwind0405 changed the title 闭包 【Day03】闭包 Feb 24, 2020
@swiftwind0405 swiftwind0405 changed the title 【Day03】闭包 【Day04】闭包 Feb 27, 2020
@swiftwind0405 swiftwind0405 changed the title 【Day04】闭包 【JavaScript】闭包 Apr 29, 2020
@swiftwind0405 swiftwind0405 changed the title 【JavaScript】闭包 JavaScript 中的闭包 Oct 22, 2020
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