本文深入解析JavaScript中的作用域机制,涵盖全局、函数及块级作用域的区别。详细阐述变量提升原理、作用域链查找规则,以及闭包的定义、特性与应用场景。结合ES6 let/const特性,探讨暂时性死区与词法作用域的实现,帮助开发者掌握高质量JS代码的核心概念。
什么是作用域
在JavaScript中,作用域定义了变量、函数及对象的可访问范围。每个标识符都被绑定到特定的作用域中,决定了代码中哪些部分可以访问这些资源。通过合理划分作用域,可以将变量和函数封装在特定的范围内,确保其在合适的上下文中被访问。

参考上图示例,Foo1函数无法直接获取Foo2函数内部声明的var4变量。
作用域类型
JavaScript中常见的作用域类型包括全局作用域(Global Scope)、局部作用域(Local Scope)以及块级作用域(Block Scope)。
全局作用域
全局作用域允许在整个代码执行期间访问其中的变量和函数。在浏览器环境中,全局作用域通常对应window对象;而在Node.js环境中,则对应globalThis或global对象。
在全局作用域中声明的变量或函数被称为全局变量或全局函数,它们可以在代码的任何位置被访问和调用。

如上图所示,Foo4函数可以成功访问全局作用域中声明的变量及函数。
const var1 = "var1";
const var2 = "var2";
function Foo1() {
const var3 = "var3";
}
function Foo2() {
const var4 = "var4";
function Foo3() {}
}
function Foo4() {
console.log(var1, var2, Foo1, Foo2); // var1 var2 [Function: Foo1] [Function: Foo2]
}
Foo4();局部作用域
JavaScript中的局部作用域通常指函数作用域(Function Scope)。在函数内部声明的变量和函数仅能在该函数内部访问,外部代码无法直接访问这些资源。

如上述Foo3函数的效果所示,它可以访问全局作用域以及当前所在函数作用域中的变量和函数。
const var1 = "var1";
const var2 = "var2";
function Foo1() {
const var3 = "var3";
}
function Foo2() {
const var4 = "var4";
function Foo3() {
console.log(var4, Foo3); // var4 [Function: Foo3]
}
Foo3();
}
Foo2();块级作用域
块级作用域是指在代码块(通常由大括号{}包裹的部分)内声明的作用域。常见的代码块包括if语句、for循环以及try-catch块等。
ES6之前
在ES6标准发布之前,由于变量声明主要使用var关键字,因此不存在块级作用域的概念,仅有全局作用域和函数(局部)作用域。那么,如何模拟块级作用域呢?
解决方案是使用立即执行函数表达式(IIFE)。通过将代码包装在匿名函数中并立即执行,可以创建一个独立的作用域,使得内部声明的变量在函数外部不可访问。
(function () {
var var5 = "var5";
console.log(var5); // var5
})();
console.log(var5); // var5 is not definedES6以后
ES6标准引入了块级作用域的概念,使用let和const关键字声明的变量具有块级作用域,它们只能在声明所在的代码块内部被访问。
if (true) {
let var5 = "var5";
console.log(var5); // var5
}
console.log(var5); // var5 is not defined作用域链
作用域链(Scope Chain)是JavaScript用于解析标识符(变量和函数)的机制,由多个嵌套的作用域组成,决定了变量和函数的查找顺序。
前文提到Foo3可以访问全局以及当前函数作用域中的标识符,这一特性归功于作用域链。当访问一个变量时,JavaScript引擎会先从当前作用域开始查找;若未找到该名称的标识符,则继续向上一级作用域查找,直到找到变量或达到全局作用域。若在全局作用域中仍未找到,则判定该标识符未定义。
变量提升
变量提升是面试中高频出现的概念,也是开发者必须掌握的重要知识点。
基础概念
变量提升是指JavaScript在代码执行前,将变量和函数声明提升到作用域顶部的行为。这一过程由JavaScript引擎在编译阶段处理。变量提升影响了整个作用域范围内的代码,允许我们在声明之前使用变量,但需注意:只有变量声明被提升,赋值操作不会被提升。基于上述概念,请思考以下代码:
console.log(var6); // undefined
var var6 = 10;上述代码的执行逻辑相当于:
var var6;
console.log(var6); // undefined
var6 = 10;优先级问题
当同一作用域中同时存在同名函数和变量时,函数声明的优先级高于变量声明。也就是说,函数会在变量之上进行声明,参考以下代码:
var a = 10;
function a() {}
console.log(a); // 10其执行过程可视为:
var a; // 函数a
var a; // 变量a
a = function () {};// 使用function声明函数可以看成是声明+赋值
a = 10;
console.log(a); // 10如何验证这一点?请思考以下代码:
function a() {}
var a;
console.log(a); // [Function: a]若去除a的赋值语句,函数的赋值顺序即可得到验证,其逻辑相当于:
var a; // 函数a
var a; // 变量a
a = function () {};
console.log(a); // [Function: a]闭包
定义
当函数开始执行时,其内部变量及函数会压入调用栈。此时,若当前作用域中有另一个函数正在使用该作用域的变量,该变量占用的内存将不会被垃圾回收机制回收,这一现象即为闭包。
换言之,闭包是指函数能够“记住”并访问其创建时的词法环境,即使在函数定义的词法作用域之外执行,同样适用。

