一个例子!浅谈作用域链

374 阅读5分钟

前言

作为热身,我们先来看一些简单的代码。

function a() {
    var num = 1;
    function b(){
        var num = 10;
        console.log(num);   //思考一下这里会输出什么结果?
    }
    b();
    console.log(num);       //思考一下这里会输出什么结果?
}
a();

相信大家心中已经有了答案,毫无疑问,这里会依次输出 101 ,接下来我们增加一些难度。

function a() {
    var num = 1;
    function b(){          //将原有的函数b()中的变量num的申明移除
        console.log(num);   
    }
    b();
    console.log(num);
}
a();

这里依次输出 11 ,或许你的心中已经开始产生困惑,不急,让我们来看看第三段代码。

function a() {             //将原有的函数a()中的变量num的申明移除
    function b(){
        var num = 10;
        console.log(num);   
    }
    b();
    console.log(num);    
}
a(); 

现在再来运行这段代码,就会发现这里产生了严重的错误,说的是 num is not defined。 没有被定义?可是明明上面有一行 var num = 10; 的代码,可是并没有起作用。

好的,我们如果进行更加细致的划分,就会发现其实第一行 console.log 有输出,第二行console.log 会报错。

通过上面三段不同的代码,我们似乎可以总结出一些规律来:

· 内部的函数可以访问到外部函数的变量

· 外部的函数无法访问到内部函数的变量

当然这是目前粗浅的认识,看到这里,我们也隐喻感觉有什么东西在限制着变量num,使得它时而有值,时而未被定义,其实这就是作用域在起作用。

作用域(scope)

我们已经知道作用域会对变量产生影响,不光**变量** ,其实 函数 和 **对象**也会受其影响,它就像一个领域一般,限制了变量、函数和对象的可访问性,使得三者只能在作用域内才能起作用。那么我们要如何从一段代码中看出作用域在何处呢?

作用域的分类

  1. 全局作用域
  2. 函数作用域
  3. 块级作用域

1. 全局作用域

var num = 1;
console.log(num); // 1

像上面的代码一样,将变量num定义在全局,就可以被访问到

2. 函数作用域

function fn(){
    var num = 1;
}
console.log(num); // num is not defined

当我们将变量num定义在函数 fn() 中,外部无法访问到num,这样变量num就在函数 fn() 的函数作用域当中。

3. 块级作用域

{
    let num = 1;
}
console.log(num); // num is not defined

利用一对花括号将变量num的定义包裹在内,这样花括号外也将无法访问到num,注意这里是用 let 定义了变量num,与之前的 var 定义变量有所不同。在块语句中使用 letconst 来申明,将会创建一个新的作用域,及块级作用域。像 if 和 switch 条件语句, for 和 while 循环语句,都可以用此方法来创建块级作用域。

介绍完作用域,我们似乎明白了开头的第三段代码为什么会出现未定义的情况了,变量num被定义在函数作用域中,外层函数将无法访问到它。但是,第二段代码的内部函数为什么又可以访问到外部函数的变量的疑问还没有得到解答,为此我们引入作用域链的概念。

作用域链(scope chain)

作用域链,听着就像是将各个作用域串连起来一般。在函数执行的时候,会创建一个称为执行期上下文的内部对象( AO 对象),在这个对象中会被记录着这个函数内部的变量、函数和对象。而作用域链就是将多个AO对象连续引用产生的链式结构。

(以开头第一段代码为例,作用域链结构如下图所示)

索引对象名
0b:AO
1a:AO
2GO(Global Object)

在代码执行时,新创建的执行期上下文会在链表的最上面,并且在函数执行完毕,它所产生的执行期上下文会被销毁。而最下面的GO对象就是指向 window ,及全局作用域。

当我们需要对某个变量进行操作时(比如变量num),我们会从作用域链的顶部开始查找这个变量,如果这个对象中无法查找到我们想要的变量,这样我们就会到下一个对象中查找,直至全局作用域查找也无结果,那么就像第三段代码那样抛出未定义的错误。

说完这些,我们似乎也就可以理解为什么内部的函数可以访问到外部函数的变量了。

当然作为补充,我们再来思考一下,外部的函数无法访问到内部函数的变量这个问题。就像上面说得那样,函数b()在执行完成之后,它所产生的执行期上下文会被销毁,因此定义的变量num也会随之消失,这时即使从作用域链顶部开始查找也无济于事。现在我们也就明白了这两句看似结论的话的背后逻辑了。

最后

我们再来看一段不一样的代码来加深我们的记忆。

var num = 10;
function b() {
   console.log(num);   //思考一下这里会输出什么结果?
}
function a() {
   var num = 20;
   b();
}
a();

我们似乎看出了一丝丝的不同,没错,函数b()的申明被放到了函数a()的外面,当时执行依然在内部,那么最后的结果会有所不同吗?

在让我们回忆一下之前所说的,函数的执行会生成AO,并且新生成的AO会放置到作用域链的顶部,在函数执行完成之后,AO将会被销毁,如此思考便可发现这段代码生成了两条作用域链,及属于函数a()和函数b(),函数b()想要访问变量num,便会在作用域链中进行查询,在全局作用域中便可查到,因此最后的结果便是打印出10