实例带你过关闭包[js篇二]

154 阅读4分钟

前言:闭包在前端知识体系中可大可小,在日常编程我们依然与闭包有着紧密的联系,他涉及作用域(链)、执行上下文,内存管理等知识,所以js篇就用实例来讲讲闭包知识。 

先上菜单【闭包】知识点:

1.作用域

在JavaScript中,ES6之前只有函数作用域和全局作用域之分。 这里将略跳过不讲函数作用域和全局作用域(因为太基础了^_^)。

块级作用域和暂时性死区

   ES6增加了块级作用域let 和const 来声明变量。块级作用域,顾名思义,作用域范围限制在代码块中,当然 ES6带来新特性,也同时带来了新概念,比如暂时性死区,

function foo() {   
    console.log(bar)  
     var bar = 2
}
foo() 

这里会输出undefined,原因是变量 bar 在函数内进行了提升

这里用let 声明时

function foo() {   
    console.log(bar)   
    let bar = 2
}
foo() 

这里会报错Uncaught ReferenceError: bar is not defined。因为使用let 或const 声明变量,会针对这个变量形成一个封闭的块级作用域,在这个块级作用域当中,如果在声明变量前访问该变量,就会报 referenceError 错误;如果在声明变量后访问,则可以正常获取变量值。

暂时性死区: 起始于函数开头,终止于相关变量声明的一行.

image.png


需要理解执行上下文的请翻看js篇第一篇 用实例带你走进this、执行上下文世界[js篇一]

函数调用栈其实很好理解,我们在执行一个函数时,如果这个函数又调用了另外一个函数,而这个「另外一个函数」也调用了「另外一个函数」,便形成了一系列的调用栈。如下代码

function foo1() {
    foo2()
}
function foo2() {
    foo3()
}
function foo3() { 
    foo4()
}
function foo4() { 
    console.log('foo4')
}
foo1()

调用关系:foo1foo2foo3foo4。这个过程是 foo1 先入栈,紧接着 foo1 调用 foo2foo2入栈,以此类推,foo3foo4,直到 foo4 执行完 —— foo4 先出栈,foo3 再出栈,接着是 foo2 出栈,最后是 foo1 出栈。这个过程「先进后出」(「后进先出」),因此称为调用栈

image.png

我们从中都可以借助 JavaScript 引擎,清晰地看到错误堆栈信息,也就是函数调用栈关系。


闭包

函数嵌套函数时,内层函数引用了外层函数作用域下的变量,并且内层函数在全局环境下可访问,就形成了闭包。

function counter(){
    var num= 1;
    return function(){
      return  num++;
}
var getNum = counter()
getNum()

这个简单的闭包例子中,

counter
创建了一个变量 num,返回打印 num 值的匿名函数,这个函数引用了变量 num,使得外部可以通过调用 getNum 方法访问到变量 num,因此在 numGenerator 执行完毕后,即相关调用栈出栈后,变量 num 不会消失,仍然有机会被外界访问.

我们知道正常情况下外界是无法访问函数内部变量的,函数执行完之后,上下文即被销毁。但是在(外层)函数中,如果我们返回了另一个函数,且这个返回的函数使用了(外层)函数内的变量,外界因而便能够通过这个返回的函数获取原(外层)函数内部的变量值。这就是闭包的基本原理

实例1

const foo = () => {  
    var arr = []   
    var i   
    for (i = 0; i < 10; i++) {   
        arr[i] = function () {  
         console.log(i)   
       }  
    }   
    return arr[0]
}
foo()()


------------------------------------答案分析---------------------------------
// 10
foo() 执行返回的是 arr[0] ,此时arr[0]函数是
function () {   console.log(i)}
变量i 值为10

实例2

var fn = nullconst foo = () => { 
    var a = 2  
    function innerFoo() {    
        console.log(a) 
    }  
    fn = innerFoo  
}

const bar = () => {  
    fn()
}

foo()
bar()



------------------------------------答案分析---------------------------------
执行到bar()时输出 2 , 根据调用栈的知识,foo 函数执行完毕之后,其执行环境生命周期会结束,所占内存被垃圾收集器释放,上下文消失。
但是通过innerfoo函数赋值给fn,fn是全局变量,这就导致了foo的变量对象a也被保留了下来。
所以函数fn在函数bar内部执行时,依然可以访问这个被保留下来的变量对象,输出结果为 2。

实例3

var fn = nullconst foo = () => {   
    var a = 2  
    function innerFoo() {   
        console.log(c)             
        console.log(a)  
    } 
  fn = innerFoo
}
const bar = () => {  
    var c = 100 
    fn()   
}
foo()
bar()


------------------------------------答案分析---------------------------------
c is not defined。
在 bar 中执行 fn() 时,fn() 已经被复制为 innerFoo,变量 c 并不在其作用域链上,
c 只是 bar 函数的内部变量


总结:

觉得不错的请给个三连哦👍,接下我会更新大家感兴趣的话题,让我们一起进步。