请思考以下代码:
const foo = (function iife() {
const num = 10;
function foo() {
return num;
}
return foo;
})();
console.log(foo()); // 10上述代码使用立即执行函数(IIFE)作为外部函数的作用域,它返回内部函数foo。而foo函数使用了IIFE函数中的num变量,从而形成闭包。最终在IIFE函数外部调用foo时,依然可以访问num变量。
特点
- 即使外部函数已经执行完毕,内部函数依然可以访问外部函数作用域中的变量(当栈将函数弹出时,变量依然处于内存中)。
- 闭包可以持有对外部变量的引用,使得外部变量的值在内部函数中保持活动状态(不被垃圾回收机制回收)。
- 闭包中的内部函数可以修改并更新外部变量的值。
- 闭包的函数可以获取到创建时的整个作用域链的标识符。
- 闭包可能会导致内存泄漏,被闭包引用的变量无法被垃圾回收机制处理。
使用场景
封装私有变量
JavaScript中没有类似TypeScript的private关键词,无法直接定义私有变量。但可以通过闭包产生私有环境作用域(ES2022后引入了#关键字,用于定义私有变量,相比于使用闭包,更直观和方便)。
const Animal = (function () {
const name = "dog";
function Animal() {}
Animal.prototype.getName = function () {
return name;
};
return Animal;
})();
console.log(new Animal().getName()); // dog延长变量周期
延长变量的生命周期也是闭包的特性之一,该效果通过内部函数对外部作用域的可访问性实现。
function delayMessage(msg) {
return function () {
return msg;
};
}
const msg = delayMessage("msg");
console.log(msg()); // msg模块化、命名空间
我们使用JavaScript中的立即执行函数实现了命名空间功能,达到模块化的效果。
var moduleA = (function () {
var privateVariable = "Hello";
// 私有函数
function privateMethod() {
console.log(privateVariable);
}
return {
// 公共函数
publicMethod: function () {
privateMethod();
},
};
})();
moduleA.publicMethod(); // Hello
console.log(moduleA.privateVariable); // undefined缓存
闭包还可以用于创建缓存函数,以提高函数的执行效率。缓存函数可以将输入参数与其对应的结果保存在内部,避免重复计算。
function createCache() {
var cache = {};
return function (key, val) {
if (!cache[key]) {
cache[key] = val;
console.log("保存");
}
return cache[key];
};
}
var cacheModule = createCache();
cacheModule("num1", "123"); // 保存
cacheModule("num2", "456"); // 保存
cacheModule("num1", "123");
cacheModule("num2", "456");ES6的作用域
在ES6标准之后,引入了const、let和箭头函数,这三者对作用域分别有什么影响呢?
const、let
块级作用域
前文提到,const和let的出现奠定了JavaScript中块级作用域的概念,使得if、for等语句中也存在作用域这一功能。然而,不仅仅是条件语句和循环语句,使用{...}定义的范围都是块级作用域。这意味着在块级作用域内部声明的变量只在该作用域内有效,并且在作用域外部无法访问。
{
let msg = "hello";
console.log(msg); // hello
}
console.log(msg); // msg is not defined变量提升
使用let和const声明的变量不会被提升到作用域的顶部,它们只能在声明后才能被访问。这点与var不太一样。
暂时性死区
暂时性死区(TDZ)指的是在变量声明前访问变量会抛出错误。只有在变量声明语句执行完成之后,变量才会进入有效状态,才能被访问和使用。这点效果与上面的变量提升效果一致。
不可重复声明
在同一个作用域中不能被重复声明,否则会报错;而使用var定义变量时,后声明的会覆盖先声明的。
这三个特点可以参考下面的代码:
console.log(var1); // 在赋值前使用了变量“var1”
const var1 = 11;
let var2 = 22; // 无法重新声明块范围变量“var2”
let var2 = 22;箭头函数
箭头函数在JavaScript中仍然与普通函数一样有函数作用域的概念。
题外话
动态作用域与词法作用域
作用域的种类有两种:分别是动态作用域和词法作用域(静态作用域)。上述我们介绍的是词法作用域,什么是词法作用域?
词法作用域
词法作用域是基于代码的静态结构来确定变量的访问规则。也就是说,它由变量和函数在代码中的声明位置而不是调用的位置来确定。请思考以下代码:
function foo1() {
var var1 = 10; // foo2可访问的作用域
// 声明foo2的作用域
return function foo2() {
console.log(var1);// 10
};
}
var foo2 = foo1();
function foo3() {
var var1 = 20; // foo2不可访问的作用域
// 执行foo2的作用域
foo2();
}
foo3();在foo1中声明了函数foo2,在foo3中执行foo2,可以看到,foo2取的var1是声明foo2的作用域中的变量(10)。这个现象说明JavaScript采用的是词法作用域。
动态作用域
反之,如果还是上述代码,foo2取的var1是执行foo2的作用域中的变量(20),就说明语言采用的是动态作用域。
总结
JavaScript作用域与变量提升是编写高质量代码所必须掌握的重要概念。本文介绍了作用域的定义、作用和目的,以及JavaScript中的不同作用域类型,包括全局作用域、函数作用域和块级作用域。我们还讨论了变量提升的概念、原理和影响范围,以及作用域链和闭包的关系。此外,还探讨了ES6的作用域,并对比了其与早期作用域的区别。最后针对动态作用域与词法作用域作了一个简单的说明。
以上就是详解JavaScript中的作用域的详细内容,更多关于JavaScript作用域的资料请关注本站其它相关文章!