一篇吃透JS作用域!

121 阅读5分钟

1.JS引擎

  我们要想理解JS作用域,必须要从底层执行内核——JS引擎说起。任何一门编程语言都有自己的引擎,在日常学习开发最常用的JS引擎有:Chrome的V8引擎和Node.js引擎,只要内置了JS引擎也就是编译环境,我们就可以执行JavaScript代码了。

2.JS的执行过程

  JavaScript的代码执行,从来不是单纯逐行解释运行,而是以作用域为基础框架,配合预解析、执行上下文、变量提升逐级推进。是理解 JS 执行全过程的核心切入点。就以V8引擎为例,来讲解V8引擎是如何执行JS代码的。

  1. 编译:V8先读取代码,其并不会第一时间执行代码,而是先编译代码。
  2. 分词:将代码拆分成一个一个词法单元。
  3. 解析/语法分析:将分好的词法单元组织成一个抽象语法树AST(Abstract Syntax Tree)。
  4. 执行:根据AST生成代码执行。

话不多说,直接上干货,带你秒懂 JS 引擎咋干活!

var a = 10;
console.log(a);

引擎工作过程:

  1. 先把整段代码全盘通读一遍;
  2. 开始拆词分词,拆出:var、a、=、10、console.log(a) 这些最小代码碎片;
  3. 把碎片按顺序拼装成 AST 语法树,帮自己理好逻辑顺序;
  4. 按顺序跑代码,打印出 10。

然后再看这段反直觉代码,老 C/Java 选手直接懵圈:

console.log(a);
var a = 10;

  放 C、Java 里,这代码早被编译器按在地上摩擦,直接报错 “变量没定义”!但在 JS 里,它不仅不报错,还输出个undefined。这就说明JS引擎在编译阶段优先扫描了var变量,也就是提前访问变量时,引擎就识别到了变量存在,但尚未赋予具体数值,当直接console.log(a)时就默认返回了初始值undefined。

  搞懂了 JS 引擎的底层干活逻辑,铺垫已经到位,接下来正式进入本文重头戏 —— 作用域!

3.作用域

 3.1全局作用域

  • 写在函数、{} 块外面的代码范围,整个代码都能访问。
var globalNum = 10;//全局变量
function fn() {
    console.log(globalNum);
}

fn();       // 可以输出 10
console.log(globalNum); // 外面也能用

 3.2函数作用域

  • 函数作用域:变量只在函数内部有效,外面访问不到,和全局正好相反。
var globalNum = 100;// 全局 global 变量

function foo() {
  var loacalNum = 20;// 函数作用域变量,只在函数里能用
  console.log(globalNum); // 可以用全局的
  console.log(loacalNum);  // 可以用自己函数里的
}

foo();
// console.log(localNum);  // 报错!外面访问不到函数里的变量

 3.3块级作用域

学习块级作用域之前,要先了解什么是let、什么是const

  3.3.1 let和const

  作为前端开发者,变量声明是每天都会用到的基础操作。ES6 之前,我们只能用 var 声明变量,但它的作用域混乱、变量泄露等问题,曾让无数开发者踩坑。ES6 推出的 let 和 const,彻底解决了 var 的痛点,成为目前前端开发的标准规范。

  1. let
  • let+{}会创建一个块级作用域,块级作用域中的变量只能在块级作用域中使用。
if (true) {
  let a = "let 有块级作用域";
  console.log(a); // 内部可访问
}
console.log(a); // 报错:msg is not defined

  • let不会带来变量提升。
 // var 的情况`
console.log(foo); // 输出undefined`
 var foo = 2;`
// let 的情况`
console.log(bar); // 报错ReferenceError`
let bar = 2;`

上面代码中,变量foovar命令声明,会发生变量提升,即脚本开始运行时,变量foo已经存在了,但是没有值,所以会输出undefined。变量barlet命令声明,不会发生变量提升。这表示在声明它之前,变量bar是不存在的,这时如果用到它,就会抛出一个错误。

暂时性死区:ES6 中,let/const 声明的变量,在声明语句执行之前,该变量所处的区域就是暂时性死区,此时访问变量会直接报错,而非像 var 那样返回 undefined

  • let不能像var一样重新声明
// 
var a = 10;
var a = 20;
console.log(a); // 输出20
let b = 10;
let b = 20; // 报错,无法重复声明
  1. const:
  • const和let有99%的相似性,不同的是const是常量,不能重新赋值。
const num = 10;
num =20;         //此时重新对num赋值就会报错
console.log(num);
  1. var、let、const 核心区别总结
变量作用域暂时性死区重复声明初始赋值重新赋值
var函数 / 全局允许可省略可修改
let块级作用域不允许可省略可修改
const块级作用域不允许必须赋值不可修改

  3.3.2块级作用域详解:

   定义:被 { } 大括号包裹的区域,就是块级作用域。if、for、while、单独 {} 都属于块级。

if (true) {
  // 块级变量,只在当前 {} 内有效
  let a = 10;
  const b = 20;
}
// 出了大括号就不能访问
console.log(a); // 报错
console.log(b); // 报错

注意:只有 let / const才有块级作用域,var没有块级作用域,会穿透 {}。

if (true) {
  var m = 100;  // var 无块级作用域,泄露到外面
  let n = 200;  // let 有块级作用域,仅限内部
  const s = 200;  // let 有块级作用域,仅限内部
}
console.log(m); // 100 能访问
console.log(n); // 报错
console.log(s); // 报错

4.总结

  • JS 引擎:JS 代码依托 V8、Node.js 等引擎执行,引擎会经过分词→解析生成 AST 抽象语法树→执行代码三个核心步骤。
  • JS 执行:特点代码不是逐行直接执行,会先进行预解析编译,存在变量提升机制,这也是var可以先使用后声明、输出undefined的原因。
  • 全局作用域:声明在函数、{}代码块外部,整个代码任意位置都可访问。
  • 函数作用域:在函数内部用var声明的变量,仅函数内部可访问,外部无法调用。
  • 块级作用域:被{}包裹的区域(if/for/while/ 单独大括号),仅 let/const能生成块级作用域,变量只能在当前块内访问;var无块级作用域,会发生变量泄露。

[想深入学习JS,推荐阅读这本经典书籍](《阮一峰 ES6 标准入门教程)