搞不懂作用域链?这篇文章让你一眼秒懂!

633 阅读6分钟

前言

新手哈基米搞不懂什么是作用域链?深入了解JS代码的执行过程让你秒懂作用域链!

经典案例

function fn() {
    console.log(myName);
}
function fn2() {
    var myName = '南北绿豆'
    fn()
}
var myName = '哈基米'
fn2()

究竟是会出现"哈基米",还是"南北绿豆"呢?诶,先不要着急哈气。想要真正弄清输出结果是"哈基米"还是"南北绿豆"。我们先要深入理解一下这段js代码究竟是怎样的一个执行过程。

作用域与它的动态载体

作用域

在程序中定义变量的区域,该位置决定了变量的生命周期。通俗来讲,作用域就是变量和函数的可访问范围

函数执行上下文 : 函数作用域的动态载体

  1. 什么是函数执行上下文?

    • 当一个函数被调用时,js引擎会先创建一个对应的“函数执行上下文”对象,并将函数中声明的变量与函数都存在其中。供函数执行时使用,由此才实现了函数作用域的概念。
  2. 其主要组成部分:

    • 变量环境:保存使用var关键字声明的对象和函数内部声明的子函数,由此实现常见的“函数作用域”。
    • 词法环境:保存使用let关键字声明的对象,主要用于实现“块级作用域”的功能,非本文重点,不做详细描述。
  3. 例子:

function fn2() {
    var myName = '南北绿豆'
    fn()
}

图片的抽象表达:js引擎会为本函数创建一个函数上下文

75EE8152-FE72-438e-A284-A224F6A6E135.png
  1. 新手可能都会有个疑问?

    • 为什么fn函数没有存在这个函数执行上下文当中呢?
      • 欸,这就告诉你fn函数的声明被存在哪里。

全局执行上下文:全局作用域的动态载体

function fn() {
    console.log(myName);
}
function fn2() {
    var myName = '南北绿豆'
    fn()
}
var myName = '哈基米'
fn2()
  1. 全局执行上下文与函数执行上下文类似

    • 由这份代码可知,在该js文件的全局下,声明了两个函数:fn与fn2,声明了一个变量: myName。

    • 所以js引擎同样会为其创建一个“全局执行上下文供全局参考,由此形成全局作用域。

461C8CE8-56E8-431a-80AB-0A5FD7C8D366.png

待解决的疑问

光靠这些我们还无法得知fn2是如何调用到fn函数的,所以我们还得了解这两个执行上下文的出现顺序,以及js引擎是如何对他们进行处理,才能够正常的执行代码。所以我们的新朋友“调用栈”便可以登场了。

js引擎的好兄弟:调用栈

帮助js引擎控制代码执行顺序的“无形态大手”

什么是调用栈?

  1. 后进先出:
    • 它与我们所知的“栈”这一数据结构一样,有着“后进先出”(LIFO)的特点。
  2. “函数执行上下文生命周期”的管理者:
    • js引擎通过调用栈间接控制“函数执行上下文的生命周期”来管理“函数的执行顺序”,由此直接决定代码的执行流程。
      • 具体过程是: 每当一个函数被调用时,js引擎会为其创建一个“函数执行上下文”,然后将该“函数执行上下文”压入栈顶,js引擎就会开始执行处于栈顶的函数,当函数执行完毕,便会让栈顶元素出栈。

函数执行上下文被压入栈的过程

全局被编译时,js引擎创建全局执行上下文并将其压入调用栈,然后开始执行全局沿着全局一行一行往下执行。

845D93CB-BD81-438d-A3B5-9D490ADB5C7A.png

当准备执行fn2()时,js引擎会在全局执行上下文中寻找fn2()的定义,发现其存在。js引擎对其进行编译并创建对应函数执行上下文并将其压入调用栈,然后开始执行函数fn2,一行一行往下执行。

C06E8B4E-D793-49e6-9A11-21AEFA2F357C.png

当准备执行fn()时,js引擎会在fn2函数执行上下文中寻找fn()的定义,未发现定义,于是继续向全局执行上下文中寻找fn()的定义。发现fn()定义,js引擎对其进行编译并创建对应函数执行上下文压入调用栈,然后开始执行fn,一行一行往下执行。

9C03BEC2-0DD7-4b97-A119-037D2BC2A8C7.png

消失的“南北绿豆”:交流的失败?

js引擎对变量与函数的寻找过程难道真的只是“由内向外寻找”?如果是这样输出的结果应该是“南北绿豆”才对。

951C6781-E75B-4942-BB5D-BBEE88EA7C0E.png

怎么是哈基米?

作用域链:作用域之间以outer指针为链接,形成的单向链表

outer指针:函数执行上文间的链接者,向外寻找的罪魁祸首

  • outer指针:当一个函数执行上文被创建时,其内部存在一个指针outer指向该函数被定义时所在的词法作用域
  • 词法作用域:一个函数被编译时一定会用一个outer指针记录该执行上下文(作用域)的外层(作用域)是谁。
  • 例子:
var num=0;
function f1()
{
    console.log(num);
}
fn1()
9E981168-8FAF-4a64-AB56-40E2074FAC46.png

由此形成了一个最简单的作用域链

  • 当执行函数内部时,在函数执行上文内部无法找到num的声明,js引擎便会沿着outer指针指向的全局执行上下文,寻找num的声明
  • 全局执行上文中存在num的声明,且num为1
  • 于是程序执行输出为1

作用域链:作用域之间以outer指针为链接,形成的单向链表

  • 由上述逻辑不断推导,最开始的“南北绿豆”消失之迷便迎刃而解了。
function fn() {
    console.log(myName);
}
function fn2() {
    var myName = '南北绿豆'
    fn()
}
var myName = '哈基米'
fn2()
882B21C2-CB2A-4e9b-8527-4C939FA5927E.png

执行fn()函数时,寻找myName的定义的过程:

  • 第一步:在fn函数执行上文中寻找,未查找到。
  • 第二部:沿着outer指针指向查找上一级,在全局执行上下文中寻找到myName=“哈基米”
  • 输出哈基米
  • 由此我们发现:作用域链的形成与函数调用的过程无关。
  • 作用域链的形成:依赖outer指针的指向。

总结

  1. 作用域: 在程序中定义变量的区域,该位置决定了变量的生命周期。通俗来讲,作用域就是变量和函数的可访问范围
  2. 调用栈:LIFO(后进先出)的执行上下文栈,用于管理函数调用顺序
  3. 执行上下文:作用域的动态载体,存储变量的具体环境
  4. outer指针:每一个函数内部都会存在一个指针outer,指向该函数的外层作用域(其所在的词法作用域)
  5. 词法作用域:函数或者变量被定义时的代码嵌套位置
  6. 作用域链:作用域之间以outer指针为链接,形成的单向链表