JavaScript 中的词法作用域、词法环境、执行上下文和闭包
在 JavaScript 编程领域,掌握词法作用域、词法环境、执行上下文、闭包和this关键字等基本概念对于编写复杂高效的代码至关重要。这些核心原则决定了变量的访问方式和函数的执行方式,使开发者能够编写更简洁、更安全、高度模块化的代码。理解这些概念有助于更好地控制作用域机制、执行流程以及不同作用域内的动态上下文this,为高级 JavaScript 编程实践和模式奠定基础。
目录
词汇范围
在编程中,尤其是在 JavaScript 中,词法作用域指的是变量和函数可访问或可见的上下文。词法作用域大致可以分为两类:全局作用域和局部作用域。
全球范围
当变量在任何函数外部声明时,它属于全局作用域。这意味着无论上下文如何,都可以从代码的任何其他部分访问和修改它。全局变量在应用程序的整个生命周期内都可访问,这使得它们易于获取,但也带来了意外修改的风险,从而可能导致程序错误。
例子:
var globalVar = "I am a global variable";
function accessGlobalVar() {
// Accessing the global variable within a function
console.log(globalVar);
}
accessGlobalVar(); // Output: I am a global variable
console.log(globalVar); // Output: I am a global variable
本地范围
另一方面,局部作用域指的是在函数或代码块(ES6 中)内声明的变量let。const这些变量只能在该函数或代码块内访问,对程序的其他部分是隐藏的。这种封装确保局部变量免受外部作用域的意外副作用影响。局部作用域主要有两种类型:函数作用域和代码块作用域。
- 函数作用域:在函数中使用 `\t`
var、let`\l` 或 ` \t` 声明的变量的作用域const限定为该函数。
例子:
function localFunctionScope() {
var functionScopedVar = "I am a local variable";
// Output: I am a local variable
console.log(functionScopedVar);
}
localFunctionScope(); // Output: I am a local variable
// Uncaught ReferenceError: functionScopedVar is not defined
console.log(localVar);
- 块作用域:在 ES6 中引入,通过 `
let<script>` 和 `<script>`const标签,在代码块内声明的变量{}只能在该代码块内访问。
例子:
if (true) {
let blockScopedVar = "I am block-scoped";
const anotherBlockScopedVar = "So am I";
var globalVar = "I am global!!!";
console.log(blockScopedVar); // Accessible here
console.log(anotherBlockScopedVar); // Accessible here
}
// Uncaught ReferenceError: blockScopedVar is not defined
console.log(blockScopedVar);
// Uncaught ReferenceError: anotherBlockScopedVar
// is not defined
console.log(anotherBlockScopedVar);
// Output: I am global!!!
console.log(globalVar);
词汇环境
词法环境是 JavaScript 引擎执行上下文的核心部分,在变量和函数的作用域、访问和管理方面起着至关重要的作用。
定义
另一方面,词法环境是 JavaScript 引擎在运行时用于基于词法作用域管理和访问变量的一种更具体的机制。它是 JavaScript 执行上下文的一部分,由两个重要部分组成:
-
环境记录:这里实际存储着词法作用域内定义的特定变量、常量和函数。
-
外部环境引用:这指的是父作用域的词法环境。它允许 JavaScript 在当前词法环境中找不到变量时,通过“链式”方式搜索变量值。
每次执行代码块或函数时,都会创建一个词法环境,其中包含所有局部变量,并与外部词法环境链接。这实际上形成了一个作用域链,决定了代码执行期间变量查找的方式。
词汇范围与词汇环境
词法作用域是 JavaScript 作用域机制的一部分——它定义了如何在嵌套代码层中解析变量名的规则。与之相对,词法环境指的是应用这些规则的具体实现——JavaScript 引擎在代码执行期间用于存储变量和函数声明,并根据词法作用域跟踪它们之间关系的具体结构。
执行上下文
在 JavaScript 中,执行上下文可以被视为一个抽象概念,它包含有关当前代码执行环境的信息。它包括跟踪代码块或函数执行所需的一切,例如this在给定时间可访问的值、变量、对象和函数。
执行上下文的类型
JavaScript 中有三种主要的执行上下文类型:
-
全局执行上下文 (GEC):这是默认或基础执行上下文。所有不在任何函数内部的代码以及全局作用域变量都在这里执行。一个 JavaScript 程序中只有一个全局执行上下文。
-
函数执行上下文(FEC):此上下文由函数内部代码的执行创建。每次函数调用都会创建一个新的 FEC,它包含函数的参数、局部变量等。它还包含对其外部环境的引用,这有助于实现词法作用域。
-
Eval 执行上下文:由函数中 JavaScript 代码的执行创建
eval,但由于安全性和性能问题,不建议使用。
执行上下文的组成部分
执行上下文由以下几个关键组件构成:
-
变量环境:这包括在当前上下文中声明的变量和函数。它本质上代表了函数和全局变量的词法环境。
-
词法环境:闭包和作用域链的奥妙之处就在这里得以体现。词法环境是一个结构,它保存着标识符-变量映射关系。(这里的“标识符”指的是变量/函数的名称,“变量”则是对对象/函数/数据的实际引用。)该环境还包含对外部环境的引用,外部环境可以指向全局环境或外部函数环境,从而实现作用域链。
-
此绑定:的值
this在执行上下文中确定并存储。的值this取决于函数的调用方式,可以引用全局上下文、对象的当前实例(在方法的情况下),或者使用诸如call、apply或之类的函数显式设置bind。
例子
为了加深理解,我们来看一个具体的例子:
let globalVar = "Welcome to the Global Context";
function outerFunction() {
let outerVar = "I'm in the Outer Function";
function innerFunction() {
let innerVar = "I'm in the Inner Function";
console.log(innerVar);
console.log(outerVar);
console.log(globalVar);
}
return innerFunction;
}
const inner = outerFunction(); // Execution Context for outerFunction is created and executed.
inner(); // Execution Context for innerFunction is created and executed.
当outerFunction调用 `function()` 时,会为其创建一个新的函数执行上下文。在 `function()` 内部,声明了outerFunction另一个函数,该函数被调用时,拥有自己的执行上下文。`function() ` 可以访问在其自身作用域(`function( )`)、`function( )` 的作用域(`function()` )和全局作用域(`function( )`)中声明的变量。这得益于 JavaScript 函数所具有的执行上下文、词法环境和闭包特性的结合。innerFunctioninnerFunctioninnerVarouterFunctionouterVarglobalVar
JavaScript 执行模型的丰富性使其能够实现复杂而结构化的执行和作用域管理,闭包和作用域通过执行上下文及其关联的词法环境得到正确管理。理解执行上下文的底层工作原理是掌握 JavaScript 并充分发挥其潜力的基础。
关闭
当一个函数即使在其词法作用域之外执行时,也能记住并访问其词法作用域,这就称为闭包。简单来说,闭包允许你从内部函数访问外部函数的作用域。这在 JavaScript 中是可行的,因为词法作用域允许函数维护对其周围词法环境的引用。
要理解闭包,需要掌握以下两个关键点:
- JavaScript 中的每个函数都可以访问它创建时的作用域(词法作用域)。
- JavaScript 函数是一等对象,这意味着它们可以从其他函数返回,并且可以像任何其他值一样传递。
当一个函数返回另一个函数时,被返回的函数会保留对其原始词法环境(即它被创建时的环境)的引用。这就是闭包的形成。尽管被返回的函数是在其原始作用域之外执行的,但它们仍然可以访问其父函数作用域中的变量。
例子
假设你想创建一个计数器对象,允许你递增、递减和获取当前计数值,但你希望将计数值设为私有,不让对象外部直接访问。这时闭包就派上用场了,它允许我们将计数变量封装在函数的作用域内,同时公开特定的功能。
function createCounter() {
let count = 0; // `count` is a private variable
return {
increment: function() {
count++;
console.log(count);
},
decrement: function() {
count--;
console.log(count);
},
getCount: function() {
console.log(count);
return count;
}
};
}
// Using the createCounter function to create a counter instance
const counter = createCounter();
counter.increment(); // Output: 1
counter.increment(); // Output: 2
counter.decrement(); // Output: 1
counter.getCount(); // Output: 1
// Trying to access the private `count` variable directly
console.log(counter.count); // Output: undefined
- 当
createCounter函数被调用时,会创建一个新的执行上下文。在这个执行上下文中,会建立一个词法环境,count变量就位于其中。这个词法环境不仅包含函数本身count,还包括内部函数 `f`increment、decrement`g` 和 `getCounth`。count变量位于函数的词法作用域内createCounter。这意味着它无法createCounter直接从函数外部的词法环境访问,从而保证了它的隐私性。 - 内部函数(
increment`a`、decrement`b`、getCount`c`)各自构成一个闭包。当一个函数(定义在另一个函数内部)即使父函数执行完毕后仍然保留对其父函数词法环境的引用时,就形成了闭包。这是因为在 JavaScript 中,函数会携带一个“背包”(即闭包),其中包含对其创建时词法环境中存在的变量的引用。因此,即使 `a` 的执行上下文createCounter在执行完毕后从执行栈中移除,`b`、`c`和`c`count仍然可以通过它们对 `a` 词法环境的闭包访问到该变量。incrementdecrementgetCountcreateCounter - 返回的对象
createCounter仅公开了 `get`increment、decrement`get` 和 `getCountget` 方法,而不直接公开count变量本身。这种封装方式确保只能count通过提供的接口修改或访问变量,从而遵循数据隐私和封装原则。这种结构由函数定义时的词法作用域决定,词法作用域规定了函数体中哪些变量可以访问。
this关键词
JavaScript 中的关键字this是一个强大的概念,它指向自己所属的对象,从而允许方法访问其所属对象。它的值取决于函数的调用方式。
this在全球范围内
在全局作用域中,this指的是全局对象。在浏览器环境中,全局对象是 `<global_object>` window,而在 Node.js 中,它是 ` <global_object> global`。
console.log(this === window); // Output: true (in a browser environment)
this在函数调用中
函数内部的值this取决于函数的调用方式:
- 常规函数调用:在全局作用域中,
this将指向全局对象(例如,window在浏览器中)。但是,在严格模式下,this将指向其他对象,undefined因为严格模式会阻止自动绑定到全局对象。
function show() {
console.log(this);
}
show(); // Output: Window (in a browser environment)
- 方法调用:当一个函数作为对象的方法被调用时,
this指向调用该方法的对象。
const person = {
name: 'Anton',
greet() {
console.log(`Hi, ${this.name}`);
}
};
// `this` refers to the `person` object
person.greet(); // Output: "Hi, Anton"
this使用箭头函数
箭头函数没有自己的this绑定。相反,它们会捕获this封闭的词法作用域的值。这种特性使得它们特别适用于回调函数。
const person = {
name: 'Antonio',
timer() {
setTimeout(() => {
console.log(this.name);
}, 1000);
}
};
person.timer(); // Output: "Antonio" (after 1 second)
在上面的例子中,内部的箭头函数从其周围的词法环境(在本例中为对象)中setTimeout获取值,说明了词法作用域如何影响值。thispersonthis
操纵,,和thisbind()call()apply()
thisJavaScript 提供了显式设置函数值的方法:
- call()
this:使用给定的值和单独提供的参数调用函数。
function introduce(language) {
console.log(`I am ${this.name} and I love ${language}.`);
}
const user = { name: 'Anthony' };
// I am Anthony and I love JavaScript.
introduce.call(user, 'JavaScript');
- apply():与 apply() 类似
call(),但参数以数组形式传递。
// I am Anthony and I love JavaScript.
introduce.apply(user, ['JavaScript']);
- bind():创建一个新函数,当调用该函数时,其
this关键字设置为提供的值,并且在调用新函数时,给定的参数序列会放在任何提供的参数之前。
const boundIntroduce = introduce.bind(user);
// I am Anthony and I love JavaScript.
boundIntroduce('JavaScript').
这些方法对于控制执行上下文尤其有用。
文章来源:https://dev.to/antonzo/lexical-scope-lexical-environment-execution-context-closure-in-javascript-5bn6