Skip to content

深入理解JS作用域链

说作用域链之前,我们需要先了解一些基础的概念

前言

在MDN中是这么定义作用域的:

作用域是当前的执行上下文,值和表达式在其中“可见”或可被访问。如果一个变量或表达式不在当前的作用域中,那么它是不可用的。作用域也可以堆叠成层次结构,子作用域可以访问父作用域,反过来则不行。

MDN认为Scope就是当前的执行上下文。

在stackoverflow的相关问题中,赞同数最高的是—函数的执行上下文就是该函数this的值。

接下来我就根据查阅资料的相关观点,并结合了自己的理解,来说一下作用域。

初始化全局对象

js引擎在执行代码钱,会在堆内存中创建一个全局对象:Global Object(GO)

该对象(GO)所有的作用域都可以访问,其中还有一个window指向自己

执行期上下文

js中有三种执行上下文,分别是

  1. 全局执行上下文 —— 这是一个默认的执行上下文, 一个程序只会有一个全局上下文,他在整个js脚本的生命周期内都会存在于执行堆栈的最底部,不会被销毁,全局上下文会生成一个全局对象window,并将this指向这个全局对象window上
  2. 函数执行上下文 —— 当一个函数被调用时,都会创建一个新的函数执行上下文,函数每次执行时对应的执行上下文都是独一无二的,所以多次调用一个函数会导致创建多个执行上下文,当函数执行完毕,它所产生的执行上下文被销毁。
  3. eval函数执行上下文 —— 执行在eval函数内部的代码有自己的执行上下文。

当函数执行时(前),会创建一个称为执行上下文的内部对象(AO)。一个执行上下文定义了一个函数执行时的环境,函数每次执行时对应的执行上下文都是独一无二的,所以多次调用一个函数会导致创建多个执行上下文,当函数执行完毕,它所产生的执行上下文被销毁。

作用域

作用域是指程序中定义常量、变量、函数、赋值等信息,通俗的理解,作用域就是变量和函数的可访问范围以及控制变量和函数的可见性和生命周期

作用域就是当前执行上下文的一个对象,该作用域中的变量,以属性的形式存放在这个变量中。

当使用到一个变量时,会在当前作用域中查找该变量,如果在当前作用域中没有找到该变量,会向上级作用域递归查找,知道global scope,如果还没找到,那么返回undefined

简单来说,作用域就是一个封闭的空间,因为这个空间是封闭的,不会对外部产生影响,外部空间不能访问内部空间,但是内部空间可以根据层级关系访问将他包裹在内的外部空间。

可能还是有点绕,接下来通过演示来说明一下

js
let a = "a";
function demo() {
  // b是在demo里定义的,外面无法访问
  let b = "b";
  console.log(a);
}
demo();
// b是demo函数里的局部变量,外面无法访问
console.log(b);

深入理解JS作用域链插图

在函数执行时,会产生自己的AO,并将自己的AO放在栈的最顶端,也就是最先访问自己,然后再一层一层往上找,假设我们在demo函数中要使用a变量,首先会在当前的作用域查找该变量a,没有找到的话会向上一级作用域Global scope,也就是于demo scope=>global scope查找,所以可以打印a的值,最后console.log(b),由于作用域没有的话会向上级作用域查找,无法访问子级作用域,最终没有找到b这个变量,所以返回undefined

如果我们在demo函数中要使用c变量,由于demo scope => global scope都没有找到c,那么返回undefined

demo scope => global scope这样的链状形式称之为作用域链,作用域是由下往上的单向结构,所以,如果我们在global scope中如果要用c变量,那么将得到undefined。

Global scope称为全局作用域,全局作用域中的变量称为全局变量

在函数内部声明的函数叫局部函数,局部函数作为AO对象的方法存在

注意一下,所有未定义就直接赋值的变量自动变为全局变量

[[Scopes]]属性

每个javascript函数都是一个对象,对象中有些属性我们可以访问,但有些不可以,这些属性仅供 javascript引擎存取,[[scope]]就是其中一个。 [[scope]]指的就是我们所说的作用域,其中存储了执行期上下文的集合。

作用域链

[[scope]]中所存储的执行期上下文对象的 集合,这个集合呈链式链接,我们把这种链式链接叫 做作用域链

拆解演示

演示代码结构

js
function a() {
  function b() {
    let b = 234;
  }
  let a = 123;
  b();
}
let glob = 100;
a();

在函数执行时,会产生自己的AO,并将自己的AO放在栈的最顶端,也就是最先访问自己

a函数被定义时,发生如下过程

首先第一步,a定义的时候产生了一个scope,里面存着全局的执行上下文

FunctionScope
defined a.[[scope]] ==>0 : GO {}

深入理解JS作用域链插图1

a函数被执行时,发生如下过程

接着a被执行了,产生了AO,将自己的AO放在作用域链的最顶端

FunctionScope
doing a.[[scope]] ==>0 : AO {}
1 : GO {}

深入理解JS作用域链插图2

b函数被定义时,发生如下过程

b在创建的时候用的是a的AO

深入理解JS作用域链插图3

b函数被执行时,发生如下过程

b被执行的时候就会把自己的AO放在第1位,也就是0位

深入理解JS作用域链插图4

所以b在使用变量时,会在作用域链的最顶端,也就是第0位找,也就是自己的AO,自己的AO没有,那么就去第1位找,也就是A的AO,如果A的AO也没有,那么就去GO找,这种链式结构就是原型链了

总结

深入理解JS作用域链插图5

拓展(块级作用域)

  • 在ES6之前是没有块作用域的概念,而ES6中新增了块级作用域。
  • 任何一对花括号{}中的语句集都属于一个块,在这之中定义的所有变量在代码块外都是不可见的,我们称之为块级作用域。在ES6中只要{}没有和函数结合在一起,那么应该就是“块级作用域”。
  • 块作用域由 { } 包括,if语句和for语句里面的{ }也属于块作用域。
  • 在块级作用域中,var定义的变量是全局变量,let定义的变量是局部变量。而在局部作用域中,无论是用var定义的变量还是用let定义的变量都是局部变量。
  • 无论是在块级作用域还是局部作用域,省略变量前面的var或者let都会变成一个全局变量。
艾雨博客微信公众号二维码
© 版权声明
文章版权归作者所有,未经允许请勿转载.
本站资源多数存于云盘,如有失效请邮件联系我.
本网站的文章部分内容可能来源于网络,如有侵权,请联系884684993#qq.com进行删除处理.