执行上下文/作用域链/闭包

83 阅读4分钟

程序是怎么执行的

JavaScript 的执行主要分为两个阶段:

  • 代码预编译阶段
    • 前置阶段:
      • 进行变量声明
      • 变量声明进行提升,但是值为 undefined;
      • 非表达式的函数声明进行提升;
  • 代码执行阶段

对于JS来说,一个变量,是如何被赋值和使用的,有什么区别?

var a = 2

  • var a 编译器会先问作用域,是不是已经有了一个a, 在当前的作用域中? 是: 编译器会忽略这个声明,然后继续往下走; 不是:在当前的作用域中,生成一个新的变量,并命名为a;
  • a = 2: 编译器会问作用域,是不是已经有一个a, 在当前的作用域中? 如果是: 赋值为2; 如果不是:去上一层的作用域中去找。

函数提升

test()  //222
function test() {
    console.log(1111)
}
test()  //222
function test(){
    console.log(22222)
}

执行上下文

  • 全局执行上下文 — 这是默认或者说基础的上下文,任何不在函数内部的代码都在全局上下文中。它会执行两件事:创建一个全局的 window 对象(浏览器的情况下),并且设置 this 的值等于这个全局对象。一个程序中只会有一个全局执行上下文。

  • 函数执行上下文 — 每当一个函数被调用时, 都会为该函数创建一个新的上下文。每个函数都有它自己的执行上下文,不过是在函数被调用时创建的。函数上下文可以有任意多个。每当一个新的执行上下文被创建,它会按定义的顺序(将在后文讨论)执行一系列步骤。

  • Eval 函数执行上下文 — 执行在 eval 函数内部的代码也会有它属于自己的执行上下文,但由于 JavaScript 开发者并不经常使用 eval,所以在这里我不会讨论它。

执行栈

let a = 'Hello World!'; 
function first() {
console.log('Inside first function');
second();
console.log('Again inside first function');
} 

function second() { 
console.log('Inside second function'); 
}

first();
console.log('Inside Global Execution Context');

image.png JavaScript 引擎创建了一个全局执行上下文并把它压入当前执行栈。当遇到 first() 函数调用时,JavaScript 引擎为该函数创建一个新的执行上下文并把它压入当前执行栈的顶部。

当从 first() 函数内部调用 second() 函数时,JavaScript 引擎为 second() 函数创建了一个新的执行上下文并把它压入当前执行栈的顶部。当 second() 函数执行完毕,它的执行上下文会从当前栈弹出,并且控制流程到达下一个执行上下文,即 first() 函数的执行上下文。

first() 执行完毕,它的执行上下文从栈弹出,控制流程到达全局执行上下文。一旦所有代码执行完毕,JavaScript 引擎从当前栈中移除全局执行上下文。

作用域

作用域,就是根据名称查找变量的规则。

词法作用域/静态作用域

词法作用域就是,定义在词法阶段的作用域。 词法作用域就是你写代码的时候,将变量写在哪里决定的, 因此,当词法分析器处理代码时,会保持作用域不变。

函数作用域

函数作用域就是,属于这个函数的全部变量,都可以在整个函数内使用。

    ```
var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f();
}
checkscope();

打印local scope JavaScript采用的是词法作用域,函数的作用域基于函数创建的位置。

作用域链

当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。

闭包

什么是闭包?

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

简单的闭包

function test(){
let data = 10 ;
    return function(){
        return data
    }
}

let data2 = test()()  //10

闭包的使用场景?

  • 当我使用FP的时候,基本上就有闭包了。
  • 当一个函数的执行,和上下文相关的时候,基本上就有闭包了。
function memorize(fn) {
    const deps = {
        '1|2': 3,
        '1|3': 4

    };
    return function(...rest) {
        const key = rest.join('|');
        dep[key] || (dep[key] = fn(...rest));
        return dep[key];
    }
}

const mAdd = memorize(add);

[IIFE]立即执行函数

(function foo(){
    var a = 10; 
    console.log(a); 
    }
)();
为什么要使用IIFE

如果只是为了立即执行一个函数,显然IIFE所带来的好处有限。实际上,IIFE的出现是为了弥补JS在scope方面的缺陷:JS只有全局作用域(global scope)、函数作用域(function scope),从ES6开始才有块级作用域(block scope)

for (var i = 0; i < 5; i++) {
        setTimeout(function() { 
            console.log(new Date, i); 
        }, 1000); 
 }

// 5,5,5,5,5

为什么是5 需要看### 事件机制/Event Loop

for (var i = 0; i < 5; i++) {
    (function(j) { 
        setTimeout(function() {
            console.log(new Date, j);
        }, 1000);
    })(i);
}

// 0,1,2,3,4