JavaScript 执行机制
JavaScript 代码执行先编译,后执行。首先在编译阶段,会生成执行上下文和可执行代码两部分,然后是执行阶段,按照顺序一行一行执行代码, 并且查找变量或者函数。

执行上下文
执行上下文(Execution context)是 JavaScript 执行一段代码时的运行环境。确定执行时的变量、函数、this等。
执行上下文为三种情况:
- 全局执行上下文,在页面生存周期内唯一;
- 函数执行上下文,在调用函数是创建;
- eval 函数执行上下文。
调用栈
调用栈是用来管理执行上下文的栈。栈的特点是后进先出。调用时有大小的,入栈的执行上下文超过一定的数目会导致栈溢出。
调用栈的入栈出栈过程:
1. 创建全局执行上下文,压入调用栈的栈底,然后执行全局代码;
2. 调用函数创建函数执行上下文,压入调用栈,然后执行函数内代码;
3. 若函数内还存在函数调用,则重复步骤2,若函数执行结束,该函数的执行上下文栈顶弹出;
4. 重复步骤2、步骤3。
变量环境
在执行上下文中存在一个变量环境的对象。用于存放变量提升的内容。
变量提升
函数声明和var关键字创建的变量声明会在编译阶段提升,且变量会设置默认值undefined。这些内容存放在变量环境对象中。
var test = 'test';
上面test变量分为声明和赋值两部分(函数只有声明过程)。在编译阶段,声明且设置默认值undefined存放在变量环境中;在执行阶段,进行赋值操作。

当提升过程中,存在同名的变量/函数处理原则:
- 如果都是变量提升,始终都会设置默认值
undefined; - 如果都是函数提升,后声明覆盖先声明的;
- 两者皆有,则会忽略变量声明。
// 同名提升,忽略变量声明
console.log(test)// ƒ test () {}
var test = 'test';
function test () {}
console.log(test)// test
词法环境
作用域
作用域指的是代码中定义变量的区域,控制着变量和函数的可见性和生命周期。作用域分为:
- 全局作用域,在代码中任何地方都可以访问;
- 函数作用域,只能在函数内部访问,函数执行结束,内部声明变量和函数会被销毁;
- 块级作用域(let/const),在大括号中的变量拥有单独的作用域(ES6 新增)。
变量提升存在的问题
变量提升存在的问题主要是:
-
变量覆盖。变量容易被不经意覆盖,导致程序不按预期执行。
var name = '变量1'; function func(){ console.log(name); if (false) { var name = '变量2'; } } func();// undefined上面的代码中,若需要使用外部的
name值则需要修改if代码块中的变量名。 -
变量污染。在代码块中变量,会一直存在直到函数结束。
var i = 1; for(var i = 0; i < 10; i++){} console.log(i);// 10
let/const
在 ES6 中引入了let/const关键字实现了块级作用域。let和const声明的变量区别在于前者声明的变量值可修改,后者声明的变量值不可修改(若变量为对象,可改变属性)。
let test1 = 'test1';
const test2 = 'test2';
test1 = 'test11';
test2 = 'test22'; //报错 Assignment to constant variable.
块级作用域解决了变量提升带来的问题。具体见如下代码:
let name = '变量1';
function func(){
console.log(name);
if (false) {
let name = '变量2';
}
}
func();// 变量1
let i = 1;
for(let i = 0; i < 10; i++){}
console.log(i);// 1
let/const使用的一些特殊之处:
-
存在暂时性死区,变量不会提升;
-
在浏览器中,顶层对象是
window。在 ES6 之前,顶层对象的属性和全局变量是等价的,ES6 做了改变,let、const和class关键字声明的全局变量不属于顶层对象的属性了;var a = 'a'; window.a // a let b = 'b'; window.b // undefined -
在函数顶层的词法环境变量,在编译阶段变量声明会存放在词法环境对象中;
-
在函数内部的块级作用域,会在执行时追加到词法环境对象中;
-
词法环境内部是一个栈结构,栈底是函数最外层/全局的变量;当进入一个块级作用域后,会将该块级作用域下变量压入栈顶,块级作用域执行完成从栈顶弹出。
outer
每个执行上下文的变量环境中,包含了一个外部引用指向外部的执行上下文,这个外部引用就是 outer。在变量查找的过程中,通过outer进行外部作用域查找。
this
全局执行上下文中的 this
全局执行上下文中的 this 指向 window 对象。
函数执行上下文中的 this
-
默认绑定。在非严格模式下,
this指向 window 对象;严格模式下,this绑定为this。// 非严格模式 function aa(){ console.log(this) } aa();// Window // 严格模式 function bb(){ 'use strict' console.log(this) } bb();// undefined -
隐式绑定。
this指向最后调用它的对象。var name = 'test1'; let obj = { name: 'test2', func(){ console.log(this.name); } } let obj1 = obj.func; obj1(); // tes1 obj.func(); // test2 -
显示绑定。通过
call()、apply()和bind()方法指定this的绑定对象。call、apply直接执行函数;bind创建一个新函数,需要手动调用执行;- 接受的第一个参数是空或者
null、undefined时: 非严格模式下,this指向window对象; 严格模式下,若参数为空或者undefined时,this绑定到undefined;参数为null时,this绑定到null; call、bind接受多个参数,apply接受一个数组参数。
-
new 绑定。
this指向实例对象。 -
箭头函数。
this由外层作用域决定,且指向函数定义时的this而非执行时。
可执行代码
JavaScript 引擎将声明以外的代码编译成子节点。
变量查找
作用域链
变量查找不是按照调用栈的顺序,从栈顶进行查找;而是在当前执行上下文中未找到时,在outer所指的执行上下文中查找,形成了作用域链。
作用域链有由词法作用域决定。词法作用域是静态作用域,由代码中函数的声明的位置决定,和函数调用无关。
闭包
通过chrome DevTools可以看出,当内部函数使用了外部函数中的变量时,就产生了闭包。(也可以理解成,在调用外部函数时,返回了一个内部函数。当内部函数中使用了外部函数的变量时,就产生了闭包。)

闭包回收:当闭包作为全局变量时,会一直存在;作为局部变量时,函数销毁后,会被垃圾回收器回收。
查找规则
- 在当前作用域中查找:
- 先从词法环境中进行查找:按照栈顶至栈底顺序查找;
- 然后在变量环境中查找。
- 执行上下文之间查找:
- 在当前作用域未查到,通过作用域链找到外部的作用域,进行查找;
- 重复步骤1,直到查找到全局作用域。
总结
主要是对 JavaScript 中执行上下文相关内容进行了一些总结,执行机制的其他的部分介绍比较粗略,后续再多多了解。