前言
新手哈基米搞不懂什么是作用域链?深入了解JS代码的执行过程让你秒懂作用域链!
经典案例
js
function fn() {
console.log(myName);
}
function fn2() {
var myName = '南北绿豆'
fn()
}
var myName = '哈基米'
fn2()
- 究竟是会出现"哈基米",还是"南北绿豆"呢?诶,先不要着急哈气。想要真正弄清输出结果是"哈基米"还是"南北绿豆"。我们先要深入理解一下这段js代码究竟是怎样的一个执行过程。
作用域与它的动态载体
作用域
- 在程序中定义变量的区域,该位置决定了变量的生命周期。通俗来讲,作用域就是变量和函数的可访问范围
函数执行上下文 : 函数作用域的动态载体
- 什么是函数执行上下文?
- 当一个函数被调用时,js引擎会先创建一个对应的"函数执行上下文"对象,并将函数中声明的变量与函数都存在其中。供函数执行时使用,由此才实现了函数作用域的概念。
- 其主要组成部分:
- 变量环境:保存使用var关键字声明的对象和函数内部声明的子函数,由此实现常见的"函数作用域"。
- 词法环境:保存使用let关键字声明的对象,主要用于实现"块级作用域"的功能,非本文重点,不做详细描述。
- 例子:
js
function fn2() {
var myName = '南北绿豆'
fn()
}
图片的抽象表达:js引擎会为本函数创建一个函数上下文

- 新手可能都会有个疑问?
- 为什么fn函数没有存在这个函数执行上下文当中呢?
- 欸,这就告诉你fn函数的声明被存在哪里。
- 为什么fn函数没有存在这个函数执行上下文当中呢?
全局执行上下文:全局作用域的动态载体
js
function fn() {
console.log(myName);
}
function fn2() {
var myName = '南北绿豆'
fn()
}
var myName = '哈基米'
fn2()
- 全局执行上下文与函数执行上下文类似
-
由这份代码可知,在该js文件的全局下,声明了两个函数:fn与fn2,声明了一个变量: myName。
-
所以js引擎同样会为其创建一个"全局执行上下文供全局参考,由此形成全局作用域。

待解决的疑问
- 光靠这些我们还无法得知fn2是如何调用到fn函数的,所以我们还得了解这两个执行上下文的出现顺序,以及js引擎是如何对他们进行处理,才能够正常的执行代码。所以我们的新朋友"调用栈"便可以登场了。
js引擎的好兄弟:调用栈
- 帮助js引擎控制代码执行顺序的"无形态大手"
什么是调用栈?
- 后进先出:它与我们所知的"栈"这一数据结构一样,有着"后进先出"(LIFO)的特点。
- "函数执行上下文生命周期"的管理者:js引擎通过调用栈间接控制"函数执行上下文的生命周期"来管理"函数的执行顺序",由此直接决定代码的执行流程。
- 具体过程是: 每当一个函数被调用时,js引擎会为其创建一个"函数执行上下文",然后将该"函数执行上下文"压入栈顶,js引擎就会开始执行处于栈顶的函数,当函数执行完毕,便会让栈顶元素出栈。
函数执行上下文被压入栈的过程
- 全局被编译时,js引擎创建全局执行上下文并将其压入调用栈,然后开始执行全局沿着全局一行一行往下执行。

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

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

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

怎么是哈基米?
作用域链:作用域之间以outer指针为链接,形成的单向链表
outer指针:函数执行上文间的链接者,向外寻找的罪魁祸首
- outer指针:当一个函数执行上文被创建时,其内部存在一个指针outer指向该函数被定义时所在的词法作用域
- 词法作用域:一个函数被编译时一定会用一个outer指针记录该执行上下文(作用域)的外层(作用域)是谁。
- 例子:
js
var num=0;
function f1()
{
console.log(num);
}
fn1()

由此形成了一个最简单的作用域链
- 当执行函数内部时,在函数执行上文内部无法找到num的声明,js引擎便会沿着outer指针指向的全局执行上下文,寻找num的声明
- 全局执行上文中存在num的声明,且num为1
- 于是程序执行输出为1
作用域链:作用域之间以outer指针为链接,形成的单向链表
- 由上述逻辑不断推导,最开始的"南北绿豆"消失之迷便迎刃而解了。
js
function fn() {
console.log(myName);
}
function fn2() {
var myName = '南北绿豆'
fn()
}
var myName = '哈基米'
fn2()

执行fn()函数时,寻找myName的定义的过程:
- 第一步:在fn函数执行上文中寻找,未查找到。
- 第二部:沿着outer指针指向查找上一级,在全局执行上下文中寻找到myName="哈基米"
- 输出哈基米
- 由此我们发现:作用域链的形成与函数调用的过程无关。
- 作用域链的形成:依赖outer指针的指向。
总结
- 作用域: 在程序中定义变量的区域,该位置决定了变量的生命周期。通俗来讲,作用域就是变量和函数的可访问范围
- 调用栈:LIFO(后进先出)的执行上下文栈,用于管理函数调用顺序
- 执行上下文:作用域的动态载体,存储变量的具体环境
- outer指针:每一个函数内部都会存在一个指针outer,指向该函数的外层作用域(其所在的词法作用域)
- 词法作用域:函数或者变量被定义时的代码嵌套位置
- 作用域链:作用域之间以outer指针为链接,形成的单向链表