javascript之作用域三(理解作用域链)

933 阅读5分钟

三、作用域链

js访问一个变量时会优先在该作用域内(访问时的那个作用域)寻找是否声明过这个变量,如果该变量已经存在,则直接使用它的值,否则会寻找该作用域的‘父作用域/上级作用域',依次类推,直到找到全局作用域为止。通俗地讲,当声明一个函数时,局部作用域一级一级向上包起来,就是作用域链。

首先看一个多级作用域的栗子:

//多级作用域
//======>此处是1级作用域
var gender = "男";
function fn(){ // ======>从这里开始是2级作用域
    //gender 可以访问  gender是全局作用域的变量,任何地方都可以访问
    //age    可以访问
    //height 不能访问

    return function(){ // ======>从这里开始是3级作用域
        //gender  可以访问
        //age     可以访问
        //height  可以访问
        var height = 180;
    }
    var age = 5;
}

由于作用域是相对于变量而言的,而如果存在多级作用域,这个变量又来自于哪里?这个问题就需要好好地探究一下了,我们把这个变量的查找过程称之为变量的作用域链

作用域链的意义:查找变量(确定变量来自于哪里,变量是否可以访问)

简单来说,查找一个变量来自哪里,能否被访问,需要以下四步:

  1. 查看当前作用域,如果当前作用域声明了这个变量,可以直接访问
  2. 查找当前作用域的上级作用域,也就是当前函数的上级函数,看看上级函数中有没有声明,有就返回变量,没有继续下一步
  3. 再查找上级函数的上级函数,直到全局作用域为止,有则返回,无则继续
  4. 如果全局作用域中也没有,我们就认为这个变量未声明(xxx is not defined)

这四步操作就描述了整个作用域链及作用域链如何查找变量的过程。

  1. 当执行函数时,总是先从函数内部找寻局部变量
  2. 如果内部找不到(函数的局部作用域没有),则会向创建函数的作用域(声明函数的作用域)寻找,依次向上

代码分析:

  1. 当执行fn1时,创建函数fn1的执行环境,并将该对象置于链表开头,
  2. 然后将函数fn的调用对象放在第二位,最后是全局对象,
  3. 作用域链的链表的结构是fn1->fn->window。
  4. 从链表的开头寻找变量a,即fn1函数内部找变量a,找到了,结果是20。
  5. 同样,执行fn2时,作用域链的链表的结构是fn2->fn->window。
  6. 从链表的开头寻找变量a,即fn2函数内部找变量a,找不到,
  7. 于是从fn内部找变量a,找到了,结果是10。
  8. 最后在最外层打印出变量a,直接从变量a的作用域即全局作用域内寻找,结果为1。

下面我们来举几个特殊的栗子: 例子1:

function fn(callback){
    var age = 20;
    callback();
}
fn(function(){
    console.log(age);//报错
    //1.在当前作用域没有查找到age
    //2.查找上一级作用域:全局作用域
    //为何是全局作用域?
    //因为看上一级作用域,不是看函数在哪调用,而是看函数在哪编写的。
    //这种特别的作用域,叫做“词法作用域”
})

这个栗子比较特殊,可能很多人会认为输出20,因为函数调用的地方是fn函数内部,恰巧age又是声明在这个函数内部的,理所应当输出20。这是错误的! 现在分析下作用域链如何查找变量的:

  1. console.log(age)的时候,在当前作用域并没有查询到age变量。所以查找上一级作用域。
  2. 上级作用域是谁?这里需要引出一个概念,查找函数上级作用域,不是看函数在哪调用,而是看函数在哪编写。所以这样来看,上级作用域就是全局作用域。
  3. 在全局作用域中并没有声明age变量,所以console.log(age);就会报错。

例子2:

 var name="张三";
    function f1(){
        var name = "abc";
        console.log(name);
    }
    f1(); // abc

如果查找一个变量时,在当前作用域找到变量,不管上级、上上级有没有同名变量都不会再去寻找。

例子3:

  var name="张三";
    function f1(){
        console.log(name);
        var name = "abc";
    }
    f1(); // undefined

如果这个栗子能看懂说明已经了解变量提升,如果不懂,参考后面的文章。

例子4:

 var name = "张三";
    function f1(){
        var name = "abc";
        return function(){
            console.log(name);
            console.log(age);
        }
        var age = 18;
    }
    var fn = f1();
    fn();
    //abc 
    //undefined

这个栗子咋一看可能有点懵,但是记住之前一句很重要的话,查找函数上级作用域,不是看函数在哪调用,而是看函数在哪编写。

例子5:

var name="张三";
    function f1(){
        return {
            say:function(){
                console.log(name);
                var name="abc";
            }
        }
    }
    var fn=f1();
    fn.say();//undefined

当前作用域查到了变量,则不会再继续寻找,直接返回该变量的值,这里打印的时候变量声明但是未赋值,所以输出undefined。

如果以上几个栗子都能看懂,说明你已经掌握了作用域&作用域链,学好作用域和作用域链可以为理解闭包打下很好的基础。

6 闭包

提到作用域就不得不提到闭包,简单来讲,闭包外部函数能够读取内部函数的变量。

优点:闭包可以形成独立的空间,永久的保存局部变量。

缺点:保存中间值的状态缺点是容易造成内存泄漏,因为闭包中的局部变量永远不会被回收

产生闭包的根本原因是作用域链

7 总结

  1. 产生闭包的原因是由作用域链引起的
  2. 函数嵌套函数,被嵌套的函数就可以称为闭包
  3. 子函数可以使用父函数的变量(访问其他函数内部的局部变量)
  4. 让变量始终保存在内存中,避免自动垃圾回收(其实上面的例子中就已经用到了的)
  5. 对外提供公有属性和方法

详细讲解可以参考我写的闭包系列:

闭包 juejin.cn/post/684490…