一文理解作用域 & 作用域链 &执行上下文

268 阅读7分钟

总览

js代码的运行分为编译阶段和执行阶段,今天从代码的这两个阶段来讲解js的作用域,作用域链,执行上下文。首先我们先知道本篇文章会涉及到的名词:

  • 变量提升

  • 函数提升

  • 作用域 (Scope)

  • 作用域链

  • 执行上下文 (Execution Context)

  • 执行上下文栈(Execution Context Stack)

变量提升 & 函数提升

这里主要发生在js代码的预编译阶段,所谓的变量提升就是为变量创造一个默认的状态。

看例子:

变量提升

function foo (){
    console.log(a) // undefined
    var a = 3
}
foo()

上边例子中a变量声明的时候是在使用(console.log)的下方,但js执行的时候进行预编译上边的代码会变成下边,这种情况就是变量提升

function foo (){
  var a = undefined;
    console.log(a) // undefined
    a = 3;
}
foo()

函数提升

函数定义的方法

  • function foo(){} //函数声明
  • var foo = function(){} // 匿名函数表达式

以上两种方式只有第一种会有函数提升,第二种出现的是变量提升。

下边是一个例子:

function hoistFunction() {
    foo(); // output: I am hoisted

    function foo() {
        console.log('I am hoisted');
    }
}
hoistFunction();

这里有一个需要注意的当两者同时出现,函数提升的权重会更高

参考文章: www.cnblogs.com/liuhe688/p/…

作用域 &作用域链

首先作用域分为全局作用域和局部作用域,es6之后又新增了块级作用域(let,const);

全局作用域:声明在函数外部的变量,在代码中任何地方都能访问到的对象拥有全局作用域(所有没有var直接赋值的变量都属于全局变量)

局部作用域:声明在函数内部的变量,和全局作用域相反,局部作用域一般只在固定的代码片段内可访问到,最常见的例如函数内部,所有在一些地方也会看到有人把这种作用域称为函数作用域(所有没有var直接赋值的变量都属于全局变量);

作用域链:当一个函数内部使用一个变量的时候,现在自己所在的作用域查找,如果没有该变量,向【上级】作用域查找,直到全局作用域,如果还没找到,就会抛出错误【*** is not defined】,其实就像是定义了一个查找的规则。这个规则定义在编译阶段。

作用域决定一个变量它所能被拿到的范围(是一个函数内部,还是全局),可以理解成:作用域链决定当你使用一个变量时候的查找规则,这个规则使用的环境是在编译时创建的,而不是执行时调用的,执行上下文有提到。

看一个例子:

var a = 100
function f1() {
    var b = 200
    function f2() {
        var c = 300
        console.log(a) // 自由变量,顺作用域链向父作用域找
        console.log(b) // 自由变量,顺作用域链向父作用域找
        console.log(c) // 本作用域的变量
    }
    f2()
}
f1()

执行上下文

执行上下文:是代码完成编译后,准备执行的时候发生的定义【可能就一瞬间】,首先,执行上下文分为全局执行上下文和函数执行上下文。一个执行上下文包含三个部分:

  1. 变量对象
  2. 作用域链
  3. this

我们分开来说这三个部分

1.变量对象

只有全局上下文的变量对象允许通过VO的属性名称间接访问;在函数执行上下文中,VO是不能直接访问的,此时由激活对象(Activation Object,缩写为AO)扮演VO的角色。激活对象 是在进入函数上下文时刻被创建的,它通过函数的arguments属性初始化

函数执行上下文

  • 函数的形参,也就是函数执行的时候有一个arguments的“类数组”,当定义函数执行上下文的时候需要注意arguments;

  • var声明的变量(这里要注意变量提升 );

  • 函数声明 (函数提升)

全局执行上下文(没有arguments这部分)

  • var声明的变量(这里要注意变量提升 );

  • 函数声明 (函数提升)

举个例子

function test1(arg){
    var a = 1;
    var b = {name: 'afei'};
    function c(){
        console.log(a);
    }
}

对应创建时的VO

VO = {
    arguments: {...arg},
    a: undefined,
    b: undefined,
    c: function c(){}
}

对应的执行阶段的VO

VO = {
    arguments: {...arg},
    a: 1,
    b: {name: 'afei'},
    c: function c()
}

2.作用域链 【Scopes】

上边提到决定你使用一个变量时候的查找规则,这个规则他以一个栈的方式存储,这里需要注意的是作用域链的查找规则是在编译阶段时候就已经定义好的,首先查找的是当前上下文的vo对象里边定义的变量,没有要查找的变量时,就会去词法作用域的父级的执行上下文的vo对象里边去找,直到全局执行上下文。举个例子:

