彻底理解js的作用域链

御弟哥哥
• 阅读 1385

之前的文章中我已经介绍了执行上下文的变量对象。在这一篇文章我要介绍执行上下文的作用域链了。

执行上下文.作用域链(scope chain)

作用域链与变量对象有着密不可分的关系,因为作用域链就是变量对象的数组!其中第一个是当前函数的活动对象,第二个是当前活动函数的父亲上下文的活动对象,第三个是当前活动函数的爷爷上下文的活动对象,……,最后一个是全局上下文的活动对象。当执行引擎在解析一个变量名的时候,会沿着当前执行函数的作用域链查找,如果在某个活动对象中找到了,则使用它。找不到则报错。

什么叫父亲上下文?这个函数在哪个上下文中被声明,那个上下文就是此函数的父亲上下文,这个函数的声明会被保存到父亲上下文的变量对象中。上层上下文(也就是所有前辈上下文)对于作用域链的创建至关重要,我们会在文章后面讲。

举个栗子:

var global_var = -1;

function outter() {
    var outter_var = 111;
    console.log(
        "in outter, global_var = " + global_var +
        ", outter_var = " + outter_var
    );

    function inner() {
        var inner_var = 333;
        console.log(
            "in inner, global_var = " + global_var +
            ", outter_var = " + outter_var +
            ", inner_var = " + inner_var
        );
    }
    inner();
}
outter();
// 输出
// in outter, global_var = -1, outter_var = 111
// in inner, global_var = -1, outter_var = 111, inner_var = 333 

开始执行outter()之前,创建的执行上下文.作用域链是:
[outter的变量对象, 全局的变量对象]

  • 解析global_var的时候,在outter的变量对象中没有找到,因此在全局的变量对象中找,找到这个属性,所以可以打印出它的值-1。
  • 解析outter_var的时候,在outter的变量对象中就找到了,因此不用往后找了,直接打印111。

inner的执行上下文.作用域链是:
[inner的变量对象,outter的变量对象, 全局的变量对象]
变量解析的过程同上。

inner的父亲上下文是outter,outter的父亲上下文是全局上下文。


为什么js有函数级作用域

前一篇文章,我们解释过为什么js不是块级作用域,到这里我们又可以解释为什么js有函数级作用域了。如果你尝试在上面例子的outter()中打印inner_var,会报错。因为outter的作用域链是[outter的变量对象, 全局的变量对象],根本就没有inner的变量对象!自然也就找不到inner_var了。


作用域链是怎么创建的

虽然说作用域链是在函数执行之前,与执行上下文一起创建的,但是实际上其中很大一部分内容在定义的时候就已经确定了。

作用域链这个数组可以分成两部分:

  1. 第一个元素就是本函数的变量对象
  2. 后面的所有元素都是上层上下文的变量对象(按照由近到远的顺序排列)。在网上的一些资料中,所有上层上下文的链叫做[[Scope]]。

第一部分是在函数执行之前创建的。第二部分其实在函数声明的时候就已经确定了,[[Scope]]是函数的一个属性,从函数声明到函数销毁一直存在,且不会改变。我们在文章开头也说过,决定上层上下文的是函数在哪里声明,而不是函数在哪里调用。 因此,本函数的变量对象一旦创建好,执行引擎就可以合并[[Scope]],创建出一个作用域链了。

可以用这种方式轻松地找出[[Scope]]:[[Scope]]就是函数被声明时的上下文的作用域链。

举个例子:

function inner() {
    // 创建作用域链:[inner的变量对象,全局上下文的变量对象],其中没有outter的变量对象!怪不得找到不到outter_var!
    console.log("in inner, outter_var = "+outter_var);
}
// inner声明完毕,[[scope]]已经确定
// 因为此时的上下文是全局上下文,所以[[scope]]就是全局上下文的作用域链,也就是全局变量对象

function outter() {
    var outter_var = 111;
    inner();  
}

outter(); // 报错:outter_var is not defined 

如果将inner声明在outter里面,就不会报错啦:

