前言
从零开始JavaScript,今天我们要学习的是什么是作用域。对于新手小白来说,作用域的概念会感觉略为抽象,但是以后面试官可能会对此进行提问,所以梳理清楚这其中的逻辑是必需的。
正文
首先我们需要知道JavaScript代码是如何在浏览器中执行的
var a = 1
function fun() {
console.log(a);
}
fun()
浏览器如何读懂代码并执行给我们看呢?
记住一句话:代码在执行前需要先编译(编译就是找到某个域中的有效标识符),之后再从上至下地执行代码。
- 浏览器进行编译,识别到第一行我们定义了一个变量a = 1。
- 浏览器继续识别,看到第三行我们定义了一个函数fun(),但此时浏览器并不会执行该函数。
- 到了第七行,浏览器看到一个函数调用,此时开始向上寻找该函数具体代码并执行。
- 显示最终结果:1
了解过程之后,我们开始今天的学习:什么是作用域?
什么是作用域
作用域(scope),程序设计概念,通常来说,一段程序代码中所用到的名字并不总是有效/可用的,而限定这个名字的可用性的代码范围就是这个名字的作用域。 --百度百科
简单来说,作用域限制了变量作用的范围,就像农村门口设置围栏限制了动物的活动范围。JavaScript提供了4种作用域分别是: 全局作用域、局部作用域、块级作用域、欺骗词法作用域
1.全局作用域
全局作用域是指函数、变量、常量等对象可以在代码的任意位置访问,拥有全局作用域的变量也被称为“全局变量”。
var a = 1
function fun() {
console.log(a);
}
fun()
在此代码中a就是一个全局变量,a变量的声明在全局都有作用。
2.局部作用域
顾名思义,局部作用域就是只能在代码的部分区域中进行访问,名字或者变量只在内部起效果和作用。
var a = 1
function fun(){
var a = 2
console.log(a)
}
fun()
在此代码中,定义了一个全局变量a = 1,在fun()函数中又定义了一个变量a = 2,那最后的运行结果是什么呢?
输出结果为2
这是为什么?
我们知道代码在执行前需要先编译,也就是先编译后执行。在执行函数时,浏览器找到对应的函数域,域中的console.log()查找变量a,发现函数中已经有a,于是将a的值打印出来。
重点:变量的查找会先从内到外的作用域中查找
当我们执行fun()函数内部的代码时,寻找a变量会先从自己的函数域中查找,如果没有找到,再跳转到全局作用域中查找。
现在又有个问题:查找变量能不能从外到内呢?我们来看这段代码
function fun(){
var a = 2
}
fun()
console.log(a)
输出结果:ReferenceError: a is not defined
这说明console.log()根本找不到变量a所在位置,由此可以得出结论:
变量的查找会先从内到外的作用域中查找,不能从外到内查找
3.块状作用域
我们先来看一段代码:
var a = 1
var a = 2
console.log(a)
输出结果:2
这段代码中我们用var 重复定义了变量a,但是程序没有报错并且输出了第二次的a = 2。这是var的一个特点,但是并不是它的优点。假如我们需要对于一段长代码进行修改加工,用var定义变量的时候可能会和前面的某个变量重名,但是程序不会报错,那么写到最后就会出现较大错误并且需要花费很多时间去排除错误。
JavaScript中定义了let变量来解决这个问题。
let变量
我们来看一段代码:
let a = 1
let a = 2
console.log(a)
输出结果:SyntaxError: Identifier 'a' has already been declared
说明let变量中不能重复声明同一个变量,我们再看一段代码:
let a = 1
{
let a = 2
console.log(a); //2
}
console.log(a); //1
依次输出 2 1
为什么这里的变量a可以重复声明而且最后一次输出的结果是第一次定义的1?
这是因为let + {}会形成块级作用域,{}中的let变量a只在当前代码块中有效,所以不会影响{}外的变量a。
不只是let变量,JavaScript中还可使用const达到目的,const具备let的一切特性,但是const声明的变量值不能被修改(常量)。
4.欺骗词法作用域
欺骗词法作用域是指通过某些机制在运行时修改词法作用域。常见的有eval()和with()。
① eval()
function foo(str) {
eval(str)
var a = 1
console.log(a, b);
}
foo('var b = 2')
输出结果:1 2
我们这里并没有直接声明变量b,但是b的值为2。
这就是eval()的作用,让原本不属于这里的代码变成就是存在于这里的,eval()将字符串'var b = 2'转化为语句,等同于声明了一个变量b = 2。
② with()
var obj = {
a: 1,
b: 2,
c: 3,
}
function foo(obj) {
with (obj) {
a = 2
}
}
var o1 = {
a: 3
}
var o2 = {
b: 4
}
foo(o2)
console.log(a);
输出结果:2
a本应该是对象中的一个属性,但是我们现在却能直接打印出a的值。 这是因为使用with()修改对象中不存在的属性时,该属性会泄漏到全局成为全局变量, console.log()便能直接获取到全局变量a。
补充知识
1. 声明提升
function foo(){
console.log(a)
var a = 2
}
foo()
输出结果:undefined
var 声明的变量存在声明提升,提升到当前作用域的顶端,虽然定义了一个变量a,但是并没有为其初始化。这段代码等同于:
function foo(){
var a
console.log(a);
a = 2
}
foo()
2.暂时性死区
let a = 1
if (true) {
console.log(a); //暂时性死区 作用域中有该变量,但是不能使用
let a =2
}
在if语句中,变量a的声明在console.log()后面,console.log()找不到对应的变量。在代码块内,使用let和const命令声明变量之前,该变量都是不可用的,这就是暂时性死区。
小结
本篇文章主要讲述了什么是作用域、作用域的种类以及补充知识。
重点:
1、代码在执行前需要先编译(编译就是找到某个域中的有效标识符),之后再从上至下地执行代码
2、变量的查找会先从内到外的作用域中查找,不能从外到内查找
3、let + {}会形成块级作用域,{}中的let变量a只在当前代码块中有效,const具备let的一切特性,但是const声明的变量值不能被修改(常量)。
4、eval()让原本不属于这里的代码变成就是存在于这里的。with()修改对象中不存在的属性时,该属性会泄漏到全局成为全局变量。
5、var 声明的变量存在声明提升,提升到当前作用域的顶端。在代码块内,使用let和const命令声明变量之前,该变量都是不可用的,这就是暂时性死区。
这篇文章是我在掘金的第一篇文章,还有许多的不足之处,如果有错误之处请指出,我会及时修改。
感谢观看!