美文网首页大前端-万物皆可JS
JavaScript作用域学习笔记

JavaScript作用域学习笔记

作者: ltaoo | 来源:发表于2017-02-21 00:45 被阅读39次

    总结来说很简单:

    • 函数运行在定义时的作用域中
    • 变量查找会从当前作用域开始查找,找不到则到下一层作用域查找,直到找到并返回或者返回 undefined

    实际例子

    var name = "global"
    function echo() {
      console.log(name)
    }
    
    echo()
    

    打印global毫无疑问,因为在echo()函数作用域没有找到,就到外层作用域中寻找,找到了值为globalname变量。

    如何查找

    在调用某个函数时,会从函数这个对象上拿到[[scope]]属性对应的作用域链,并且将函数的变量对象也放入到这个作用域链中,而该作用域链是在函数定义时就确定了的。

    同样是上面的例子,作用域链的变化是这样的:

    // 调用 echo() 函数前
    scopes = {
      0: {
        name: 'global',
        echo: {
          name: 'echo',
          function: function () {console.log(name)},
          [[scope]]: scopes
        },
        console,
        parseInt: function () {...}
        // 等一些全局变量与方法
      }
    }
    

    重点在echo对象上有[[scope]]属性,指向的就是最外面的这个scopes,即作用域链。

    在调用echo()后,会将该函数的变量对象:

    {
      this: Window
    }
    

    放到echo对象的[[scope]]属性对应的scopes上,那就变成了这样:

    // 调用 echo() 时
    scopes = {
      1: {
        this: Window
      },
      0: {
        name: 'global',
        echo: {
          name: 'echo',
          function: function () {console.log(name)},
          [[scope]]: scopes
        },
        console,
        parseInt: function () {...}
        // 等一些全局变量与方法
      }
    }
    

    echo函数内寻找name就是指会先在

    {
      this: Window
    }
    

    这个变量对象上寻找名为name的键,如果没有找到就向下(将 0 视为下)寻找,所以就会在:

    {
        name: 'global',
        echo: {
          name: 'echo',
          function: function () {console.log(name)},
          [[scope]]: scopes
        },
        console,
        parseInt: function () {...}
        // 等一些全局变量与方法
    }
    

    这个变量对象上寻找,然后找到了值为globalname并返回。

    进阶问题

    尝试解答下面代码会打印出什么?

    
    var name = "global"
    
    function echo () {
      console.log(name) // 打印什么?
    }  
    
    function change() {
      var name = "local"
      echo()
    }
    
    change() 
    

    具体分析

    按照上面的分析过程,在调用change()之前,作用域链应该是这样的:

    // 调用 change() 前
    scopes = {
      0: {
        name: 'global',
        echo: {
          name: 'echo',
          function: function () {console.log(name)},
          [[scope]]: scopes
        },
        change: {
          name: 'change',
          function: function () {console.log(name)},
          [[scope]]: scopes
        },
        console,
        parseInt: function () {...}
        // 等一些全局变量与方法
      }
    }
    

    多了change这个键值对,其他没什么,然后调用change函数:

    // 调用 change 函数时
    scopes = {
      1: {
        name: 'local',
        this: Window
      },
      0: {
        name: 'global',
        echo: {
          name: 'echo',
          function: function () {console.log(name)},
          [[scope]]: scopes
        },
        change: {
          name: 'change',
          function: function () {console.log(name)},
          [[scope]]: scopes
        },
        console,
        parseInt: function () {...}
        // 等一些全局变量与方法
      }
    }
    

    如果在change函数内寻找name变量,找到的肯定是local毫无疑问。最后是echo函数的调用,也是这段代码的意义所在,调用时是这样的:

    // 调用 echo 函数时
    scopes = {
      1: {
        this: Window
      },
      0: {
        name: 'global',
        echo: {
          name: 'echo',
          function: function () {console.log(name)},
          [[scope]]: scopes
        },
        change: {
          name: 'change',
          function: function () {console.log(name)},
          [[scope]]: scopes
        },
        console,
        parseInt: function () {...}
        // 等一些全局变量与方法
      }
    }
    

    所以在echo函数内寻找不到name,就到下层变量对象找,找到了global。所以这段代码最终是打印出了global

    图片可能更为直观,灰色块为作用域链,即scopes,绿色块为变量对象,如下所示 ↓

    模拟流程

    浅拷贝?

    虽然到目前为止能够解释绝大部分的作用域问题,但还存在疑问:

    [[scope]]: scopes
    

    这里是指

    在一个函数被定义的时候, 会将它定义时刻的scope chain链接到这个函数对象的[[scope]]属性。

    我们都知道,对象是引用类型,那从 2 -> 3 的过程中,尤其是echo函数执行时,先从echo函数上拿到[[scope]]属性,这时候不应该是:

    scopes = {
      1: {
        name: 'local',
        this: Window
      },
      0: {
        name: 'global',
        echo: {
          name: 'echo',
          function: function () {console.log(name)},
          [[scope]]: scopes
        },
        change: {
          name: 'change',
          function: function () {console.log(name)},
          [[scope]]: scopes
        },
        console,
        parseInt: function () {...}
        // 等一些全局变量与方法
      }
    }
    

    这样的吗,因为[[scope]]保存的是scopes的内存地址啊,所以我能够自己解释为

    在函数定义时,将scopes进行浅拷贝并保存到[[scope]]属性上。

    所以上面的例子,作用域链严格来说应该是这样的:

    scopes = {
      0: {
        name: 'global',
        echo: {
          name: 'echo',
          function: function () {console.log(name)},
          [[scope]]: {
            0: {...}
          }
        },
        change: {
          name: 'change',
          function: function () {console.log(name)},
          [[scope]]: {
             0: {...}
          }
        },
        console,
        parseInt: function () {...}
        // 等一些全局变量与方法
      }
    }
    

    但是真的是这样吗?

    参考

    相关文章

      网友评论

        本文标题:JavaScript作用域学习笔记

        本文链接:https://www.haomeiwen.com/subject/qbvewttx.html