JS -> 深入浅出JavaScript作用域和作用域链

473 阅读4分钟

前言

在JavaScript中有一个作用域(Scope)的特性,作用域可以将代码层次分层,按照特定的规则隔离和充分利用变量。以便子作用域可以访问父作用域,从而形成作用域链,本文将以人话的方式来聊聊作用域和作用域链。

本文内容概要如下: 内容概要

作用域(Scope)

1. 什么是作用域

作用域可以理解为对变量(包括函数、对象)的生命周期和可访问性,制定的一套规则。我们举一个简单的例子来说明:

var globalVariable = '全局变量'
function foo() {
    var localVariable = "局部变量";
}

foo();
console.log(globalVariable); // 全局变量
console.log(localVariable); // Uncaught ReferenceError: localVariable is not defined

从上边例子可知,localVariable是在函数作用域内定义,在foo() 执行完后,这个变量就被销毁,在全局作用域内是访问不到的。

在ES6之前,没有 块级作用域这个概念,只有全局作用域、函数作用域和动态作用域。 ES6之后,我们可以通过 关键字letconst来创建 块级作用域。

2.全局作用域

直接在script标签内的JS代码,都是全局作用域。有以下几种情况可以拥有全局作用域:

  • 最外层函数 和在最外层函数外面定义的变量拥有全局作用域

    var globalVariable = '全局变量'
    function foo() {
        var localVariable = "局部变量";
        function innerFoo() {
            console.log("innerFoo")
        }
        console.log(localVariable);
    }
    
    console.log(globalVariable); // 全局变量
    console.log(localVariable); // Uncaught ReferenceError: localVariable is not defined
    foo();    // 局部变量
    innerFoo();  // Uncaught ReferenceError: innerFoo is not defined
    
    // window
    console.log(window.globalVariable); // 全局变量
    window.foo();  // 局部变量
    

    由最后两句代码可知,在全局作用域内,创建的变量都会变为window对象的属性,创建的变量都会变为window的方法保存

  • 在函数或代码块{}内未定义的变量也拥有全局作用域

  function foo() {
     variable = "我仍然是全局变量";
     var localVar = '局部变量'

 }
 foo() // 要先执行这个函数
 console.log(variable); // 我仍然是全局变量
 console.log(localVar); // Uncaught ReferenceError: localVar is not defined

3.函数作用域

在函数内部定义的变量,拥有局部作用域。调用函数时创建函数作用域,函数执行完毕,作用域销毁。在局部作用域内是封闭的,从外层作用域内无法访问函数内部的作用域。

function foo(){
    var localVar = "函数内部作用域"
}

foo()
console.log(localVar); // Uncaught ReferenceError: localVar is not defined

4.块状作用域

ES6之后,使用letconst自变量,可以产生块状作用域,块状作用域可谓好处多多,自己去度娘一波

if(true) {
    let blockVar = "块状作用域"
    var variable = "HopeSun"
}

console.log(variable)  // HopeSun
console.log(blockVar)  // Uncaught ReferenceError: blockVar is not defined

可见,在块状作用域之外的地方是访问不了变量的

5.动态作用域

在执行阶段才能确定的作用域,叫动态作用域

var variable = 'HopeSun'
function foo() {
    console.log(this.variable)
}

foo(); // HopeSun
foo.bind({variable: "variable"})() // variable

上述代码中bind把作用域的范围指向了{variable: "variable"},因此this指向的是当前作用域对象

词法作用域和动态作用域

作用域是在代码编写的时候决定,还是在在吗执行过程中决定?

var variable = 'HopeSun'

console.log(variable)
function foo() {
    console.log(variable)
}

如上例中,在写代码时我们就知道 variable 是全局作用域,如果在函数内部定义的变量就是函数作用域,用专业术语就是:词法作用域。通俗讲就是:变量的定义域是定义时决定而不是在运行时决定,因此词法作用域取决于源码,通过静态分析可以确定,因此词法作用域也称为静态作用域。相反,只有执行阶段才能决定的变量,就是动态作用域

作用域链

1.自由变量

首先认识一下自由变量

var a = 100
function foo() {
    var b = 200
    console.log(a) // a 是自由变量
    console.log(b)
}

foo()

在上边代码中,当访问a 变量时,在当前作用域内并没有定义(对比变量 b)。当前作用域没有定义的变量,这称为 自由变量。自由变量的值如何得到,向父级作用域寻找(不严谨)。

2. 什么是作用域链

对于自由变量,当获取值时,会向父级作用域寻找,若父级作用域也没有,就会一层一层向上寻找,知道找到全局作用域,若无,就放弃。这用一层一层的关系,就是** 作用域链**。

var a = 100
function foo() {
    var b = 200
    function bar(){
        console.log(a) // 自由变量
    }
    bar()
}

foo()

如上代码,当要获取a值时,寻找顺序:bar->foo->全局作用域

3. 自由变量的取值

关于自由变量的值,上文提到要到父作用域中取,其实有时候这种解释会产生歧义。

var x = 10
function foo() {
    console.log(x)
}

function show(fun) {
    var x = 20; 
    (function(){
        fun(); // 10
    })()
}

show(foo);

在foo函数中,取自由变量x的值时,要到哪个作用域中取?——要到创建foo函数的那个作用域中取,无论foo函数将在哪里调用。

因此,在自由变量获取值时,要到创建这个函数的作用域中取值,这里强调的是“创建”,而不是“调用”

参考文章和书籍

深入理解JavaScript作用域和作用域链
Scope(作用域)