引
我们都知道,JS代码的执行顺序总是与代码先后顺序有所差异,当先抛开异步问题你会发现就算是同步代码,它的执行也与你的预期不一致,比如:
function f1() {
console.log("听风是风");
}
f1(); //echo
function f1() {
console.log("echo");
}
f1(); //echo
按照代码书写顺序,应该先输出”听风是风”,再输出”echo”才对,很遗憾,两次输出均为”echo”;如果我们将上述代码中的函数声明改为函数表达式,结果又不太一样:
var f1 = function () {
console.log("听风是风");
};
f1(); //听风是风
var f1 = function () {
console.log("echo");
};
f1(); //echo
这说明代码在执行前一定发生了某些微妙的变化,JS引擎究竟做了什么呢?这就不得不提JS执行上下文了。
一、什么是执行上下文
执行上下文(Execution Context)是JavaScript代码执行的环境,它定义了变量或函数有权访问的其他数据,决定了它们各自的行为。
1.1 执行上下文的类型
JavaScript中有三种类型的执行上下文:
全局执行上下文(Global Execution Context):
- 浏览器环境中,全局对象是
window - Node.js环境中,全局对象是
global - 全局执行上下文只有一个,在整个应用程序生命周期中存在
- 浏览器环境中,全局对象是
函数执行上下文(Function Execution Context):
- 每当一个函数被调用时,都会创建一个新的函数执行上下文
- 函数执行上下文可以有多个,它们会被推入执行栈中
Eval执行上下文(Eval Execution Context):
- 当
eval()函数执行时创建 - 由于安全和性能问题,通常不推荐使用
eval()
- 当
二、执行栈
2.1 执行栈
执行栈(Execution Stack)也称为调用栈,是一种后进先出(LIFO)的数据结构,用于存储在代码执行过程中创建的所有执行上下文。
2.2 执行栈的工作原理
- 当JavaScript引擎开始执行代码时,首先创建全局执行上下文并推入栈底
- 每当遇到函数调用时,创建新的函数执行上下文并推入栈顶
- 函数执行完毕后,其执行上下文从栈顶弹出
- 控制权返回给之前的执行上下文
- 当所有代码执行完毕后,全局执行上下文从栈中弹出
2.3 执行栈示例
function foo() {
console.log("foo");
bar();
}
function bar() {
console.log("bar");
}
foo();
// 执行栈变化过程:
// 1. 全局执行上下文入栈
// 2. foo函数执行上下文入栈
// 3. bar函数执行上下文入栈
// 4. bar函数执行完毕,出栈
// 5. foo函数执行完毕,出栈
// 6. 全局执行上下文出栈
三、执行上下文的创建过程
执行上下文创建分为两个阶段:创建阶段和执行阶段。
3.1 创建阶段
JS执行上下文的创建阶段主要负责三件事:确定this、创建词法环境组件(LexicalEnvironment)、创建变量环境组件(VariableEnvironment)。
ExecutionContext = {
ThisBinding = <this value>, // 确定this的值
LexicalEnvironment = {}, // 创建词法环境组件
VariableEnvironment = {}, // 创建变量环境组件
};
3.1.1 确定this的值
- 全局执行上下文:this总是指向全局对象(浏览器中为window对象)
- 函数执行上下文:this的值取决于函数的调用方式
3.1.2 词法环境组件
词法环境是一个包含标识符变量映射的结构,这里的标识符表示变量/函数的名称,变量是对实际对象【包括函数类型对象】或原始值的引用。
词法环境由环境记录与对外部环境引入记录两个部分组成:
- 环境记录:存储当前环境中的变量和函数声明的实际位置
- 外部环境引入记录:保存自身环境可以访问的其它外部环境(类似作用域链)
词法环境分为两种:
全局词法环境:
- 外部环境引入记录为null
- 环境记录类型为”对象环境记录”
函数词法环境:
- 包含用户在函数中定义的所有属性方法外,还包含arguments对象
- 外部环境引入可以是全局环境,也可以是其它函数环境
- 环境记录类型为”声明性环境记录”
// 全局环境
GlobalExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Object",
},
outer: <null>
}
};
// 函数环境
FunctionExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
},
outer: <Global or outerfunction environment reference>
}
};
3.1.3 变量环境组件
变量环境也是词法环境,具备词法环境所有属性。ES6中唯一的区别在于:
- 词法环境:存储函数声明与
let、const声明的变量 - 变量环境:仅存储
var声明的变量
通过一个例子来看:
let a = 20;
const b = 30;
var c;
function multiply(e, f) {
var g = 20;
return e * f * g;
}
c = multiply(20, 30);
对应的全局执行上下文伪代码:
GlobalExectionContext = {
ThisBinding: <Global Object>,
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Object",
a: <uninitialized>, // let const 为 uninitialized
b: <uninitialized>,
multiply: <func>
},
outer: <null>
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Object",
c: undefined // var 为 undefined
},
outer: <null>
}
};
对应的函数执行上下文伪代码:
FunctionExectionContext = {
ThisBinding: <Global Object>,
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
Arguments: {0: 20, 1: 30, length: 2},
},
outer: <GlobalEnvironment>
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
g: undefined
},
outer: <GlobalEnvironment>
}
};
关键区别:
var声明在创建阶段被设置为undefinedfunction声明在创建阶段被设置为自身函数let和const被设置为未初始化(uninitialized)
这就是变量提升的本质,也是let和const存在暂时性死区的原因——JS引擎对它们的初始化赋值不同。
3.2 执行阶段
在执行阶段,JavaScript引擎会:
- 执行代码,给变量赋值
- 执行函数调用
- 解析变量引用
代码执行时根据创建阶段的环境记录对应赋值:
var在创建阶段为undefined,执行阶段赋予定值let/const值为uninitialized,如果有值就赋值,无值则赋予undefined
四、变量对象与活动对象
4.1 历史背景
ES3之前的执行上下文创建过程使用变量对象(Variable Object,VO)和活动对象(Active Object,AO)的概念来解释。从ES5开始,官方用词法环境(LexicalEnvironment)和变量环境(VariableEnvironment)替代了它们,因为更容易理解。
变量对象与活动对象其实是同一个概念:
- 变量对象:与执行上下文相关的数据作用域,存储了在上下文中定义的变量和函数声明
- 活动对象:在函数执行上下文中,用活动对象来表示变量对象
这与ES5+中的全局环境记录(对象环境记录)和函数环境记录(声明性环境记录)一一对应。
ES6新增的let和const不存在变量提升,因此需要用词法环境和变量环境的概念来解释这个问题。
4.2 变量对象(Variable Object,VO)
变量对象是执行上下文中的一个特殊对象,用于存储变量和函数声明。
- 全局执行上下文的变量对象是全局对象(window/global)
- 函数执行上下文的变量对象在进入执行阶段后称为活动对象(Active Object,AO)
4.3 活动对象(Active Object,AO)
活动对象是函数执行上下文进入执行阶段后,变量对象被激活的状态。
- 活动对象包含变量对象的所有属性
- 此外还包含函数的arguments对象
- 活动对象是在函数执行时创建的
4.4 变量对象的创建过程
- 函数参数:建立arguments对象,属性为参数名,值为参数值
- 函数声明:如果变量对象中已存在同名属性,则替换它
- 变量声明:如果变量对象中不存在同名属性,则添加它,值为undefined;如果已存在,则忽略
五、变量提升
变量提升(Hoisting)是JavaScript的一个特性,指变量和函数声明会被提升到其所在作用域的顶部。
5.1 变量提升的原理
变量提升的本质是在执行上下文的创建阶段,变量和函数声明被添加到变量对象中,而赋值操作在执行阶段才会进行。
5.2 变量提升示例
console.log(a); // undefined
var a = 10;
// 等价于
var a;
console.log(a); // undefined
a = 10;
// 函数声明提升
foo(); // 正常执行
function foo() {
console.log("foo");
}
// 函数表达式不会提升
bar(); // 报错:bar is not a function
var bar = function () {
console.log("bar");
};
六、作用域
作用域(Scope)是指变量和函数的可访问范围。
6.1 作用域类型
- 全局作用域:在代码的任何地方都能访问
- 函数作用域:仅在函数内部可访问
- 块级作用域:ES6引入,使用
let和const声明的变量,仅在块级作用域内可访问
6.2 作用域链
作用域链是由当前执行上下文的变量对象和所有父级执行上下文的变量对象组成的链表结构。
当查找变量时,JavaScript引擎会从当前执行上下文的变量对象开始查找,如果找不到,则向上查找父级执行上下文的变量对象,直到找到全局执行上下文的变量对象。
6.3 作用域链示例
var globalVar = "global";
function outer() {
var outerVar = "outer";
function inner() {
var innerVar = "inner";
console.log(innerVar); // inner
console.log(outerVar); // outer
console.log(globalVar); // global
}
inner();
}
outer();
七、this指向
this是执行上下文中的一个特殊对象,其指向取决于函数的调用方式。
7.1 this指向规则
- 默认绑定:在非严格模式下,全局函数中的this指向全局对象;在严格模式下,this指向undefined
- 隐式绑定:当函数作为对象的方法调用时,this指向该对象
- 显式绑定:使用call()、apply()、bind()方法,this指向指定的对象
- new绑定:使用new关键字创建实例时,this指向新创建的实例
- 箭头函数:箭头函数没有自己的this,它会捕获所在上下文的this值
7.2 this指向示例
// 默认绑定
function foo() {
console.log(this); // 非严格模式下指向window
}
foo();
// 隐式绑定
const obj = {
name: "obj",
foo: function () {
console.log(this.name); // obj
},
};
obj.foo();
// 显式绑定
function bar() {
console.log(this.name);
}
const obj1 = { name: "obj1" };
const obj2 = { name: "obj2" };
bar.call(obj1); // obj1
bar.apply(obj2); // obj2
const boundBar = bar.bind(obj1);
boundBar(); // obj1
// new绑定
function Person(name) {
this.name = name;
}
const person = new Person("张三");
console.log(person.name); // 张三
// 箭头函数
const obj3 = {
name: "obj3",
foo: function () {
setTimeout(() => {
console.log(this.name); // obj3,箭头函数捕获foo函数的this
}, 1000);
},
};
obj3.foo();
八、闭包
闭包(Closure)是指有权访问另一个函数作用域中变量的函数。
8.1 闭包的形成
当一个函数嵌套在另一个函数内部,并且内部函数引用了外部函数的变量,当内部函数在外部函数执行完毕后仍然被引用时,就形成了闭包。
8.2 闭包的特性
- 延长变量生命周期:闭包可以使外部函数的变量在函数执行完毕后仍然存在
- 访问外部作用域:内部函数可以访问外部函数的变量
- 数据私有化:可以创建私有变量和方法
8.3 闭包示例
function createCounter() {
let count = 0;
return {
increment: function () {
count++;
return count;
},
decrement: function () {
count--;
return count;
},
getCount: function () {
return count;
},
};
}
const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.decrement()); // 1
console.log(counter.getCount()); // 1
九、执行上下文的生命周期
执行上下文的生命周期包括创建阶段、执行阶段和销毁阶段。
创建阶段:
- 创建变量对象
- 建立作用域链
- 确定this指向
执行阶段:
- 变量赋值
- 函数调用
- 执行代码
销毁阶段:
- 执行完毕后,执行上下文被销毁
- 局部变量和函数引用被释放
十、执行上下文与性能优化
10.1 性能影响
- 执行栈溢出:递归调用过深可能导致执行栈溢出
- 内存泄漏:闭包可能导致内存泄漏,因为它会保持对外部变量的引用
- 作用域链查找:深层作用域链查找会影响性能
10.2 优化建议
- 避免过深的递归:使用迭代替代递归
- 合理使用闭包:避免不必要的闭包
- 减少作用域链查找:将频繁访问的变量缓存到局部作用域
- 避免使用eval:eval会创建新的执行上下文,影响性能
十一、常见问题与解决方案
11.1 变量提升导致的问题
问题:变量在声明前被访问,值为undefined。
解决方案:
- 使用
let和const代替var,它们具有块级作用域,虽然也会被提升,但会形成暂时性死区(TDZ),在声明前访问会报错,避免意外访问undefined - 始终在作用域顶部声明变量
11.2 this指向混乱
问题:函数中的this指向不符合预期。
解决方案:
- 使用箭头函数,它会捕获外部上下文的this
- 使用bind()方法绑定this
- 使用变量保存this,如
const self = this
11.3 闭包导致的内存泄漏
问题:闭包引用的变量不会被垃圾回收。
解决方案:
- 不再使用闭包时,将其引用设为null
- 避免在闭包中引用大型对象
- 合理设计闭包,只引用必要的变量
十二、代码优化建议
12.1 减少全局变量
// 优化前:使用全局变量
var count = 0;
function increment() {
count++;
}
// 优化后:使用闭包
function createCounter() {
let count = 0;
return function () {
return ++count;
};
}
const increment = createCounter();
12.2 避免不必要的闭包
// 优化前:不必要的闭包
function createFunctions() {
const functions = [];
for (var i = 0; i < 5; i++) {
functions.push(function () {
return i;
});
}
return functions;
}
// 优化后:使用let或IIFE
function createFunctions() {
const functions = [];
for (let i = 0; i < 5; i++) {
functions.push(function () {
return i;
});
}
return functions;
}
12.3 优化作用域链查找
// 优化前:深层作用域链查找
function deepScope() {
// 深层嵌套
function inner() {
// 多次访问外部变量
console.log(outerVar);
console.log(outerVar);
console.log(outerVar);
}
let outerVar = "value";
inner();
}
// 优化后:缓存变量
function optimizedScope() {
let outerVar = "value";
function inner() {
// 缓存到局部变量
const localVar = outerVar;
console.log(localVar);
console.log(localVar);
console.log(localVar);
}
inner();
}
十三、总结
执行上下文是JavaScript代码执行的核心概念,理解它对于掌握JavaScript的运行机制至关重要:
- 执行上下文类型:全局执行上下文、函数执行上下文、Eval执行上下文
- 执行栈:存储执行上下文的后进先出结构
- 创建过程:确定this绑定、创建词法环境、创建变量环境
- 词法环境 vs 变量环境:词法环境存储函数声明与
let/const声明的变量,变量环境仅存储var声明的变量 - 变量提升的本质:
var声明被设置为undefined,function声明被设置为自身函数,而let/const被设置为uninitialized - 变量对象与活动对象:ES3旧概念,与ES5+的词法环境/变量环境概念一一对应,后者更易理解
通过理解执行上下文,我们可以更好地理解JavaScript的运行机制,写出更高效、更可维护的代码。