JS代码执行完整过程?执行上下文?词法作用域?作用域链?变量&活动对象?

JavaScript代码的执行过程

图片

(1)编译阶段由编译器完成,将代码翻译成可执行代码,这个阶段作用域规则会确定,编译阶段经历以下三个步骤:

分词/词法分析:将由字符组成的字符串分解成(对编程语言来说)有意义的代码块,这些代码块被称为词法单元token);
解析/语法分析:将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树——“抽象语法树”(AST);
代码生成:将AST转换为可执行代码的过程被称为代码生成。

(2)执行阶段由引擎完成,主要任务是执行可执行代码,**执行上下文****在这个阶段创建**。

图片

JavaScript代码执行过程中的“演员”:

  • 编译器:负责语法分析及代码生成等脏活累活;
  • 引擎:从头到尾负责整个JavaScript程序编译及执行过程;
  • 作用域:负责收集并维护由所有声明的标识符变量组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限

预编译步骤

执行前先扫描一下整体语法语句,如果有语法错误,那么直接报错,程序停止执行,没有错误的话,开始逐行解释执行。

局部预编译的 4 个步骤:

  1. 创建 AO 对象(Activation Object)执行期上下文。
  2. 找形参和变量声明,作为 AO 对象的属性,值暂时给个 undefined
  3. 将实参值和形参统一。
  4. 在函数体里面找函数声明,值赋予函数体。

举个栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function fn(a) {
console.log(a);
// 变量声明+变量赋值(只提升变量声明,不提升变量赋值)
var a = 123;
console.log(a);
// 函数声明
function a() {}
console.log(a);
// 函数表达式
var b = function () {};
console.log(b);
// 函数
function d() {}
}
//调用函数
fn(1);

1.预编译第 1 步:创建 AO 对象

1
2
AO{
}

2.预编译第 2 步:找形参和变量声明,写入 AO 对象的属性,值暂时给个 undefined

1
2
3
4
AO{
a : undefined,
b : undefined
}

3.预编译第 3 步:将实参值和形参统一

1
2
3
4
AO{
a : 1,
b : function(){...}
}

4.预编译第 4 步:在函数体里面找函数声明,写入 AO 对象方法。

1
2
3
4
5
AO{
a : function a(){...},
b : undefined,
d : function d(){...}
}

全局预编译的 3 个步骤:

创建 GO 对象(Global Object)全局对象。

找变量声明,将变量名作为 GO 属性名,值为 undefined

查找函数声明,作为 GO 属性,值赋予函数体


执行上下文

  • 当 js 引擎第一次遇到要执行的代码的时候,首先会创建一个全局的执行上下文并压入当前执行栈,执行栈是一种拥有后进先出的数据结构的栈
  • 此后,每当 JS 引擎要调用一个函数之前,就会进行局部预编译,创建一个新的执行上下文并压入栈顶
  • 每当栈顶的执行上下文执行完毕,JS 引擎会将其从栈中弹出,控制流程到达下一个上下文。

对于每一个执行上下文都含有三个重要属性:变量对象,作用域链,this

三种执行上下文类型

  • 全局上下文
  • 函数上下文
  • eval 和 with 上下文

作用域概述:

  • 全局作用域(Global Scope)
    • 最外层函数和在最外层函数外面定义的变量拥有全局作用域
    • 所有末声明直接赋值的变量自动声明为拥有全局作用域,likea = 10
    • 所有 window 对象的属性拥有全局作用域。
  • 局部作用域(Local Scope), 只有局部代码才能访问到的作用域。
    • 函数作用域
    • 块级作用域
  • 欺骗作用域

    或者可分为

词法作用域和动态作用域

词法作用域由编译时函数声明的位置来确定,而动态作用域由执行时的位置来确定


欺骗作用域(已废弃不用)

  • eal()和 with()方法
  • 词法作用域完全由写代码期间函数声明的位置来定义。怎么样才能在运行时来修改(也就是欺骗)词法作用域
  • 由于会导致性能问题已经被废弃 * 因为 js 解释器编译阶段会对性能进行优化,而使用 eval 或者 with 会破坏解释器对作用域的管理规则,解释器怕出问题就不会再优化,所以性能下降

    eval

eval 方法会将它的参数(str)作为 JS 代码来执行,该方法内的变量的作用域是不一定的

  • eval 方法若写在函数内,则它是函数作用域;
  • 写在全局作用域下, 它是全局作用域

    要在函数内部使 eval 中变量拥有全局作用域的两个方法

  1. 使用window.eval(),但需要考虑浏览器兼容性,在 IE9 之前,它无效,还是局部
  2. eval赋值给一个全局变量,使之具有全局作用域,然后在函数中使用该变量

    with

with 可以接受一个对象作为参数,并凭空创建了一个全新的词法作用域,但这个块内部正常的 var 声明并不会被限制在这个块的作用域中,而是被添加到 with 所处的函数作用域中。


块级作用域

let、const 声明的变量拥有块级作用域

它们只在相应的**代码块(花括弧内)**中有效,在外部访问不到


作用域链:

概述

  • 作用域链:根据名称查找变量(标识符名称)的一套规则。
  • 作用域链的用途是保证执行环境有序访问它有权访问的变量和函数

    作用域链创建和变量查找过程

  • JS 引擎预编译到函数声明时,在函数对象内部生成一个只能由 JS 引擎访问到的属性[[scope]],该属性保存该函数的作用域链,作用域链的头部存储这个函数生成的执行上下文AO,尾部存储 GO
  • 这个作用域链中记录了此函数内部所能访问到的所有数据。
  • 其中链的**头部****是当前函数的活动对象**(AO),如果存在多个函数嵌套,接下来指向的是当前函数上层函数的活动对象(AO), 依次类推;
  • 无论存不存在函数嵌套,作用域链**尾部****都是全局对象**。
  • 当访问一个数据时,会沿着作用域链头部往后找,直到找到全局对象,如果都没有,将报错。
    • 若找到第一个匹配的变量时,就停止查找,被称为遮蔽效应
    • 函数内部作用域在最顶端,证明了函数可以访问外部的变量,而外部无法访问函数内部的变量

变量对象与活动对象:

  • 变量对象:
    • 编译时所创建,只有 JS 引擎能访问.
    • 变量对象包括函数声明时的参数,内部声明的变量以及函数。
  • 活动对象:
    • 还是变量对象本身,只不过是因为处在执行阶段可以被访问所以成了活动对象
  • 二者区别:为同一个对象处于不同程序运行时的两种称谓。(或者是处于执行上下文的不同生命周期)

参考:

JS面向对象?几种继承方式?原型继承? JS常见的内存泄漏?

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×