Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

作用域、执行上下文、作用域链 #7

Open
Zijue opened this issue Dec 7, 2020 · 0 comments
Open

作用域、执行上下文、作用域链 #7

Zijue opened this issue Dec 7, 2020 · 0 comments

Comments

@Zijue
Copy link
Owner

Zijue commented Dec 7, 2020

最近学习前端知识看到一个大佬写的 blog 感觉特别的棒,于是在阅读和学习之后打算总结( 😂 )到自己的知识体系中,建议直接看大佬的总结
关于作用域链部分内容,参考了另一位大佬的总结

作用域(scope)

作用域即函数或变量的可见区域。即函数或者变量不在这个区域内就无法访问到。

函数作用域

用函数形式以 function(){...} 类似的代码包起来的 ...(省略号)区域,即函数作用域

//全局作用域

function func(){//作用域A
    var a = "coffe";

    (function(){//作用域B。一个IIFE形式的函数,把不想公开的内容隐藏起来
        var a = "1891";
        var b = "b";

        //这里可以放有很多其他要对外隐藏的内容:变量或者函数
        //……
        //…...

        console.log(a);
    })();//>> 1891

    console.log(a);//>> coffe
    console.log(b);//>> Uncaught ReferenceError: b is not defined
}

上述代码中,用一个 IIFE 加匿名函数的写法,把变量 b 隐藏起来,函数外面就没法访问它,函数内部可以访问到它。在任何时候都尽量用匿名函数把要调试的代码片段包起来,然后用 IIFE 的形式立即执行。

块级作用域(ES6添加)

ES6 规定,在某个花括号对 {} 的内部用 let 关键字声明的变量或函数拥有块级作用域

处于向后(backward)兼容的考虑,在块级作用域中声明的函数依然可以在作用域外部引用;如果需要函数只在块级作用域中起作用,应该用 let 关键字写成函数的表达式形态,具体看下面这段代码。

{
  function func(){//函数声明
    return 1;
  }
}
console.log(func());//>> 1
// 等同于下面这段代码
{
  var func = function (){//未使用let关键字的函数表达式
    return 1;
  }
}
console.log(func());//>> 1

// 在花括号对 {} 内部由 let 关键字声明的函数,才是真正的处于块级作用域内部
{
  let func = function (){
    return 1;
  }
}
console.log(func());//>> func is not defined

为什么要引进块级作用域

var 声明的变量有副作用:声明提前

(function() {
  console.log(a); //>> undefined
  console.log(b); //>> ReferenceError
  var a = "coffe"; //声明提前
  let b = "1891"; //由let关键字声明的变量,不存在提前的特性
})();

其次,var 声明变量有污染

(function() {
  for (var i = 0; i < 100; i++) {
    //……很多行代码
  }
  function func() {
    //……很多行代码
  }
  //……很多行代码
  console.log(i); //>> 100
})();

// 循环里面的 i 在循环完毕后就没有用了,但并没有被回收掉,而是一直存在的“垃圾”变量,污染了当前的环境。而用 let 声明变量,事后这种垃圾变量会很快被回收掉

(function() {
  for (let i = 0; i < 100; i++) {
    //……很多行代码
  }
  function func() {
    //……很多行代码
  }
  //……很多行代码
  console.log(i); //>> ReferenceError
})();

综上,应该使用 let,尽量避免使用 var,除非想定义一个全局变量。

执行上下文(Execution Context)

定义

执行上下文就是当前 JavaScript 代码被解析和执行时所在的环境,也叫作执行环境;它是一个抽象概念。

JavaScript 中运行任何的代码都是在执行上下文中运行,在该执行上下文的创建阶段,变量对象、作用域链、this 指向会分别被确定。

类型

  • 全局执行上下文:不在任何函数中的代码都位于全局执行上下文中。它做了两件事:1.创建一个全局对象,在浏览器中这个全局对象就是 window 对象;2.将 this 指针指向这个全局对象。一个程序中只能存在一个全局执行上下文
  • 函数执行上下文:每次调用函数时,都会为该函数创建一个新的执行上下文。每个函数都拥有自己的执行上下文,但是只有在函数被调用的时候才会被创建。一个程序中可以存在任意数量的函数执行上下文
  • eval 执行上下文:运行在 eval 函数中的代码也获得了自己的执行上下文,ES6 之后不再推荐使用 eval 函数

执行上下文的生命周期

执行上下文的生命周期包括三个阶段:创建阶段 => 执行阶段 => 回收阶段。

创建阶段

当函数被调用,但未执行任何其内部代码之前,会做以下三件事:

  • 创建变量对象:首先初始化函数的参数 arguments,提升函数声明和变量声明(变量的声明提前依赖于 var 关键字)
  • 创建作用域链:在执行期上下文的创建阶段,作用域链是在变量对象之后创建的。作用域链本身包含变量对象,用于解析变量。当被要求解析变量时,JavaScript 始终从代码嵌套的最内层开始,如果最内层没有找到变量,就会跳转到上一层父作用域中查找,直到找到该变量
  • 确定 this 指向

执行阶段

创建完成之后,就会开始执行代码,在这个阶段,会完成变量赋值、函数引用以及执行其他代码。

回收阶段

函数调用完毕后,函数出栈,对应的执行上下文也出栈,等待垃圾回收器回收执行上下文。

画图理解过程

执行上下文栈

var a = "coffe"; //1.进入全局执行上下文
function out() {
    var b = "18";
    function inner() {
        var c = "91";
        console.log(a+b+c);
    }
    inner(); //3.进入inner函数的执行上下文
}
out(); //2.进入out函数的执行上下文

