JavaScript 的作用域和作用域链,原来就是这么一回事

228 阅读7分钟

这是我参与8月更文挑战的第8天,活动详情查看8月更文挑战

前言

JavaScript 中有一个被称为作用域(Scope) 的特性。作用域的概念,对于许多新手开发者来说,不是很容易理解,但这个概念也是跟闭包这个老生常谈的知识点有联系。本文将用最简单,最容易理解的方式,来解释作用域和作用域链,希望对大家有所帮助。

什么是作用域?

作用域是指程序源代码中定义变量的区域。

作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。换句话说,作用域决定了代码区块中变量和其他资源的可见性。上面关于作用域的含义,可能不太懂。我们换种方式:

作用域这个术语,听起来是很高深,但要理解它,其实挺简单的。

可以把它想象成地盘知名度的概念,变量就是明星

有的变量只在自己的地盘--函数里被认得;有些变量就像好莱坞大明星,在程序的每一个角落都有影响力。

一切,取决于这个变量诞生的地方声明的方式。

一个变量的地盘有多大,能生效的范围有多广,就称为这个变量的作用域(Scope)。

我们还是结合代码来体会一下

function sayHi() {
    var myName = '追梦玩家';
    console.log('你好,' + myName);
}
sayHi(); // 执行 sayHi 函数,打印 "你好,追梦玩家"
console.log(myName); // Uncaught ReferenceError: myName is not defined

运行上面的代码,我们可以看到打印 myName 的时候,会报错,这是因为 myName 这个变量在全局作用域没有声明,所以在全局作用域下,获取 myName 会报错。变量 myName,只能在函数作用域里面使用,不能在全局作用域使用。

我们可以理解成:

作用域就是一个独立的地盘,让变量不会外泄、暴露出去。也就是说作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。

作用域有哪些?

下面我们来看看 JavaScript 的作用域有哪些?

ES6 之前 JavaScript 没有块级作用域,只有全局作用域和函数作用域。ES6 的到来,为我们提供了"块级作用域",可通过新增命令 let 和 const 来体现。

也就是作用域有 3 种:

  • 全局作用域——成龙大哥
  • 函数作用域——香港喜剧天王星爷
  • 块级作用域——上学时,班上 KTV 的麦霸「褒义」

全局作用域

声明在任何函数之外的顶层作用域的变量就是全局变量,这样的变量拥有全局作用域。

上面说明的,变量包括使用 var 声明和 function 关键字声明的函数。

var name = '追梦玩家'; // 全局作用域内的变量

// 函数作用域
function showName() {
    console.log(name);
}

// 块作用域
{
  name = '砖家'
}

showName(); // 输出 "砖家"

上面这个例子,我们可以看出,全局变量在全局作用域、函数作用域和块级作用域都可以获取到。

所有未定义直接赋值的变量,自动声明为拥有全局作用域

function sayHi() {
    myName1 = '砖家';
    var myName = '追梦玩家';
    console.log('你好,' + myName);
}
sayHi(); // 执行 sayHi 函数,打印 "你好,追梦玩家"
// console.log(myName); // Uncaught ReferenceError: myName is not defined
console.log(myName1); // "砖家"

所有window对象的属性拥有全局作用域 一般情况下,window 对象的内置属性都拥有全局作用域,例如 window.name、window.location、window.top、window.navigator.userAgent 等等。

console.log(navigator.userAgent);
console.log(window.navigator.userAgent);
// 上面两个打印的结果都是 "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36"

函数作用域

全局作用域有个弊端

如果我们写的很多JS 代码当中的变量声明,都没有使用函数包括,那么它们就全部都在全局作用域中。这样的话就会污染全局命名空间,容易引起命名冲突或覆盖。

// 张三写的代码
var myName = '追梦玩家';

// 李四写的代码
var myName = '砖家';

