从零开始JavaScript之什么是作用域

158 阅读6分钟

前言

从零开始JavaScript,今天我们要学习的是什么是作用域。对于新手小白来说,作用域的概念会感觉略为抽象,但是以后面试官可能会对此进行提问,所以梳理清楚这其中的逻辑是必需的。

正文

首先我们需要知道JavaScript代码是如何在浏览器中执行的

var a = 1

function fun() {
    console.log(a);
}

fun()

浏览器如何读懂代码并执行给我们看呢?

记住一句话:代码在执行前需要先编译(编译就是找到某个域中的有效标识符),之后再从上至下地执行代码

  1. 浏览器进行编译,识别到第一行我们定义了一个变量a = 1。
  2. 浏览器继续识别,看到第三行我们定义了一个函数fun(),但此时浏览器并不会执行该函数。
  3. 到了第七行,浏览器看到一个函数调用,此时开始向上寻找该函数具体代码并执行。
  4. 显示最终结果: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命令声明变量之前,该变量都是不可用的,这就是暂时性死区

这篇文章是我在掘金的第一篇文章,还有许多的不足之处,如果有错误之处请指出,我会及时修改。

感谢观看!