作用域

79 阅读4分钟

作用域是什么

  1. 简单来说,它体现了JS引擎对变量名的处理机制。这个机制在不同语言中有不同实现,大体可以分两类:静态作用域(也称词法作用域)与动态作用域。
  2. 静态作用域(词法作用域)里,对于函数体中的一个符号,不会逐层检查函数的调用链,而是检查函数定义时的外部环境,即捕捉的是函数定义时该符号的绑定。

先来看一段代码

function func (){
    var vari = 12;
}
func()
console.log(vari) 
function func (){
    console.log(vari)
}
var vari = 12
func()

这里体现的就是JS中的变量作用域。 JS采用的是静态作用域。举个例子,

var a = 10;
function func (){
    console.log(a);
}
function func1 (){
    var a=5;
    func()
}

由于JS采用静态作用域,所以func定义时保留了对外层a变量的引用,结果为10。

  JS 作用域的特点

  1. 在JS中,作用域就像套娃一般,外层套内层。变量在调用中将会由调用点出发,向外层寻找符合变量名的变量,直至到达全局作用域。
  2. 一共有三种方式可以形成作用域,全局、函数、块。全局作用域在运行时便默认存在,我们可以使用的是函数与块作用域。

  还是举个例子,

//全局
function foo(){
   let vari = 10;//函数作用域
   if(vari==10){
       let vari = 20//块级作用域
       console.log(vari)
   }
   {
       let abc = 10//就算只有花括号也可以构成块级作用域
   }
   console.log(abc) 
   console.log(vari)
}
foo()

上面这个例子简单展示了块级作用域与函数作用域。

此外,函数(包括回调函数)及对象方法将会使用其被定义时所处的作用域,而非使用其实际调用时的作用域。这也引出了闭包,不过这里我们先不展开讲闭包。一个函数作用域的例子:

function foo(){
    console.log(a)
}
function bar(func){
    let a =10;
    func()
}
bar(foo)

好了,你已经学会了作用域的基本操作,那来试试这道题吧:

function foo(){
    let a = 10;
    {
        a=20;
        let b=30;
        let d=()=>{
            console.log(b)
        }
        {
            let b=40;
            console.log(a);
            console.log(b);
            d()
        }
        console.log(b)
        {
            let c=50;
            console.log(b);

        }
        console.log(c);
    }
    console.log(a)
}
foo()

注意,块级作用域只在使用let与const声明变量时可用,使用var声明变量不会触发块级作用域。在正常情况下我们推荐使用let声明变量,它拥有块级作用域,在重复定义时let会报错,而var只会默默地进行赋值操作。比如这个例子:

function foo(){
    var a=10;
    if(a){
        var a=20;
        console.log(a)
    }
    console.log(a);
}

变量提升

  在引擎层面上,JS会将每个函数作用域中的变量声明提升至函数顶端,无论他在哪(哪怕是不可能被执行的代码块),而对变量的赋值则会推迟到变量语句实际所处的位置。let与const也会有提升情况,不过这两个存在暂时性死区(TDZ)现象以模拟块级作用域。

function foo(){
    console.log(a)
    {
        console.log(a)
        let a=10;
    }
}

遮蔽

function foo(){
    let a=10;
    {
        let a=20;
        console.log(a);
    }
    console.log(a);
}

全局作用域

  • 在JS中,全局作用域可以使用globalThis进行引用。除此之外在各个环境中还有不同的引用全局作用域的方法,比如浏览器的window,Node.js的global,web Worker的self。
  • 由于模块(module)机制的出现,全局作用域的重要性逐渐降低,语句更多的是在自己所属的函数作用域下执行。

    LHS与RHS

  • 我们之前讲到JS引擎会沿着作用域向上寻找变量、直至全局作用域。那么问题来了,假如某个变量名直到全局作用域都没有找到(比如还未定义就使用这个变量)会发生什么呢?一般这种情况常见于书写错误,比如经典的mian、jvav。此时我们需要了解JS引擎的LHS与RHS(left hand search,左侧搜索以及右侧搜索)。
  • LHS与RHS最原始的定义是对在表达式两侧变量的查找。比如一个赋值表达式:a=b,对a的查找是LHS,对b的查找是RHS。引申之后LHS即对变量的赋值,RHS即对变量的引用。看如下代码:
    function foo (a){
        let b = a;
        return a+b
    }
    let c = foo(2)
    
  • 我们从c声明开始,c赋值触发一次LHS,调用函数触发一次RHS,参数赋值触发一次LHS,b赋值触发一次LHS,a调值触发一次RHS,a+b触发两次RHS。
  • 那么学习它对开发的意义是什么呢?意义在于全局作用域对LHS与RHS搜索失败的处理方法不同。LHS、即赋值搜索、失败后会在全局作用域创建同名变量,并对其赋值;而RHS、即取值搜索、失败后会抛出Reference Error。LHS的默认行为可能会导致调试过程中出现问题。不过在严格模式下,LHS失败也会抛出Reference Error。
  • 实践上不要刻意使用这个特性!每一个使用的变量都要尽可能的在作用域顶端完成声明! 这里讲解是用于排查错误使用。