五分钟带你秒杀JavaScript作用域

1 阅读6分钟

JS的心脏:JS引擎

  单刀直入地说:JavaScript 是一门灵活全面的解释型语言,而这个多面手执行代码,依赖于引擎。目前最主流的引擎有两类:

1.浏览器内置引擎:Chrome的V8引擎等等

2.Node.js引擎:基于V8引擎构建的服务器端JS运行环境

  我们以V8引擎为例,它就像JS的心脏,可以读取并执行JS代码,是JS能适配多种环境的核心驱动。

JS的执行过程

  要了解JS的作用域,首先我们要知道JS怎样执行的。v8引擎执行 JS 代码就像一个高效的流水线。分为四个阶段:

  1.v8首先会读取JS代码,不会立刻执行,先编译,把要加工的材料送上流水线。

  2. 分词:即词法分析。把代码拆分开为一个个词法单元 ,把流水线上的原材料进行分拣

  3.解析:即语法分析。将词法单元组织成一个 语法树 AST(抽象语法树) ,构建一个产品的设计图。

  4. 执行:根据 AST 生成代码并执行,合成出一个完整的产品。

  话不多说,我们实战一下,看看这个引擎怎样工作

var a=10
console.log(a)

  引擎执行过程:

  1.读取代码,把语句拆成零件,准备送上流水线了: 发现'var'、'console.log()'等词法单元。

  2.构建AST,表示函数声明和调用关系。

  3.生成机器码并执行,返回结果'10'。

  可以看到,我们先定义了一个变量a,并赋值为10,然后我们将其输出了。假如我有点叛逆,把这两句倒过来写呢?

  也就是变成这样:

console.log(a)
var a=10

  会输出10吗?不该吧。 答案却让你大跌眼镜:不会报错!

  不是吧?有丰富编程经验的你觉得,使用还未声明的变量,就是天王老子来了也该报错!这就来到另一个概念:JS声明提升

  JS声明提升:JS在编译阶段会将变量声明和函数声明提升到当前作用域的顶部,但赋值留在原地。

讲了这么多,到底什么是作用域?

  作用域:变量的可访问范围,有以下几种:

全局作用域:

  • 代码最外层声明的变量,整个代码都能访问
var globalVar = '我是全局变量'

function test() {
    console.log(globalVar)  // ✅ 可以访问
}
test()  // 输出:我是全局变量

函数作用域:

  • 在函数内部用 var 声明的变量,只在函数内部有效
  • 函数的参数也是函数作用域的一部分
function test() {
    var localVar = '我是局部变量'
    console.log(localVar)  // ✅ 可以访问
}
test()
console.log(localVar)       // ❌ 报错!访问不到

  那我们来看这么一段代码:

function outer() {
    const outerVar = '外层'
    function inner() {
        const innerVar = '内层'
        console.log(outerVar)  // ✅ 能访问外层
        console.log(innerVar) // ✅ 能访问自己
    }
    console.log(innerVar)     // ❌ 报错,访问不到内层
}

  当函数作用域嵌套时该怎么办才好?请你牢记:

内层函数可以访问外层函数的变量,外层函数无法访问内层函数的变量。

  聪明的你发现:使用var来声明好像不太对劲?有点反直觉:var可以重复声明一个变量、可以不声明变量之前就使用这个变量。这对开发者来说太别扭了。

  因此在ES6的更新中,引入了新的关键词let与const,也带来了块级作用域。(文末附ES6入门教程链接)

3.块级作用域:指 {} 代码块内部的作用域,在该作用域内声明的变量, 只在当前代码块内有效 ,超出代码块外部无法访问。

{
    let a = 10    // 只在这个 {} 内有效
    const b = 20  // 只在这个 {} 内有效
}
console.log(a)  // ReferenceError: a is not defined
console.log(b)  // ReferenceError: b is not defined

  值得注意的是,这些语句可以形参块级作用域:

代码块示例
if 语句if () { let x = 1 }
for 循环for (let i = 0; i < 3; i++) { }
while 循环while () { let x = 1 }
普通 {}{ let x = 1 }
函数内部function() { let x = 1 }

let与const

  let

  • let + {} = 块级作用域,哪怕就写个光秃秃的{}也成立

  • let 不会带来声明提升

  这修复了使用 var 不会出bug的bug,那么:

{
    let a=10
}
console.log(a)

  终于会报错了,let回避了声明提升带来的许多弊端。

const:const是常量,不能重新赋值,也不能重新声明,例如:

let a=10
a=20
//不会报错,可以重新赋值

const a=10
const a=12
//则会报错,不可以赋值了

暂时性死区:就是上面说的,let、const变量在声明之前的这段区域,再次区域内访问变量会报错。暂时性是因为这个区域是临时的,一旦执行到变量声明行,死区就结束了

再来个例题吧

var a=1
function foo(b){
    var a=3;
    console.log(a,b)
}
 foo(2)

  会输出什么?

  (3,2)

  解析:

  • var a = 1 是全局变量
  • var a = 3 是 foo 函数内部的局部变量

在函数内部,局部变量会遮蔽(覆盖)全局变量

当 JavaScript 查找变量时,会先在当前作用域查找,找到了就停止查找。

8f052040-bcf8-4e31-9c49-68170eee0fcc.png

如果去掉函数内部的 var 呢?

var a = 1

function foo(b) {
    a = 3        // 注意:没有 var
    console.log(a, b)
}

foo(2)           // 输出: 3 2
console.log(a)   // 输出: 3 —— 全局变量被修改了!

⚠️ 注意:没有 var 声明的赋值,会修改全局变量!这是 var 声明提升带来的常见坑。

总结

  1. JS 引擎:V8 引擎负责编译和执行 JS 代码
  2. 执行过程:编译(分词→解析)→ 执行
  3. 声明提升:变量/函数声明会提升到作用域顶部
  4. 三种作用域:全局作用域、函数作用域、块级作用域(ES6)
  5. 作用域链:查找变量时逐级向上查找
  6. 声明方式:优先使用 const,其次 let,避免 var
声明方式作用域重复声明声明提升
var函数作用域允许
let块级作用域禁止暂时性死区
const块级作用域禁止暂时性死区

📚 延伸阅读ES6 入门教程 - 阮一峰


如果觉得本文对你有帮助,欢迎点赞、收藏、关注!你的支持是我创作的动力 🌹🌹