透过 JavaScript 执行机制看懂闭包

129 阅读6分钟

一、声明提升

  • v8引擎会先编译代码,再执行。

思考下述代码的执行结果是什么?

showName();
console.log(myname); // 输出:undefined

var myname='zs';
function showName(){
    console.log('函数showNamw 执行了');
}

这段代码的执行结果是先打印函数showNamw 执行了,再输出undefined,其原因主要与JavaScript中的声明提升机制有关。

1、变量声明提升

在js中,使用var声明的变量会被提升到函数或全局作用域的顶部,但只有变量的声明会被提升,而不是变量的初始化赋值操作。也就是说,var myname='zs'会被拆分成两步:

  1. 在代码执行之前,var myname被提升到全局作用域的顶部,此时myname被声明为一个变量,其初始值为undefined
  2. 代码自上而下执行,当执行到console.log(myname)时,myname已经被声明,但此时它还没有被赋值为'zs',故输出undefined

2、函数声明提升

函数声明也会被提升到其所在作用域的顶部。function showName(){...}是一个函数声明,它会被提升到全局作用域的顶部。因此,当代码执行到showName()时,showName函数已经被声明并可用,会正常执行并打印函数showNamw 执行了
由于声明提升的存在,代码的实际执行顺序可以理解为:

var myname; // 变量声明被提升,myname初始值为undefined
function showName(){ // 函数声明被提升
    console.log('函数showNamw 执行了');
}
showName(); // 调用函数,打印“函数showNamw 执行了”
console.log(myname); // 此时myname的值仍为undefined,所以输出undefined
myname='zs'; // 变量赋值操作

二、调用栈

  • js 引擎追踪函数的一个机制,管理一份代码的执行关系,栈底是全局上下文,栈顶是当前执行的上下文。
  • 函数执行时,会创建一个执行上下文,入栈;函数执行完,出栈。
  • 调用栈不能设计的太大,否则,js 引擎在查找上下文时会花费大量时间。
var a=2function add(){
    var b=10;
    return a+b;// 12
}
add();

执行过程:在全局作用域中声明了一个变量a和一个add函数。在add函数局部作用域内声明了一个变量b。函数内部返回ab的和时,当前作用域没有a,可往外层全局作用域中查找。

eb67012b8dbf72db14f2d3cf2090f61.png


三、块级作用域

  • let,const结合{}
function foo(){
    var a=1
    let b=2
    {
        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) // d 没有被声明 // 报错 
}
foo()

执行过程:在foo函数的作用域中声明变量ab。由于var声明的变量具有函数作用域,a在整个函数foo中都有效。let声明的变量具有块级作用域,b只在函数foo的作用域内有效。在块级作用域{}(代码块)中声明变量bcd。同上,var声明的变量c会被提升到函数foo的顶部,因此c在整个函数foo中都有效。bd形成小型的栈结构,只在该块级作用域中有效。块级作用域结束,块级作用域的内容销毁,即出栈。

0edb42e1aeb2e42ec53c417cb9dbf8a.png

总结:

  • let声明的变量只在声明它们的块级作用域内有效。 在块级作用域外访问这些变量会导致错误。
  • var声明的变量具有函数作用域,即使在块级作用域中声明,也会提升到函数的顶部,因此在整个函数中都有效。

四、作用域链

  • 在js中,每个块级作用域都与包含它的外部作用域形成层级关系,构成作用域链。
  • js 引擎在查找变量时,会先从当前作用域查找,如果没有,就会向上一级作用域查找,直到找到为止,如果没有,就会报错。
  • 作用域链的下一级是谁,是由 outer 指针决定的。

例 1:

function bar(){
    console.log(myname) // 输出:hh
} // 指针指向全局(词法作用域)——看声明在哪里
function foo(){
    var myname="xx";
    bar();// 函数调用
}// 指针指向全局
var myname="hh";
foo()

执行环境:

ca5e74c505da9d9684e139f727167cf.png

例 2:

var num =10
function a(){
   var count = 18;
   function b(){
    var num = 20c()
   }
   function c(){
    console.log(num) // 输出:10
   }
   b()
}
a()

执行过程:

8e545290d4b55ebe27032629c564d4e.png


五、闭包

  • 根据作用域链的查找规则,内部函数一定有权利访问外部函数的变量。另外,一个函数执行完毕后它的执行上下文一定会被销毁。那么当函数 A 内部声明了一个函数 B,而函数 B 被拿到 A 的外部执行时。为了保证以上两个规则正常执行,A 函数在执行完毕后会将 B 需要访问的变量保存在一个集合中,并留在调用栈当中,这个集合就是闭包。
  • 缺点:内存泄漏,闭包会一直存在于内存中,不会被垃圾回收机制回收。
function foo(){
    var myname='ll'
    var age=18

    return function bar(){
        console.log(myname); // 输出:ll
    }
}
var baz = foo() // 规则1:foo函数体执行完,foo函数执行上下文销毁
// 规则2-作用域链的查找规则:内部的作用域一定有权力去访问它外部的作用域的变量,即bar函数可访问foo函数
// 二则规则冲突
baz() 

执行过程:

8d6c7918906784de43a8eff058b5d5d.png


小例题:

var arr=[] // function(){} function(){} function(){} function(){} function(){}
for(var i =1; i <= 5; i++){
        arr.push(function(){
            console.log(i); // 输出:6 6 6 6 6 
            })
}
// i=1 func*1 i=2 // 函数体没有触发执行,只是放入栈中
// i=2 func*2 i=3
// i=3 func*3 i=4
// i=4 func*4 i=5
// i=5 func*5 i=6
// i=6 循环终止 此时打印栈中的函数体
// i 是全局作用域的变量,函数体声明在全局,outer指针指向全局

for(var j=0; j<arr.length;j++){
    arr[j]() // arr数组调用,函数触发
}

如何将输出值改为1 2 3 4 5

解1:

  • let声明变量i,形成块级作用域。
var arr=[] 
for(let i =1; i <= 5; i++){
        arr.push(function(){ 
            console.log(i); // 输出:1 2 3 4 5
            })
    }
//i 和 func 形成块级作用域,函数体指针指向该块级作用域,循环五次分别创建五个块级作用域

解2:

  • 形成闭包
var arr=[] 
for(var i =1; i <= 5; i++){
    function foo(j){
        arr.push(function(){  //匿名函数被拿到 foo 外面 // 分别形成五个闭包
            console.log(j); // 输出:1 2 3 4 5
            })
    }
    foo(i)
    // (function(j){
    //     arr.push(function(){  
    //         console.log(j); // 输出:1 2 3 4 5
    //         })
    // })(i) // ()()自执行函数
}