前言
作用域是JavaScript中一个重要的概念,它决定了变量和函数在代码中的可访问性和可见性。了解JavaScript的作用域对于编写高效、可维护的代码至关重要。本文将深入介绍JavaScript作用域相关的知识点,其中包括作用域类型,作用域链,变量提升以及闭包等。
什么是作用域
在之前的文章中我们对TS中的模块进行了详细的分享,TS的在防止命名污染,冲突时采用的是命名空间,当我们看过TS命名空间的编译产物后我们就会知道,实际上其实际做法是采用JS的作用域实现一个独立的区域达到解决命名冲突和变量访问的可见性作用
JS中的作用域是一个存储变量、函数以及对象的位置,每个变量、函数和对象都被存储在一个特定的作用域中,它是指变量和函数在代码中的可访问范围,作用域决定了代码中哪些部分可以访问特定的变量和函数,通过作用域,我们可以将变量和函数封装在不同的作用域中,使其在合适的范围内可访问
就像上图中的示例,Foo1无法获取Foo2中的var4变量
作用域类型
常见的作用域类型包括全局作用域(Global Scope)、局部作用域(Local Scope)和块级作用域(Block Scope),JS也不例外
全局作用域
全局作用域是在整个代码中都可访问的作用域。在浏览器环境中,全局作用域通常是指window对象;在node环境下,则是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();
局部作用域
JS中的局部作用域一般代指函数作用域(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{...}等
ES6之前
在ES6之前,由于变量都是使用var声明的,所以没有块级作用域此类概念,只有全局作用域和函数(局部)作用域。那么需要模拟块级作用域如何怎么操作呢?
答案是使用立即执行函数表达式(IIFE):通过将代码包装在匿名函数中并立即执行该函数,可以创建一个独立的作用域,使得内部声明的变量在函数外部不可访问
(function () { var var5 = "var5"; console.log(var5); // var5 })(); console.log(var5); // var5 is not defined
ES6以后
在es6以后,官方推出了块级作用域的概念,使用let和const关键字声明的变量具有块级作用域,它们只能在声明的代码块内部访问。
if (true) { let var5 = "var5"; console.log(var5); // var5 } console.log(var5); // var5 is not defined
作用域链
作用域链(Scope Chain)是JS用于解析标识符(变量和函数)的机制,它是由多个嵌套的作用域组成的,它决定了变量和函数的查找顺序。
上面我们说到局部作用域时谈到的Foo3可以访问全局以及当前函数作用域中的标识符这个特点就归功于作用域链这个概念。当访问一个变量时,JS引擎会先从当前作用域开始查找,如果找不到这个名称的标识符则继续向上一级作用域查找,直到找到变量或达到全局作用域为止,如果在全局作用域中仍然找不到,则认为该标识符未定义。
变量提升
变量提升的概念出现在面试题中的频率十分高,对于开发者来说也是不可忽略的重要知识点;在之前的面试题文章中的Q6和Q7两题中就出现了变量提升相关的知识点
基础概念
变量提升是JS在代码执行前将变量和函数声明提升到作用域顶部的行为,它由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变量
特点
- 即使外部函数已经执行完毕,内部函数依然可以访问外部函数作用域中的变量(当栈将函数弹出时,变量依然处于内存中)
- 闭包可以持有对外部变量的引用,使得外部变量的值在内部函数中保持活动状态(不被垃圾回收机制回收)
- 闭包中的内部函数可以修改并更新外部变量的值
- 闭包的函数可以获取到创建时的整个作用域链的标识符
- 闭包可能会导致内存泄漏,被闭包引用的变量无法被垃圾回收机制处理
使用场景
封装私有变量
JS中没有TS的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
模块化、命名空间
在TypeScript模块文章中我们使用JS中的立即执行函数实现了命名空间功能,达到模块化的效果
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的出现奠定了JS中的块级作用域的概念,使if、for等语句中也存在作用域这一功能;然而不仅仅是条件语句、循环语句,使用{...}定义的范围都是块级作用域,这意味着在块级作用域内部声明的变量只在该作用域内有效,并且在作用域外部无法访问
{ let msg = "hello"; console.log(msg); // hello } console.log(msg); // msg is not defined
变量提升
使用let和const声明的变量不会被提升到作用域的顶部,它们只能在声明后才能被访问;这点与var不太一样
暂时性死区
暂时性死区指的是在变量声明前访问变量会抛出错误。只有在变量声明语句执行完成之后,变量才会进入有效状态,才能被访问和使用。这点效果与上面的变量提升效果一样
不可重复声明
在同一个作用域中不能被重复声明,否则会报错;而使用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)。这个现象说明JS采用的是词法作用域。
动态作用域
那么反之,如果还是上述代码,foo2取的var1是执行foo2的作用域中的变量(20),就说明语言采用的是动态作用域
总结
JavaScript作用域与变量提升是编写高质量代码所必须掌握的重要概念。本文介绍了作用域的定义、作用和目的,以及JavaScript中的不同作用域类型,包括全局作用域、函数作用域和块级作用域。我们还讨论了变量提升的概念、原理和影响范围,以及作用域链和闭包的关系。此外,还探讨了ES6的作用域,并对比了其与早期作用域的区别。最后针对动态作用域与词法作用域作了一个简单的说明。
好了,以上就是文章的全部内容了,谢谢你看到了这里,如果觉得文章不错的话,还望三连支持一下,感谢支持!