jQuery、Zepto 这些第三方库,为了避免全局作用域的这个弊端,所有的代码,都会放在一个自执行函数里面 (function() {//...})()。这其实是函数作用域的一个体现。

立即执行函数的作用,就是创建一个独立的作用域,这个作用域里面的变量,外面访问不到,即避免「变量污染」。

在函数内部定义的变量,拥有函数作用域。

var name = '追梦玩家'; // name 是全局变量
function sayHi1() {
    // myName 被定义成函数作用域变量
    var myName = '砖家';
    console.log('你好,' + myName);
}

function sayHi2(name) {
    // name 是传入 sayHi2 函数的形参,其实也是局部变量
    console.log('你好,' + name);
}

sayHi1(); // "你好,砖家"
sayHi2(name); // "你好,追梦玩家"
console.log(myName); // Uncaught ReferenceError: myName is not defined

{
    console.log(myName); // Uncaught ReferenceError: myName is not defined
}

myName 是在函数内部定义的变量,它们就被“画地为牢”,只能在函数作用域内部访问,全局作用域和块级作用域都是访问不到它的,会访问会出现报错。

块级作用域

块级作用域就是使用一对大括号包裹的一段代码,比如函数、判断语句、循环语句,甚至单独的一个{}都可以被看作是一个块级作用域。

ES6 开始,新增了 let 和 const,这两个声明变量的关键字。这两个关键字定义的变量,如果被大括号包裹住,这样就可以被看做是一个块级作用域。

块级作用域在如下情况被创建:

  • 在一个函数内部
  • 在一个代码块(由一对花括号包裹)内部
{
    let myName = '追梦玩家';
    console.log(myName); // "追梦玩家"
}

console.log(myName); // 报错,Uncaught ReferenceError: myName is not defined

function sayHi() {
    console.log('你好,' + myName); // 报错,Uncaught ReferenceError: myName is not defined
}
sayHi(); // 执行 sayHi 函数

在上面的例子,我们可以看出,块作用域内的变量只要出了自己被定义的那个代码块,那么就无法访问了。这点和函数作用域比较相似 —— 它们都只在“自己的地盘”上生效,所以它们也统称为” 局部作用域 “。

作用域链

作用域套作用域,就有了作用域链

当一个块或者一个函数嵌套在另一个块或者函数中时,就发生了作用域的嵌套。比如这样:

var hello = 'hello';
function init() {
    var name = "Mozilla";
    function sayHello() {
        console.log(hello, name);
    }
    sayHello();
    console.log(myName); // 报错
}
init();

上面这个例子中,有 init 的函数作用域、sayHello 的函数作用域和全局作用域。它们的关系示意如下:

image.png

当 sayHello 函数被调用的时候,执行 console.log(hello, name);,我们试图在 sayHello 这个函数里面访问变量 hello 和 name 的时候,发现在当前函数作用域内并没有找到这两个变量,因为 sayHello 这个函数作用域没有对 hello 和 name 这个变量声明,所以一开始肯定是找不到的。

要想找到 hello 和 name,该怎么做?可以理解,这个时候,JS 引擎探出头去,去上层作用域(init) ,找到了 name,那么就可以直接拿来用了。

在 init 函数作用域并没有找到 hello,所以这个时候,JS 引擎还会继续“探出头去”,去上层作用域(全局作用域),找到了 hello 变量,那么就可以直接拿来用了。

为什么打印 myName,会报错呢? 因为 init 函数作用域,没有找到变量 myName,所以 JS 引擎“探出头去”,去上层作用域(全局作用域),没有找到 myNam,并且全局作用域已经没有上层作用域了头探不出去了),那就歇菜,报错!

在这个查找过程中,层层递进的作用域,就形成了一条作用域链,作用域链关系展示如下:

image.png

当然上面说的作用域链,并没有很深入,后续会深入变量对象,然后再深入理解作用域链的相关机制。

参考

文中如有错误,欢迎在评论区指正,如果这篇文章帮助到了你或者喜欢,欢迎点赞和关注。