var a = "global var";
function foo(){
    console.log(a);
}
function outerFunc(){
    var a = "var in outerFunc";
    function innerFunc(){
        var a = "var in innerFunc";
        foo();
    } 
    innerFunc();
}
outerFunc()

/*代码执行 outerFunc()==>innerFunc()==>foo() 
当foo()执行的时候用到了a这个变量,当前vo对象不存在a这个变量,这时他会
去找定义foo这个函数的位置[也就是上边提到的词法作用域]的父级也就是window 
全局上下文的vo对象 ‘global var’
*/

上边foo函数的作用域链就是[[Scopes]]=[,windowScope,fooScope]

3.this

先要明确一点,this的值是在执行的时候才能确定,定义的时候不能确认!

通过分析一个例子来说this的部分

// 情况1
function foo() {
  console.log(this.a) //1
}
var a = 1
foo()

// 情况2
function fn(){
  console.log(this);
}
var obj={fn:fn};
obj.fn(); //this->obj

// 情况3
function CreateJsPerson(name,age){
//this是当前类的一个实例p1
this.name=name; //=>p1.name=name
this.age=age; //=>p1.age=age
}
var p1=new CreateJsPerson("尹华芝",48);
// 情况4
function add(c, d){
  return this.a + this.b + c + d;
}
var o = {a:1, b:3};
add.call(o, 5, 7); // 1 + 3 + 5 + 7 = 16
add.apply(o, [10, 20]); // 1 + 3 + 10 + 20 = 34

// 情况5
<button id="btn1">箭头函数this</button>
<script type="text/javascript">
    let btn1 = document.getElementById('btn1');
    let obj = {
        name: 'kobe',
        age: 39,
        getName: function () {
            btn1.onclick = () => {
                console.log(this);//obj
            };
        }
    };
    obj.getName();
</script>

接下来我们逐一解释上面几种情况

  • 对于直接调用 foo 来说,不管 foo 函数被放在了什么地方,this 一定是 window

  • 对于 obj.foo() 来说,我们只需要记住,谁调用了函数,谁就是 this,所以在这个场景下 foo 函数中的 this 就是 obj 对象

  • 在构造函数模式中,类中(函数体中)出现的 this.xxx=xxx 中的 this 是当前类的一个实例

  • call、apply 和 bind:this 是第一个参数

  • 箭头函数 this 指向:箭头函数没有自己的 this,看其外层的是否有函数,如果有,外层函数的 this 就是内部箭头函数的 this,如果没有,则 this 是 window

image

执行上下文栈

函数多了,就有多个函数执行上下文,每次调用函数创建一个新的执行上下文,那如何管理创建的那么多执行上下文呢?

JavaScript 引擎创建了执行上下文栈来管理执行上下文。可以把执行上下文栈认为是一个存储函数调用的栈结构,遵循先进后出的原则。

关键点

  • JavaScript 执行在单线程上,所有的代码都是排队执行。
  • 一开始浏览器执行全局的代码时,首先创建全局的执行上下文,压入执行栈的顶部。
  • 每当进入一个函数的执行就会创建函数的执行上下文,并且把它压入执行栈的顶部。当前函数执行完成后,当前函数的执行上下文出栈,并等待垃圾回收。
  • 浏览器的 JS 执行引擎总是访问栈顶的执行上下文。
  • 全局上下文只有唯一的一个,它在浏览器关闭时出栈。

举个例子:

var color = "blue";
function changeColor() {
    var anotherColor = "red";
    function swapColors() {
        var tempColor = anotherColor;
        anotherColor = color;
        color = tempColor;
    }
    swapColors();
}
changeColor();

上述代码运行按照如下步骤:

  • 当上述代码在浏览器中加载时,JavaScript 引擎会创建一个全局执行上下文并且将它推入当前的执行栈
  • 调用 changeColor 函数时,此时 changeColor 函数内部代码还未执行,js 执行引擎立即创建一个 changeColor 的执行上下文(简称 EC),然后把这执行上下文压入到执行栈(简称 ECStack)中。
  • 执行 changeColor 函数过程中,调用 swapColors 函数,同样地,swapColors 函数执行之前也创建了一个 swapColors 的执行上下文,并压入到执行栈中。
  • swapColors 函数执行完成,swapColors 函数的执行上下文出栈,并且被销毁。
  • changeColor 函数执行完成,changeColor 函数的执行上下文出栈,并且被销毁。

image

最后

1.如有错误,评论指出。

2.看到这了,点个赞支持下吧!感谢大家。

3.关注公众号 TT的Web世界。