Skip to content
On this page

重学前端 --- 闭包&执行上下文

在代码执行前,会先进行词法分析、跟预编译,最后再解析执 行词法分析 --> 预编译 --> 解析执行

img

词法分析

img

  • 当我们打开一个网页时,浏览器都会去请求对应的 HTML 文件。虽然平时我们写代码时都 会分为 JS、CSS、HTML 文件,也就是字符串,但是计算机硬件是不理解这些字符串的, 所以在网络中传输的内容其实都是 0 和 1 这些字节数据
  • 当浏览器接收到这些字节数据以后,它会将这些字节数据转换为字符串
  • 将语法(字符串)分析成一个最小单位,也叫 token, 将这个 token 解析翻译成 AST。 检查是否有低级错误,比如多了一个括号,少了一个分号等等。

预编译

预编译:准备各种需要运行的环境。这也是我们要弄明白作用域的根本步骤。这个阶段它做 了如下的工作:

  • 创建 AO(GO)对象
  • 找形参和变量声明,将形参和变量名作为 AO(GO)属性名,此时值为 undefind
  • 实参和形参合并
  • 在函数体里面找函数声明,值赋予函数体。

预编译可以分为:

  • 全局预编译
    • 创建 GO 对象 (Global Object)
    • 找全局的变量声明,将变量声明作为 GO 的属性名,值为 undefined (var)
    • 在全局找函数声明,将函数名作为 GO 对象的属性名,值赋予函数体 (function)
  • 局部预编译
    • 创建 AO 对象 (Activation Object)
    • 找形参和变量声明,将变量声明和形参作为 AO 的属性名,值为 undefined
    • 将实参和形参统一
    • 在函数体内找函数声明,将函数名作为 AO 对象的属性名,值赋予函数体

AO/GO 是指活动对象或者说是执行期上下文,执行期!!!而非执行上下文

  • 根据预编译的特点,也能够理解为什么 var 跟 function 会有变量提升,var 出来的变 量被赋值成 undefined,函数则赋予函数体
  • let 跟 const 是 es6 的新特性,在执行上下文的创建阶段并不会被赋值。

作用域

  • 在函数创建(定义)时,就创建了作用域
  • 作用域就是代码的执行环境活动对象/执行期上下文的集合就是作用域。预编 译创建的正是活动对象
  • 作用域分为:
    • 全局作用域
    • 局部作用域(函数作用域)

实例看问题:

function a(){
    function b(){
        var bb = 234
        aa = 0
    }
    var aa = 123
    b()
    console.log(aa) // 输出:0
}
var global = 100
a()
function c(){
	var b = 1;
	function a(){
		console.log("111,"+ b );
		var b = 2;
		console.log("222,"+ b );
	}
	a();
	console.log("333,"+ b );
}
c();

输出:111,undefined 222,2 333,1

image.png

分析:

  • 对于全局而言,它的全局作用域就是全局活动对象
  • 对于 a 函数而言,它的作用域就是 a 的活动对象 + 全局活动对象
  • 对于 b 函数而言,它的作用域就是 a 的活动对象 + b 的活动对象 + 全局活动对象

所以当在 b 函数中修改在 a 函数定义的 aa 的值时,它也就是修改了他们共同的引用,所 以在 a 函数中打印会变化。所以作用域其实就是活动对象的集合。

作用域链

作用域链其实就是将活动对象串联到一起的一个集合概念,当访问一个变量时,会在该 变量所在的作用域中去寻找,逐级往上。

  • 作用域链在 JS 内部中是以数组的形式存储的,数组的第一个索引对应的是函数本身的 执行上下文,也就是当前执行的代码所在环境的变量对象,下一个索引对应的空间存储 的是该对象的外部执行环境,依次类推,一直到全局执行环境
  • 当在 Javascript 中使用一个变量的时候,首先 Javascript 引擎会尝试在当前作用域下 去寻找该变量,如果没找到,再到它的上层作用域寻找,以此类推直到找到该变量或是已 经到了全局作用域,如果在全局作用域里仍然找不到该变量,它就会直接报错。
  • 每一个函数都有自己的一个作用域链,访问这个函数的变量时,遵循这个函数产生的作用 域链去寻找。变量的查找一定是遵循作用域链的向上搜索

执行上下文

JS 代码在执行前,JS 引擎总要做一番准备工作,这份工作其实就是创建对应的执行上下 文;
执行上下文有且只有三类,全局执行上下文,函数上下文,与 eval 上下文

创建执行上下文的方式

  • 全局执行上下文(只有一个,由浏览器创建,能够使用 this 访问它),我们通过 var 创建的全局对象,都可以通过 window 直接访问
  • 函数执行上下文(能有无数个,当函数被调用时创建,函数执行完销毁)
  • eval

执行上下文的栈

  • 调用栈: LIFO(Last In First Out 后进先出,也就是先进后出)
  • 栈底永远有一个全局执行上下文(window),它在浏览器关闭时出栈
  • 同步执行,只有栈顶的上下文在执行,其余在等待
function f1() {
    f2();
    console.log(1);
};
function f2() {
    f3();
    console.log(2);
};
function f3() {
    console.log(3);
};

f1();//3 2 1

创建阶段跟执行阶段

创建阶段

ExecutionContext = {
    // 确定this的值
    ThisBinding = <this value>,
    // 创建词法环境组件
    LexicalEnvironment = {},
    // 创建变量环境组件
    VariableEnvironment = {},
};
  • 绑定 this
  • 创建词法环境: 存储函数声明与 let、const 声明的变量, 包括环境记录与外部环境引 入记录
  • 创建变量环境: 仅仅存储 var 声明的变量包括环境记录与外部环境引入记录
  • 在执行上下文创建阶段,函数声明与 var 声明的变量在创建阶段已经被赋予了一个值 ,var 声明被设置为了 undefined,函数被设置为了自身函数,而 let const 被设置为 未初始化。这就是为什么 var 跟函数有变量提升,而 let,const 没有的原因

执行阶段

代码执行时根据之前的环境记录对应赋值,比如早期 var 在创建阶段为 undefined,如果 有值就对应赋值,像 let、const 值为未初始化,如果有值就赋值,无值则赋值 undefined。