JS 引擎会在执行所有代码之前,先在堆内存中创建一个全局对象(Global Object、GO),包含 String、Math、Date、parseInt()
等属性和方法。所有作用域都可以访问这个全局对象。
在浏览器中 Global Object 就是 Window 对象。
执行上下文(执行环境、Execution Context):
执行上下文是当前代码的执行环境。每当 JS 代码在运行的时候,它都是在执行上下文中运行。
执行上下文的类型:
JS 中有三种执行上下文的类型:
-
全局执行上下文:全局执行上下文是最外围的执行上下文。在 web 浏览器中,全局执行上下文被认为是 window 对象,因此所有的全局变量和全局函数都是作为 window 对象的属性和方法创建的。一个程序中只会有一个全局执行上下文。
在执行全局的代码前,JS 引擎会创建一个全局执行上下文;程序执行完毕或者网页被关闭后,全局执行上下文环境被销毁。
-
函数执行上下文:每个函数都有自己的执行上下文。函数执行上下文可以有无数个。
每当一个函数被调用时, 都会为该函数创建一个新的函数执行上下文;当函数执行完毕后,该函数执行上下文被销毁。
-
eval 函数执行上下文:eval 函数内部的代码也有属于它自己的执行上下文,很少用到。
执行上下文的生命周期:
执行上下文有创建、执行、回收三个生命周期。
创建阶段:
创建阶段会做三件事:创建变量对象、创建作用域链、确定 this 的指向。
因此,每个执行上下文,都有三个重要的属性:变量对象、作用域链、this。
-
创建变量对象。
变量对象(Variable object,VO):每个执行上下文都会关联一个变量对象,当前执行上下文中定义的所有变量声明、函数声明、函数的形参都会被添加到这个对象中。这就是函数声明提升和变量提升的原因
变量对象包括:- 变量声明:由变量的名称和其对应的值组成变量对象的一个属性,此时其对应的值是 undefined。如果变量的名称和已经声明的函数的名称或者函数的形参相同,变量声明不会干扰到这些已经存在的属性。
- 函数声明:由函数的名称和其对应的值组成变量对象的一个属性,此时其对应的值是函数对象本身,在执行上下文的创建阶段,函数就会被创建出来。如果变量对象已经存在相同名称的属性,则完全替换这个属性。
- 函数的形参(如果是函数执行上下文的话):由函数形参的名称和其对应的值组成变量对象的一个属性。如果没有实参的话,属性值为 ubdefined。
活动对象(activation object, AO):活动对象其实就是被激活的变量对象,只有到当进入一个执行上下文中,这个执行上下文的变量对象才会被激活,所以才叫 activation object。
变量对象是规范上的或者说是引擎实现上的,无法在 JavaScript 环境中访问到;只有活动对象上的各种属性才能被访问。function foo(a) { console.log(b) console.log(c) var b = 2 function c() {} var d = function() {} b = 3 } foo(1) // 当进入代码还没执行时,这时候的 AO: AO = { arguments: { 0: 1, length: 1 }, a: 1, b: undefined, c: reference to function c(){}, d: undefined }
-
创建作用域链:当执行函数,进入函数执行上下文,创建 AO 后,就会将活动对象添加到作用域链的最前端。
函数作用域是在函数定义的时候确定的,这是因为函数有一个内部属性
[[scope]]
,当函数定义的时候,就会保存所有父变量对象到其中,[[scope]]
就是所有父变量对象的层级链。当执行函数,进入函数执行上下文,创建 AO 后,就会将活动对象添加到作用域链的最前端。
当代码在一个环境中执行时,会创建变量对象的一个作用域链。作用域链的前端,始终都是当前执行的代码所在环境的变量对象,作用域链中的下一个变量对象来自包含环境,再下一个变量对象则来自下一个包含环境,一直延续到全局执行环境,全局执行环境的变量对象始终都是作用域链中的最后一个对象。
当查找变量时,会先从当前作用域的执行上下文的变量对象中查找,如果没有找到,就会从父级作用域执行上下文的变量对象中查找,一直找到全局作用域的执行上下文的变量对象。
作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。function out() { console.log(out) function inner() { console.log(inner) } } out()
-
确定 this 的指向。
执行阶段:
从上到下依次执行代码。遇到变量声明的话,为变量赋值;遇到函数声明的话,由于函数在执行上下文创建阶段已经被创建,此时会直接跳过。
// 当代码执行后,这时候的 AO:
AO = {
arguments: {
0: 1,
length: 1
},
a: 1,
b: 3,
c: reference to function c(){},
d: reference to FunctionExpression "d"
}
// 因此,代码执行顺序是这样的:导致了变量提升和函数提升
function foo(a) {
var b
unction c() {}
var d
console.log(b)
console.log(c)
b = 2
function c() {}
d = function() {}
b = 3
}
回收阶段:
执行上下文出栈,被垃圾回收机制进行回收。
执行上下文栈(Execution Context Stack、ECS):
JS 引擎内部有一个执行上下文栈,它是用于执行代码的调用栈,被用来存储和管理代码运行时创建的所有执行上下文,拥有 LIFO(后进先出)的数据结构。
当 JS 引擎执行 JS 脚本时,它首先会创建一个全局的执行上下文并且压入执行上下文栈;每当 JS 引擎遇到一个函数调用,它会为该函数创建一个新的执行上下文并压入执行上下文栈的顶部;当该函数执行结束时,执行上下文从执行上下文栈中弹出,控制流程到达执行上下文栈的下一个执行上下文。
全局执行上下文总是在栈的底部;当前运行的总是栈顶的那个执行上下文。
let str = 'javascript'
function foo() {
console.log('foo')
bar()
}
function bar() {
console.log('bar')
}
foo()
- 当上述代码在浏览器中运行时,JS 引擎首先会创建一个全局执行上下文并把它压入执行上下文栈。
- 当遇到
foo()
函数调用时, JS 引擎创建了一个 foo 函数的执行上下文并把它压入到执行上下文栈的顶部。 - 当从
foo()
函数内部调用bar()
函数时,JS 引擎创建了一个 bar 函数的执行上下文并把它压入到执行上下文栈的顶部。 - 当
bar()
函数执行完毕,它的执行上下文会从执行上下栈中弹出,控制流程到达下一个执行上下文,即foo()
函数的执行上下文。 - 当
foo()
函数执行完毕,它的执行上下文从执行上下栈中弹出,控制流程到达全局执行上下文。 - 一旦所有代码执行完成,JS 引擎就从执行上下文栈中移除全局执行上下文。
作用域 Scope:
作用域就是一段代码所在的区域。分为全局作用域、函数作用域和块级作用域(ES6 中新增)。作用域的作用是隔离变量,不同作用域下同名变量不会有冲突。
JS 采用词法作用域,也就是静态作用域。函数的作用域在函数定义的时候就决定了。
与词法作用域相对的是动态作用域,函数的作用域是在函数调用的时候才决定的。
ES6 之前,if 语句、for 语句等的大括号没有封闭作用域的功能,都是全局作用域。
if (true) { var box=’blue’ } console.log(box) // blue
var a = 1
function out(){
var a = 2
inner()
}
function inner(){
console.log(a)
}
out() // 1
作用域链:
作用域链:由多个执行上下文的变量对象构成的链表就叫做作用域链,它的方向是从下而上的。查找变量时就是沿着作用域链来查找的。
查找一个变量的规则:在当前作用域下的执行上下文的变量对象中查找该变量,如果有直接返回,否则再在上一级作用域的执行上下文的变量对象中查找,以此类推,直到全局作用域,如果还找不到抛出异常。
内部环境可以通过作用域链访问所有的外部环境,但是外部环境不能访问内部环境中的任何变量和函数。
var a = 1
function fn1() {
var b = 2
function fn2() {
var c = 3
console.log(c) // 3
console.log(b) // 2
console.log(a) // 1
console.log(d) // 报错
}
fn2()
}
fn1()
延长作用域链:
有些语句可以在作用域链的前端临时增加一个变量对象,作用域链就会得到加长,该变量对象会在代码执行后被移出。对 with 语句来说,会将指定的对象添加到作用域链的最前端;对 catch 语句来说,被抛出的错误对象会创建一个新的变量对象加到作用域的最前端。
作用域与执行上下文的联系与区别:
上下文栈的数量是 n + 1:n 是调用几次函数, 1 是全局执行上下文。
作用域的数量也是 n + 1:n 是定义几次函数,1 是全局作用域。
var x = 10
function fn1() {
console.log(x)
}
function fn2(f) {
var x = 20
f() // 打印输出 10。有三个作用域:全局作用域、fn1 函数作用域、 fn2 函数作用域,作用域在函数定义时就已经确定好不会再变了。因此 fn1() 的上级作用域是全局作用域
}
fn2(fn1)
联系:执行上下文是从属于所在的作用域。
区别:
-
作用域是静态的,在编写代码的时候就确定了;执行上下文是动态的,在调用执行的时候才确定。
JS 代码的整个执行过程,分为两个阶段,代码编译阶段与代码执行阶段。
编译阶段由编译器完成,将代码翻译成可执行代码,这个阶段作用域规则会确定。
执行阶段由引擎完成,主要任务是执行可执行代码,执行上下文在这个阶段创建。 -
全局作用域之外,每个函数都会创建自己的作用域,只要函数定义好了就一直存在,且不会再变化;全局执行上下文是在全局作用域确定之后、JS 代码马上执行之前创建的,函数执行上下文是在调用函数时、函数体代码执行之前创建的。
当编写代码的时候,确定全局作用域和函数作用域 —> 当执行代码的时候,动态创建和销毁全局执行上下文和函数执行上下文。
当创建执行上下文的时候,会创建一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中 --> 会创建变量对象的一个作用域链 —> 当查找变量时,会先从当前作用域的执行上下文的变量对象中查找,如果没有找到,就会从父级作用域执行上下文的变量对象中查找,一直找到全局作用域的执行上下文的变量对象。