JavaScript 中的执行上下文与执行上下文栈、作用域与作用域链、闭包
在 JavaScript 的世界里,理解执行上下文与执行上下文栈、作用域与作用域链以及闭包这些概念,对于编写高效、健壮的代码至关重要。
执行上下文(Execution Context)
执行上下文是 JavaScript 执行代码的环境抽象概念。每当 JavaScript 引擎遇到可执行代码(全局代码、函数代码、eval 代码)时,就会创建一个执行上下文。执行上下文包含了代码执行所需的所有信息,如变量环境(Variable Environment)、词法环境(Lexical Environment)、this 值等。
变量环境(Variable Environment)
变量环境用于存储在执行上下文创建阶段声明的变量和函数声明。在函数执行上下文中,它还包含了函数的参数。变量环境在创建阶段就已确定,后续代码执行过程中一般不会改变(除了通过 eval 修改)。例如:
javascript
function add(a, b) {
let result = a + b;
return result;
}
add(2, 3);
在调用 add 函数时,会创建一个函数执行上下文。其变量环境中会包含参数 a 和 b,值分别为 2 和 3,以及变量 result,在声明时初始值为 undefined,后续赋值为 5。
词法环境(Lexical Environment)
词法环境与变量环境类似,但它更侧重于存储通过 let 和 const 声明的变量。词法环境在代码执行过程中可能会发生变化,因为 let 和 const 声明的变量存在块级作用域。例如:
javascript
{
let localVar = 10;
const constantVar = 20;
console.log(localVar, constantVar);
}
// console.log(localVar); // 这里会报错,localVar 超出了其作用域
在上述代码块中,创建了一个新的词法环境,其中包含 localVar 和 constantVar。当代码块结束,这个词法环境的生命周期也结束,外部无法访问其中的变量。
this 值
this 值在执行上下文中也有特定的绑定规则。在全局执行上下文中,this 指向全局对象(在浏览器环境中是 window,在 Node.js 环境中是 global)。在函数执行上下文中,this 的值取决于函数的调用方式:
- 作为对象方法调用时,this 指向该对象。
javascript
const obj = {
value: 10,
print: function() {
console.log(this.value);
}
};
obj.print(); // 输出 10
- 通过 call、apply 或 bind 方法调用时,this 被显式设置为传入的第一个参数。
javascript
function greet() {
console.log(this.message);
}
const message = "Hello, World!";
greet.call({message: "Hi, there!"}); // 输出 Hi, there!
- 普通函数调用时,this 指向全局对象(在严格模式下,this 为 undefined)。
javascript
function sayHello() {
console.log(this);
}
sayHello(); // 在非严格模式下输出 window 对象
执行上下文栈(Execution Context Stack)
执行上下文栈,也称为调用栈(Call Stack),是 JavaScript 引擎用来管理执行上下文的一种数据结构。它是一个后进先出(LIFO)的栈结构。当 JavaScript 引擎开始执行代码时,首先会创建全局执行上下文并将其压入执行上下文栈。每当遇到函数调用时,就会为该函数创建一个新的执行上下文并压入栈顶。函数执行完毕后,其对应的执行上下文从栈顶弹出,控制权交还给栈中下一个执行上下文。例如:
javascript
function first() {
console.log('Inside first function');
second();
console.log('Again inside first function');
}
function second() {
console.log('Inside second function');
}
first();
在上述代码中,首先创建全局执行上下文并压入栈。调用 first 函数时,创建 first 函数的执行上下文并压入栈顶,此时输出 Inside first function。接着调用 second 函数,创建 second 函数的执行上下文并压入栈顶,输出 Inside second function。second 函数执行完毕,其执行上下文从栈顶弹出,first 函数继续执行,输出 Again inside first function。最后 first 函数执行完毕,其执行上下文也从栈顶弹出,全局执行上下文成为栈顶元素。如果执行上下文栈满了,就会导致栈溢出错误(Stack Overflow Error),常见于递归函数没有正确设置终止条件的情况。例如:
javascript
function infiniteRecursion() {
infiniteRecursion();
}
infiniteRecursion();
上述代码会不断创建新的执行上下文并压入栈,最终导致栈溢出错误。
作用域(Scope)
作用域决定了变量和函数的可访问性。JavaScript 中有两种主要的作用域类型:全局作用域和局部作用域(函数作用域、块级作用域)。
全局作用域
全局作用域是最外层的作用域,在全局作用域中声明的变量和函数在整个代码中都可访问(除非被同名的局部变量遮蔽)。例如:
javascript
let globalVar = 10;
function globalFunction() {
console.log(globalVar);
}
globalFunction(); // 输出 10
这里 globalVar 和 globalFunction 都在全局作用域中声明,在函数内部可以访问到全局变量 globalVar。
局部作用域
局部作用域又分为函数作用域和块级作用域。
函数作用域
函数作用域指的是在函数内部声明的变量和函数只在该函数内部可访问。函数作用域为函数内的变量提供了一种封装机制,避免变量污染全局作用域。例如:
javascript
function outer() {
let localVar = 20;
function inner() {
console.log(localVar);
}
inner();
}
outer(); // 输出 20
// console.log(localVar); // 这里会报错,localVar 超出了其作用域
在 outer 函数内部声明的 localVar 变量,在 inner 函数内部可以访问到,但在 outer 函数外部无法访问。
块级作用域
块级作用域是由 {} 包裹的代码块,通过 let 和 const 声明的变量具有块级作用域。例如:
javascript
for (let i = 0; i < 5; i++) {
console.log(i);
}
// console.log(i); // 这里会报错,i 超出了其块级作用域
在 for 循环的代码块中声明的 let i 变量,其作用域仅限于该 for 循环块,循环结束后,外部无法访问 i。
作用域链(Scope Chain)
作用域链是一个由执行上下文的词法环境组成的链表。当查找变量时,JavaScript 引擎会从当前执行上下文的词法环境开始,如果找不到,就会沿着作用域链向上查找,直到全局作用域。例如:
javascript
let globalVar = 10;
function outer() {
let outerVar = 20;
function inner() {
let innerVar = 30;
console.log(globalVar, outerVar, innerVar);
}
inner();
}
outer(); // 输出 10 20 30
在 inner 函数执行时,查找 globalVar,首先在 inner 函数的词法环境中找不到,然后沿着作用域链向上,在全局执行上下文的词法环境中找到 globalVar。同理,找到 outerVar 在 outer 函数的词法环境中,innerVar 在 inner 函数自身的词法环境中。如果在作用域链上都找不到某个变量,就会导致 ReferenceError 错误。例如:
javascript
function func() {
console.log(nonExistentVar);
}
func(); // 报错:ReferenceError: nonExistentVar is not defined
这里 nonExistentVar 没有在任何作用域中声明,所以在查找时会抛出 ReferenceError 错误。
闭包(Closure)
闭包是 JavaScript 中一个强大且独特的特性。当一个函数内部返回另一个函数,并且返回的函数引用了外部函数的变量时,就形成了闭包。闭包使得这些外部变量在外部函数执行完毕后依然能够被访问和使用。例如:
javascript
function outer() {
let outerVar = 10;
return function inner() {
console.log(outerVar);
};
}
const closureFunc = outer();
closureFunc(); // 输出 10
在上述代码中,outer 函数返回了 inner 函数,inner 函数引用了 outer 函数中的 outerVar 变量。当 outer 函数执行完毕后,outerVar 变量并没有被销毁,因为 inner 函数形成的闭包保持了对它的引用。通过调用 closureFunc(即 inner 函数),依然可以访问到 outerVar 的值。闭包在实际开发中有很多应用场景,比如实现数据封装和模块模式。
数据封装
javascript
function counter() {
let count = 0;
return {
increment: function() {
count++;
return count;
},
decrement: function() {
count--;
return count;
}
};
}
const myCounter = counter();
console.log(myCounter.increment()); // 输出 1
console.log(myCounter.decrement()); // 输出 0
在 counter 函数中,通过闭包将 count 变量封装起来,外部代码只能通过 increment 和 decrement 方法来间接操作 count,保证了数据的安全性和封装性。
模块模式
javascript
const myModule = (function() {
let privateVar = 10;
function privateFunction() {
console.log('This is a private function');
}
return {
publicFunction: function() {
privateFunction();
console.log(privateVar);
}
};
})();
myModule.publicFunction(); // 输出 This is a private function 和 10
// console.log(privateVar); // 这里会报错,privateVar 是私有的
// privateFunction(); // 这里会报错,privateFunction 是私有的
在上述模块模式的示例中,通过立即执行函数表达式(IIFE)创建了一个闭包。privateVar 和 privateFunction 都是私有的,外部无法直接访问,只有通过暴露的 publicFunction 方法才能间接使用内部的私有成员,实现了模块的封装和信息隐藏。
然而,过度使用闭包也可能会导致内存泄漏问题。因为闭包会保持对外部变量的引用,如果这些变量不再需要,但由于闭包的存在而无法被垃圾回收机制回收,就会占用不必要的内存。例如:
javascript
function createLeak() {
let largeData = new Array(1000000).fill(1);
return function() {
console.log(largeData.length);
};
}
const leakFunc = createLeak();
// 即使 createLeak 函数执行完毕,largeData 由于被闭包引用,无法被垃圾回收
在上述代码中,createLeak 函数内部创建了一个占用大量内存的数组 largeData,并且返回的函数形成的闭包引用了 largeData。即使 createLeak 函数执行完毕,largeData 也无法被垃圾回收,导致内存泄漏。为了避免这种情况,在不需要闭包引用外部变量时,应及时解除引用,例如:
javascript
function createNoLeak() {
let largeData = new Array(1000000).fill(1);
let temp = largeData.length;
return function() {
console.log(temp);
};
}
const noLeakFunc = createNoLeak();
// 这里 largeData 可以被垃圾回收,因为闭包没有直接引用它
在这个改进后的代码中,将 largeData.length 的值存储在一个临时变量 temp 中,闭包只引用 temp,这样 largeData 就可以被垃圾回收机制回收,避免了内存泄漏。
综上所述,执行上下文与执行上下文栈、作用域与作用域链以及闭包是 JavaScript 中非常重要的概念,它们相互关联,共同构成了 JavaScript 代码执行和变量管理的基础。深入理解并熟练运用这些概念,能让开发者在编写 JavaScript 代码时更加得心应手,编写出高质量、高性能的代码。
最后,对于下一阶段项目的练习,要充分利用这次机会,提高对于学过前端知识的实践能力,提升编程思维,同时更重要的是借此机会加深和小组其它成员之间的练习,锻炼团队合作意识,为后续的团队协作锻炼自己。