彻底理解js的作用域链

御弟哥哥 等级 688 0 0

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

执行上下文.作用域链(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]]将函数声明时所在上下文的作用域链给存起来了!


收藏
评论区

相关推荐

js 基础之弱类型/变量提升/TDZ/块作用域/重复定义/Object.freeze()
弱类型 在JS中变量类型由所引用的值决定 var web "helloWorld"; console.log(typeof web); //string web 99; console.log(typeof web); //number web {}; console.log(typeof web); //object 变量提升
JS - 作用域
一、作用域 作用域,即变量(变量作用域又称上下文)和函数生效(能被访问)的区域或集合 换句话说,作用域决定了代码区块中变量和其他资源的可见性 举个例子 function myFunction() {     let inVariable  "函数内部变量"; } myFunction();//要先执行这个函数,否则根本不知
44道JS难题,还不来考考?
国外某网站给出了44道JS难题,试着做了下,只做对了17道。这些题涉及面非常广,涵盖JS原型、函数细节、强制转换、闭包等知识,而且都是非常细节的东西,透过这些小细节可以折
44道JS难题,做对一半就是高手
国外某网站给出了44道JS难题,试着做了下,只做对了17道。这些题涉及面非常广,涵盖JS原型、函数细节、强制转换、闭包等知识,而且都是非常细节的东西,透过这些小细节可以折
彻底理解js的执行上下文,以及变量对象
在js中,执行上下文(Execution Context)是非常重要的一种对象,它保存着函数执行所需的重要信息,其中有三个属性:变量对象(variable object),作用域链(scope chain),this指针(this value),它们影响着变量的解析,变量作用域和函数this的指向。 上下文栈(Execution Context S
彻底理解js的作用域链
在之前的文章(https://www.helloworld.net/p/G4dFV7tALU4J)中我已经介绍了执行上下文的变量对象。在这一篇文章我要介绍执行上下文的作用域链了。 执行上下文.作用域链(scope chain) 作用域链与变量对象有着密不可分的关系,因为作用域链就是变量对象的数组!其中第
彻底理解js闭包
在文章开头,我先放出MDN给出的定义: 闭包是指那些能够访问独立(自由)变量的函数 (变量在本地使用,但定义在一个封闭的作用域中)。换句话说,这些函数可以“记忆”它被创建时候的环境。 现在不需要看懂它,我会在第一个例子中解释清楚它的意思。让我们开始吧! 2018.3.20更新:现在MDN上的定义已经改为:"A closure is the comb
Python 中的闭包
闭包定义: 如果在一个内部函数里,对在外部作用于(但不是在全局作用域)的变量进行引用,那么内部函数就被认为是闭包 Python 中的闭包 原文出处: 田小计划(http://www.cnblogs.com/wilber2013/p/4658894.html) 闭包(closure)是函数式编程的重要的语法结构
JS核心原理理解闭包
前置概念在正式看闭包之前,我们先来学习一下前置知识,那就是JS中的作用域,我们知道,在ES5之中,作用域分为两种:全局作用域和函数作用域,随着ES6的到来,新增了块级作用域,想更好的理解闭包,那么搞清楚作用域是首要条件全局作用域我们知道,对于变量而言,我们一般会分成两类:全局变量和局部变量,一般定义在最外围环境的为全局变量,定义在函数当中的为局部变量,在we
人工智能数学基础10:域、函数及相关概念
一、运算封闭若从某个非空数集中任选两个元素(同一元素可重复选出),选出的这两个元素通过某种(或几种)运算后的得数仍是该数集中的元素,那么,就说该集合对于这种(或几种)运算是封闭的。如自然数集N对加法,乘法运算是封闭的,整数集Z对加、减、乘法运算是封闭的,有理数集、复数集对四则运算是封闭的,初等函数集合对不定积分运算是不封闭的。二、数域数域简称域,设P是由一些
JS - 从执行上下文的角度来理解闭包
今天看到一篇关于闭包的文章,里面有这样一句话 “就我而言对于闭包的理解仅止步于一些概念,看到相关代码知道这是个闭包,但闭包能解决哪些问题场景我了解的并不多”,这说的不就是我么,每每在面试中被问及什么是闭包,大部分情况下得到的答复是(至少我以前是)A函数嵌套B函数,B函数使用了A函数的内部变量,且A函数返回B函数,这就是闭包。而往往面试官想要听到的并不是这样的
你所知道的JS变量作用域
变量的作用域,指的是变量在脚本代码中的可读、可写的有效范围,也就是脚本代码中可以使用这个变量的区域。在ES6之前,变量的作用域主要分为全局作用域、局部作用域(也称函数作用域)两种;在ES6及其之后,变量的作用域主要分为全局作用域、局部作用域、块级作用域这3种。相应作用域变量分别称为全局变量、局部变量、块级变量。全局变量声明在所有函数之外;局部变量是在函数体内
秒懂js作用域与作用域链
JavaScript 中有一个被称为作用域(Scope)的特性。虽然对于许多新手开发者来说,作用域的概念并不是很容易理解,本文我会尽我所能用最简单的方式来解释作用域和作用域链,希望大家有所收获!好了下面开始我们的正文作用域常见的解释(什么是作用域)1. 一段程序代码中所用到的名字并不总是有效,而限定它的可用性的范围就是这个名字的作用域;2. 作用域规定了
你不可不知的JS面试题(第三期)
1、什么是闭包?如图所示,闭包就是一个定义在函数内部的函数,其作用是将函数内部和函数外部连接起来。大家知道,作用域的问题,就是在函数内部定义的变量称为局部变量,外部取不到值。下面我们通过代码来更加详细地看一下: function A()        let x  1;        return function B()            c
一篇文章带你了解JavaScript作用域
在JavaScript中,对象和函数也是变量。在JavaScript中,作用域是你可以访问的变量、对象和函数的集合。JavaScript 有函数作用域: 这个作用域在函数内变化。一、本地JavaScript变量一个变量声明在JavaScript函数内部,成为函数的局部变量。局部变量有局部作用域: 它们只能在函数中访问。JS://code here can n