# 理解JavaScript作用域

JavaScript 定义了一套如何存储以及查找变量的规则,这套规则就是作用域

作用域共有两种主要的工作模型。第一种是最为普遍的,被大多数编程语言所采用的词法作用域,另外一种叫作动态作用域。JavaScript 采用的就是词法作用域。

作用域主要有全局作用域、函数作用域以及 ES6 新增的块级作用域。 下面我们详细的了解一下上面介绍的作用域。

# 全局作用域

拥有全局作用域的是全局变量。

在不用的环境中,全局变量是不同的:

  • 在浏览器环境中全局变量是 window
  • 在 Node.js 中全局变量是 golbal

在浏览器 JS 中我们可以通过 window.spoce 的方式访问全局作用域下的定义的变量。

# 函数作用域

函数作用域的含义是指,属于这个函数的全部变量都可以在整个函数的范围内使用及复用(事实上在嵌套的作用域中也可以使用)

在 JavaScript 中每个函数都会创建一个函数作用域。在函数之外一般是访问不到函数内定义的变量,只能在函数内部进行访问。

function fn() {
    var spoce = 'fn'
    console.log(spoce); // 1
}
console.log(spoce); // 2

在上面的代码中1 会打印出 fn, 但是执行到2时会报错 Uncaught ReferenceError: spoce is not defined

# 块作用域

在 ES6 之前,JavaScript 不存在块级作用域。

if(true) {
    var spoce = 'define spoce'
}
console.log(spoce) // 会发生什么???

如果是块级作用域在外面是不应该访问到 spoce 这个变量,代码就会报错 Uncaught ReferenceError: spoce is not defined。而实际上这段代码会打印出 define spoce,为什么会这样呢?

用 var 声明的变量存在变量提升的问题。var spoce = 'define spoce',分为两个部分:var spocespoce = 'define spoce',在解析 JS 代码时会把var spoce提升到函数作用域的顶部,在 if 条件语句中只是对其进行了赋值操作。所以上面的代码是在函数作用域中声明了spoce,所以打印的是 define spoce

if(true) {
    let spoce = 'define spoce'
}
console.log(spoce) // 又会发生什么???

难道这里不是打印 define spoce 吗?

其实代码执行到这里就会报错 Uncaught ReferenceError: spoce is not defined

let 关键字会将声明的变量隐式的绑定在所在的块作用域(通常是 {...} 内部),所以在 if {...} 这个块之外是访问不到块作用域的,所以执行到 console.log(spoce) 的时候 spoce 是没有声明的。

使用 let 声明变量的时候还要注意的是 let 声明的变量不会变量提升,使用变量必须在声明之后才能使用,否则也是会抛出 Uncaught ReferenceError: spoce is not defined

在 ES6 中还有一个 const 关键字与 let 类似,只是 const 声明变量不能进行修改。

# 作用域链

var a = 10;
function fn() {
    var b = 5;
    function fn1() {
        var c = 1;
        console.log('result: ', a + b + c); // result: 16
    }
    fn1();
}
fn();
变量abc 都会查找当前作用域是否存在变量a bc,如果不存在则继续查找父级作用域直至查询到全局作用域为止。这样一套变量的查找规则其实就是作用域链的作用,规定了一个变量能访问的作用域。至于为什么会这样以后有时间再细细研究(内部属性 [[Scope]] 的作用)。
var a = 10;
function fn() {
    var a = 5;
    var b = 5;
    function fn1() {
        var c = 1;
        console.log('result: ', a + b + c); // result: 11
    }
    fn1();
}
fn();

作用域查找会在找到第一个匹配标签的时候就结束,因此内外作用域有同名的变量的时候,会产生遮蔽效应,直接采用了内层的同名变量a。

除了 let、const 之外,with 和 try/catch 也会产生块级作用域,这是这两种方法并不推荐使用,如果感兴趣可自行 google。

# 一个面试题

var z = 10;
function fn() {
    console.log(z)
}

(function (fn) {
    var z = 20;
    fn();
})(fn)

上面打印出来的结果是 10,为什么? JS 的作用域是静态作用域,变量声明的时候就已经确定了其作用域。fn 的作用域内没有声明 z 变量,就会查找上层作用域即全局作用域。

# 总结

在 JavaScript 中函数作用域是我们最常使用的作用域单元,ES6 流行之后,块级作用域我们使用的也越来越多,let、const 声明的变量会添加到块级作用域中。