探索JS作用域

6 阅读4分钟

引言:在新手学JS最易懵的3个点——作用域、闭包、原型,其中作用域是基础

一、什么是 JS 作用域

大白话:作用域就是“变量/函数的可用范围”——哪里能访问,哪里不能访问,由它的“出生地”决定

其中,可分为三大作用域(全局作用域,函数作用域,块级作用域)

1.全局作用域

顾名思义,该作用域作用于全局,在该作用域声明的变量能在任何地方被访问

var a = 100

if(true){
  console.log(a) // 100
}

function foo(){
  console.log(a)
}
foo() // 100

console.log(a) // 100

由此可见 : 在全局作用域声明的变量,在语句(例如 if ), 函数, 全局内均能访问到。

2. 函数作用域

该作用域作用于函数内部,仅在函数内部生效,函数的形参也包含在函数作用域内部

function foo(){
  var a = 100
  console.log(a)
}

foo() // 100

console.log(a) // 未定义

由此可见,定义在函数内部的变量无法在函数作用域外访问到

3.块级作用域

块级作用域 (let,const 和 { } 语法配合使用会导致声明的变量处在一个作用域中)

{
  var a = 100
}
{
  let b = 200
}
{
  const c = 300
}
console.log(a) // 100
console.log(b) // 未定义
console.log(c) // 未定义

如上述代码所示,在被{ } 包裹 var 声明的 a 还是能在全局访问到, 而 let / const 声明的变量则不能被全局访问到,这是因为他们形成了自己的块级作用域


作用域的核心规则

1.变量提升(var的“坑”,let/const的优化)

先来谈谈何为变量提升

var a = 100 
console.log(a) // 100

console.log(b) // undefined
var b = 200

由 var 定义的变量会存在变量提升的现象,就好像头和身子分离了,a 的例子看不出来, b 就豁然开朗了, 一般别的语言这个时候就已经开始报错了,但是 JS 的 var 不一样,它特殊,在 V8 引擎里它长这样

var a = 100 
console.log(a) // 100

var b
console.log(b) // undefined
b = 200

可以看到 b 的声明跑到前面去了,这种现象我们称之为变量提升

又如

// 全局作用域
{
  var a = 1; // var 声明的变量,穿透 {},属于外层(全局)作用域
  console.log(a); // 1
}
console.log(a); // 1  能访问到,说明 {} 没形成独立作用域

// 函数作用域(对比)
function fn() {
  var b = 2; // 函数作用域内的变量,外层访问不到
}
console.log(b); // 报错:b is not defined(函数作用域生效)

这种反人类直觉的东西不应该存在,所以 JS 后续推出了 let/const 的优化


console.log(c) // 未定义
let c = 100 


console.log(d) // 未定义
const d = 300

这样才符合直觉

2.作用域链(变量查找的规则)

作用域的查找规则就像是剥洋葱,一层一层往外层找,直到全局作用域,找不到就报错

// 全局作用域
let all = "全局变量";
// 外层函数作用域
function outerFn() {
  let outer = "外层变量";
  // 内层函数作用域
  function innerFn() {
    let inner = "内层变量";
    // 查找顺序:inner(当前内层作用域) outer(外层作用域) global(全局作用域)
    console.log(inner); // 输出:内层变量(当前作用域找到)
    console.log(outer); // 输出:外层变量(当前找不到,去外层找)
    console.log(all); // 输出:全局变量(内层、外层都找不到,去全局找)   
    // 注释打开会报错(全局也找不到)
  }
  innerFn();
}
outerFn();

3. if/for 不是对象,也没有“作用域”

这里的意思是 if/for 不会像上述的全局,函数拥有自己独特的作用域

   {
  let c = 3; // let 声明,被 {} 的块级作用域包裹
  const d = 4; // const 同理
  console.log(c); // 3
  console.log(d); // 4
}
console.log(c); //  报错:c is not defined(块级作用域隔离)
console.log(d); //  报错:d is not defined

// 经典例子:for 循环的块级作用域
for (let i = 0; i < 3; i++) {
  // 每次循环都会创建一个独立的块级作用域,i 只在当前循环生效
  setTimeout(() => console.log(i), 0); // 输出 0、1、2 
}

for (var j = 0; j < 3; j++) {
  // var 无块级作用域,j 是全局的,循环结束后 j=3
  setTimeout(() => console.log(j), 0); // 输出 3、3、3 
}

就像上述代码所展示,var 声明的变量还是跑到全局了,所以输出的都是 3 , 当然这里还涉及到了定时器的异步,后续文章也会的对异步展开讨论