这是我参与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 的函数作用域和全局作用域。它们的关系示意如下:
当 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,并且全局作用域已经没有上层作用域了头探不出去了),那就歇菜,报错!
在这个查找过程中,层层递进的作用域,就形成了一条作用域链,作用域链关系展示如下:
当然上面说的作用域链,并没有很深入,后续会深入变量对象,然后再深入理解作用域链的相关机制。
参考
- 深入理解JavaScript作用域和作用域链
- 从编译原理的角度理解作用域
- 你不可不知的 JavaScript 二三事#Day5:湯姆克魯斯與唐家霸王槍——變數的作用域(Scope) (1)
- 你不可不知的 JavaScript 二三事#Day6:湯姆克魯斯與唐家霸王槍——變數的作用域(Scope) (2)
文中如有错误,欢迎在评论区指正,如果这篇文章帮助到了你或者喜欢,欢迎点赞和关注。