上述代码执行上下文入栈出栈的全过程如图所示:

结合代码与流程图可知:

  • 全局执行上下文在代码开始执行时就创建,有且只有一个,永远在执行上下文栈的栈底,浏览器窗口关闭时他才出栈
  • 函数被调用的时候创建函数的执行上下文环境,并且入栈
  • 只有栈顶的执行上下文才是处于活动状态的,也即只有栈顶的变量对象才会变成活动对象

变量对象(Variable Object,VO)

变量对象是一个类似于容器的对象,与作用域链、执行上下文息息相关,存储了在上下文中定义的变量和函数声明。

不同执行上下文中的变量对象稍有不同,具体看看全局上下文的变量对象和函数上下文的变量对象。

函数执行上下文中的变量对象

创建过程总共有三个阶段:

  • 建立arguments对象。检查当前执行上下文中的参数,建立该对象下的属性与属性值
  • 检查当前执行上下文中的函数声明(function 关键字声明的函数),在变量对象中以函数名建立一个属性,属性值为指向该函数所在内存地址的引用。如果该属性之前已经存在,那么该属性将会被新的引用所覆盖
  • 检查当前执行上下文中的变量声明,每找到一个变量声明,就在变量对象中以变量名建立一个属性,属性值为 undefined。如果该变量名的属性已经存在,为了防止同名的函数被修改为 undefined,则会直接跳过,原属性值不会被修改

当执行上下文进入执行阶段后,变量对象会变为活动对象(Active Object,AO)。此时原先声明的变量会被赋值。变量对象和活动对象都是指同一个对象,只是处于执行上下文的不同阶段

通过伪代码来表示变量对象和活动对象

VO={
    Arguments:{},//实参
    Param_Variable:具体值,//形参
    Function:<function reference>,//函数的引用
    Variable:undefined//其他变量
}

AO={
    Arguments:{},//实参
    Param_Variable:具体值,  //形参
    Function:<function reference>,//函数的引用
    Variable:具体值//注意,这里已经赋值了
}

未进入执行上下文的执行阶段之前,变量对象中的属性都不能访问。但是进入执行阶段后,变量对象被激活转变为了活动对象,里面的属性可以被访问了,然后开始进行执行阶段的操作。

全局执行上下文的变量对象

全局执行上下文的变量对象是 window 对象。

全局执行上下文的生命周期,与程序的生命周期一致,只要程序运行不结束(比如关掉浏览器窗口),全局执行上下文就会一直存在。其他所有的执行上下文,都能直接访问全局执行上下文里的内容。

作用域链(Scope Chain)

多个作用域对应的变量对象串联起来组成的链表就是作用域链,这个链表是以引用的形式保持对变量对象的访问。作用域链保证了当前执行上下文对符合访问权限的变量和函数的有序访问。

当查找变量时,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象(即全局对象)。

下面以一个函数的创建和激活两个时期来说明作用域链是如何创建和变化的。

函数创建

JavaScript 采用词法作用域。函数的作用域在函数定义的时候就决定了。

函数有一个内部属性 [[scope]],当函数创建的时候,就会保存所有父变量对象到其中,你可以理解 [[scope]] 就是所有父变量对象的层级链,但是注意:[[scope]] 并不代表完整的作用域链。

以下面代码为例:

function foo(){
    function bar(){
        ...
    }
}

函数创建时,各自的 [[scope]] 为:

foo.[[scope]] = [
    globalContext.VO
];

bar.[[scope]] = [
    fooContext.AO,
    globalContext.VO
];

函数激活

当函数激活时,进入函数上下文,创建 VO/AO 后,就会将活动对象添加到作用域链的前端。
这时候执行上下文的作用域链,我们命名为 Scope

Scope = [AO].concat([[Scope]]);

至此,作用域链创建完毕。

流程梳理

以下面代码为例,总结一下函数执行上下文中作用域链和变量对象的创建过程:

var scope1 = "global scope";
function foo(){
    var scope2 = 'local scope';
    return scope2;
}
foo();

执行过程如下:

  • foo 函数被创建,保存作用域链到内部属性 [[scope]]
foo.[[scope]] = [
    globalContext.VO
];
  • 执行 foo 函数,创建 foo 函数执行上下文并入栈
ExecStack = [
    fooContext,
    globalContext
];
  • foo 函数并不会立即执行,而是进入函数执行上下文的创建阶段
  1. 复制函数 [[scope]] 属性创建作用域链
fooContext = {
    Scope: foo.[[scope]],
}
  1. 用 arguments 创建活动对象,并初始化活动对象,加入形参、函数声明、变量声明
fooContext = {
    AO: {
        arguments: {
            length: 0
        },
        scope2: undefined
    }
    Scope: foo.[[scope]],
}
  1. 将活动对象压入 foo 作用域链顶端
fooContext = {
    AO: {
        arguments: {
            length: 0
        },
        scope2: undefined
    },
    Scope: [AO, [[Scope]]]
}
  • 准备工作做完,进入执行阶段,随着函数的执行,修改 AO 的属性值
fooContext = {
    AO: {
        arguments: {
            length: 0
        },
        scope2: 'local scope'
    },
    Scope: [AO, [[Scope]]]
}
  • 查找到 scope2 的值,返回后函数执行完毕,函数上下文从执行上下文栈中弹出
ExecStack = [
    globalContext
];
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant