JS入门必读:JavaScript中的闭包

205 阅读5分钟

闭包

前言

前面我们了解到变量为什么会存在声明提升,也就是 js 的执行机制。现在,这一篇文章带你攻克号称最难理解的闭包。

词法环境

首先,让我们来看一下这个代码:

function varTest() {
    var x = 1;
    if (true) {
        var x = 2;
        console.log(x);
    }
    console.log(x);
}

通过上一篇文章的学习(JS入门必读:JavaScript 执行机制),我们可以画出他的编译过程:输出为 2 2。

image.png

那修改一下 if 语句,输出会有变化吗?

function varTest() {
    var x = 1;
    if (true) {
        let x = 2;   //var => let
        console.log(x);
    }
    console.log(x);
}

输出会变成:2 1。 为什么输出会改变呢?

首先我们要知道,let 和 const 定义的变量会被放进词法环境中,词法环境会维护一个新的栈,每一个块级作用域都会形成一个块级上下文,入词法环境的栈(维护块级作用域之间的变量不相互冲突)

让我们再来看看这个函数的执行上下文:

image.png

那我们可能会有这样一个问题,第二个 console.log 为什么会找词法环境中的 x 而不去找变量环境中的?

if、while 这种语句里,除非用 let、const 声明,才是块级作用域。

块级作用域里的变量不影响块级作用域外的变量。

所以第二个 console.log 放在了块级作用域里,肯定是去找词法环境中的 x

========================================================================

我们再来看一个复杂一点的代码,能不能画出这个函数的执行上下文。

function foo() {
    var a = 1
    let b = 2
    {                    //这里 花括号{} 和 let 会形成块级作用域,但是 var 不会
      let b = 3
      var c = 4
      let d = 5
      console.log(a);   //1
      console.log(b);   //3
    }
    console.log(b);    //2(块级上下文出栈)
    console.log(c);    //4
    console.log(d);    //报错no defined
  }
  foo()

来看看 foo 执行上下文

9a42ea8c3adf26273713301a75cc9750.png

v8 的规则是,查找变量先查找自己的词法环境,在词法环境的规则是从上往下,没有再从词法环境来到变量环境查找。

也就是这种顺序:

69c4db150925896f247232ed0d0e5eba.png

那我们第一个 console.log(a) ,最后是在变量环境中找到,a = 1

console.log(b) ,在词法环境中的最上面的栈中找到,b = 3

第二个 console.log(b) ,因为块级作用域执行完毕,所以( b = 3d = 6 )这个栈销毁,所以我们在词法环境中的唯一一个栈找到 b = 2 ;

console.log(c) 在变量环境中找到,c = 4

console.log(d) ,没有寻找到,d = undefined

作用域链

我们首先看这个代码,打印出来的会是什么?

function bar() {
    console.log(myname);    
}
function foo() {
    var myname = '卢卡'
    bar()
    console.log(myname)
}
var myname = '奈布'
foo()

让我们来画出这个代码的编译执行情况:

image.png

这时候 bar 函数执行,要寻找 mynamebar 执行上下文中找不到,那应该去哪里找呢?这时候我们要知道:

词法作用域:函数定义在了哪个域,这个域就叫该函数的词法作用域。

执行上下文有一个 outer 指针(外部引用),指向该函数的词法作用域,全局的 outer 为null

我们应该去 bar 函数的词法作用域去找,也就是全局执行上下文去找,找到 myname = '奈布'。 所以输出为:

image.png

v8 在查找变量的过程中,顺着执行上下文中的 outer 只想查清一整根链,这种链状关系就叫作用域链

来看看这个代码就懂了:

function main() {
    let con = 3;
    function foo() {
        let con = 4;
        function bar() {
            let con = 5;
            console.log(con)
        }
        bar()
     }
     foo()
}
main()
//作用域链:bar => foo => main =>全局

闭包

1. 闭包的概念?

我们先来看一个代码:

function foo() {
    function bar() {
        var a = 1;
        console.log(b);
    }
    var b = 2
    return bar    //返回函数体
}
const baz = foo() // baz为bar,foo()执行完毕,执行上下文销毁
baz()             //b要去bar的词法作用域找,foo()执行上下文被销毁,留下了闭包

通过我们之前的了解,我们画出的调用栈情况是不是这样子的:

image.png

我们现在就有一个疑问?bar 函数要找 b ,自己的执行上下文中没有,那就去找词法作用域,也就是 foo 的执行上下文,但是 foo 函数执行完毕执行上下文销毁,那这个代码会不会报错?

让我们来看看答案:

image.png

会输出 2 。

这是因为,foo 执行上下文被销毁,但是留下来了一个小区间,这个区间,也就是闭包(closure)。

在 js 中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量。当通过调用一个外部函数返回的内部函数时,即使外部函数调用完了(即销毁了其执行上下文),但是内部函数引用了外部函数中的变量,那么这些变量依然需要保存在内存中,我们把这些变量的集合称为闭包

2. 为什么会有闭包这个概念?

词法作用域的规则 和 函数调用完毕他的执行上下文一定要被销毁 两个规则冲突。

3. 缺点:内存泄漏----调用栈的可用空间变得更少

存在很多个闭包,那么调用栈就会爆栈。

4. 优点:变量私有化,封装模块

//让这个代码实现累加效果,可以把 num 提出来变成全局变量。
//但如果是大型的项目代码,每要实现一个类似累加的功能,都要定义一个全局变量,
//那这个代码的可维护性和可读性将会大打折扣,这时候可以使用闭包,闭包的优点也就是变量私有化。
function add() {
    let num = 0;
    console.log(++num);
}
add()  // 1
add()  // 1
//使用闭包
function add() {
    let num = 0;
    return function () {
        console.log(++num);
    }
}
const res = add()
res()  // 1
res()  // 2

练习

面试问 :这个代码输出什么

function fn() {
    var arr = []
    for (var i = 0; i < 5; i++) {
      arr.push(function() {
        console.log(i)
      })
    }
    return arr
  } 
  
  var funcs = fn()
  for (var j = 0; j < 5; j++) {
    funcs[j]()  //数组中的五个函数依次执行
  }
  // 5 5 5 5 5

面试官会继续问:我想要打印 0 1 2 3 4

  function fn() {
    var arr = []
    for (var i = 0; i < 5; i++) {
        function foo(n) {                // n 不会被销毁,会保留在闭包中,等到将来 fn 的内部函数生效的时候,n 才会被访问。
            arr.push(function() {
                console.log(n)
            })
        }
        foo(i)
    }
    return arr
  } 
  
  var funcs = fn()
  for (var j = 0; j < 5; j++) {
    funcs[j]()  //数组中的五个函数依次执行
  }
  //或者var i改成let i

这个是上个代码的调用栈,这里把循环给省略了。

image.png