function outter() {
    var outter_var = 111;

    function inner() {
        // 创建作用域链:[inner的变量对象,outter的变量对象,全局上下文的变量对象]
        console.log("in inner, outter_var = " + outter_var);
    }
// 声明完毕,[[scope]]已经确定
// 因为此时的上下文是:outter上下文
// outter上下文的作用域链是:[outter上下文的变量对象,全局上下文的变量对象]
// 所以[[scope]]是:[outter上下文的变量对象,全局上下文的变量对象]
    inner();
}

outter();
//  输出:in inner, outter_var = 111 

解释闭包的原理

如果看过我之前的文章彻底理解js闭包现在应该已经能掌握闭包的使用方法了。是时候给闭包最后一击了!我们接下来要用作用域链来解释闭包的原理!
看看我在“彻底理解js闭包”中举的一个栗子:

function outter() {
  var private= "I am private";
  function show() {
    console.log(private);
  }
// [[scope]]已经确定:[outter上下文的变量对象,全局上下文变量对象]
  return show;
}

var ref = outter();
// console.log(private);  // 尝试直接访问private会报错:private is not defined
ref(); // 打印I am private 

outter执行完以后,private不会被销毁,并且只能被show方法所访问,而尝试直接访问它会出现报错。

  • 首先我们解释为什么不能直接从外部访问它:
    执行到报错的那一行的时候,当前上下文的作用域链是:[全局上下文变量对象],其中的变量对象中并没有private(private在outter的上下文变量对象中)!这就是为什么我们不能直接访问private
  • 然后我们解释为什么能够通过show来访问private: 调用ref(也就是show)的时候,因为show的[[scope]]是:
    [outter变量对象,全局变量对象]
    所以创建的作用域链是:
    [show变量对象,outter变量对象,全局变量对象]
    查找private的时候,在outter变量对象中找到了,所以可以访问到。

可见,闭包之所以能够“记忆”它被创建时候的环境,就是因为[[scope]]将函数声明时所在上下文的作用域链给存起来了!


点赞
收藏
评论区
推荐文章
blmius blmius
2年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Souleigh ✨ Souleigh ✨
3年前
JS - 作用域
一、作用域作用域,即变量(变量作用域又称上下文)和函数生效(能被访问)的区域或集合换句话说,作用域决定了代码区块中变量和其他资源的可见性举个例子function myFunction(){    let inVariable  "函数内部变量";}myFunction();//要先执行这个函数,否则根本不知
Jacquelyn38 Jacquelyn38
2年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
御弟哥哥 御弟哥哥
3年前
彻底理解js的执行上下文,以及变量对象
在js中,执行上下文(ExecutionContext)是非常重要的一种对象,它保存着函数执行所需的重要信息,其中有三个属性:变量对象(variableobject),作用域链(scopechain),this指针(thisvalue),它们影响着变量的解析,变量作用域和函数this的指向。上下文栈(ExecutionContextS
Stella981 Stella981
2年前
Android So动态加载 优雅实现与原理分析
背景:漫品Android客户端集成适配转换功能(基于目标识别(So库35M)和人脸识别库(5M)),导致apk体积50M左右,为优化客户端体验,决定实现So文件动态加载.!(https://oscimg.oschina.net/oscnet/00d1ff90e4b34869664fef59e3ec3fdd20b.png)点击上方“蓝字”关注我
Wesley13 Wesley13
2年前
ES6 简单整理
1.变量声明let和constlet与const都是块级作用域,letfunctionname(){letage12;//age只在name()函数中存在}constconstname'tom'name'jack'//
Wesley13 Wesley13
2年前
35岁是技术人的天花板吗?
35岁是技术人的天花板吗?我非常不认同“35岁现象”,人类没有那么脆弱,人类的智力不会说是35岁之后就停止发展,更不是说35岁之后就没有机会了。马云35岁还在教书,任正非35岁还在工厂上班。为什么技术人员到35岁就应该退役了呢?所以35岁根本就不是一个问题,我今年已经37岁了,我发现我才刚刚找到自己的节奏,刚刚上路。
Wesley13 Wesley13
2年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
3个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这