JavaScript作用域:从基础到闭包的探索手记

229 阅读7分钟

前言

在JavaScript中,作用域这一概念对于理解和编写正确的代码至关重要。它定义了变量和函数的可访问性,也就是在哪里可以找到并使用它们。以下是关于JavaScript作用域的知识分享,涵盖了全局作用域、函数作用域、块级作用域以及“欺骗”词法作用域的技术。

1. 全局作用域

全局作用域是整个JavaScript程序中最外层的作用域。它始于脚本或独立的.js文件的开始,止于脚本的结束。在这个作用域中声明的变量和函数在整个程序中都是可见的,我们可以从程序的任何位置访问全局作用域内的变量,示例如下:

// 全局变量声明
let globalLet = "I'm a global variable"

// 全局函数声明
function globalFunction() {
  console.log("This is a global function.")
}

// 在全局作用域内访问全局变量和函数
console.log(globalLet)   // 输出: "I'm a global variable"
globalFunction()   // 输出: "This is a global function."

// 在其他函数内部访问全局变量和函数
function anotherFunction() {
  console.log(globalLet)   // 输出: "I'm a global variable"
  globalFunction();   // 输出: "This is a global function."
}

// 即使在脚本末尾,全局变量和函数依然可访问
console.log(globalLet)   // 输出: "I'm a global variable"
globalFunction()   // 输出: "This is a global function."

2. 函数作用域

函数作用域,也称为局部作用域,是限于函数内部的作用域。在函数内部声明的变量和函数只能在其所在函数的范围内被访问,而不能在函数外部直接访问,示例如下:

let a = 1
function foo() {
  let a = 2
  function bar() {
    console.log(a)
  }
  bar()
}

foo()    //执行结果 2

在上面的代码中,foo()函数被调用时是这样的过程:

foo 函数内部调用 bar() 时,bar 函数试图访问变量 a,变量的查找是可以从内部作用域内往外部作用域查找的,它首先在自己的作用域内寻找变量 a,而 bar 函数内部并没有声明变量 a,所以它会继续向上搜索它外部的作用域,显然它在 foo 函数作用域中找到了变量 a。因此,bar 函数打印出 foo 函数作用域内的 a 的值 2

综上所述,当执行 foo() 函数时,因为它访问的是 foo 函数作用域内的局部变量 a,而非全局变量 a,这就是为什么结果是 2 而不是 1

3. 块级作用域

块级作用域是由花括号 {} 包围的代码块所形成的作用域,如if语句、for循环、switch语句等。 let与最近的 {} 包围的代码也可以形成代码块,与函数作用域类似,我们不能从花括号 {} 之外访问到它们,示例如下:

if (true) {
  let a = 1
}

console.log(a)  //这段代码会报错

代码报错的原因是let{}组合,形成了块级作用域,我们不可以从外部访问到内部的变量,如果我们将let改为var就可以得到结果1,它们在作用域规则上的行为存在显著差异,因为var声明的变量存在声明提升,它的声明和初始化发生在代码执行之前,相当于编译,编译要干的事,是找到当前域中的有效标识符,由于 var a 在全局作用域中被声明,所以在 if 语句外部的 console.log(a) 能够访问到该变量,并输出其值 1,过程如下:

var a; // 声明被提升到作用域顶部,此时值为 undefined
if (true) {
  a = 1; // 实际赋值操作
}
console.log(a); // 输出:1

4. 欺骗词法作用域

在js中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量的,当内部函数被返回到外部函数之外时,即使外部函数执行结束了,但是内部函数引用了外部函数的变量,那么这些变量依旧会被保存在内存中,我们把这些变量的集合称为闭包,示例如下:

function foo(str, a) {
  eval(str) // 相当于写了var b = 3
  console.log(a, b);
}

foo('var b = 3', 1)

最后得到的结果是 1 3,也就是说eval()将原本不属于此处的代码搬到了此处,让代码执行的就像天生就定义在此处一样,是不是很神奇,我们再看一个:

function a() {
  function b() {
    var bbb = 222
    console.log(aaa) //结果111
  }
  var aaa = 111
  return b
}
var c = a()
c()

当开始执行这段代码时,全局执行上下文入栈,变量环境里有函数ac。接下来函数a的执行上下文入栈,里面放了函数b和变量aaa,当函数a执行完毕后,按道理讲函数areturn b结束将出栈,但是因为a体内声明了函数b,但是函数b却不在函数a内调用,而是在函数a外被调用,这就会导致它的执行上下文无法被完全释放,它就会留下一个闭包,闭包里面放了未使用到的aaa = 111。接下来才是函数b的入栈,它的执行上下文放了bbb = 222,当它执行console.log(aaa)时,发现自己的执行上下文里没有aaa,它就会去它的词法作用域a内寻找,最后我们在a的遗产——闭包里找到了aaa。怎么样,闭包是不是很强大功能呢?

闭包的优缺点

  • 变量私有化
    闭包是JavaScript中最常用的实现变量私有化的手段。通过在一个函数内部定义变量,并返回一个可以访问这些内部变量的函数(通常是在该函数内嵌套函数),这样就可以创建一个闭包。闭包使得内部变量在其外部函数执行完毕后仍然存在,并且只对该闭包返回的函数可见,外部代码无法直接访问和修改。
  • 内存泄露
    由于闭包会维持对外部变量的引用,可能导致这些变量及它们所引用的对象在逻辑上不再需要时仍无法被垃圾回收器回收,从而造成内存泄漏。

尾声

全局作用域作为最外层的作用域,提供了全局变量和函数的共享空间,但过度使用易引发命名冲突、数据泄露和难以调试的问题。因此,应遵循最小权限原则,仅在必要时才将变量提升至全局作用域。

函数作用域为变量和函数提供了局部的生存环境,确保了各函数内部逻辑的独立性,避免了全局变量的滥用。理解作用域链有助于在多层嵌套的作用域中正确查找和访问变量。

块级作用域letconst声明引入,提供了更精细的变量生命周期控制,尤其适用于循环、条件语句等需要临时变量的场景,有效避免了变量提升和作用域混淆。

闭包作为JavaScript中独特的“欺骗”词法作用域的机制,允许内部函数访问并操作外部函数的作用域,即使外部函数已经执行完毕。闭包在实现数据封装、异步编程、模块化等方面发挥着关键作用,但同时也要求开发者注意其可能导致的内存管理和代码复杂性问题。

总的来说,深入理解并恰当地运用JavaScript作用域知识,是提升编程技能、写出高质量代码的重要一步。