Blog icon indicating copy to clipboard operation
Blog copied to clipboard

JavaScript深入之执行上下文

Open mqyqingfeng opened this issue 7 years ago • 79 comments

前言

《JavaScript深入之执行上下文栈》中讲到,当 JavaScript 代码执行一段可执行代码(executable code)时,会创建对应的执行上下文(execution context)。

对于每个执行上下文,都有三个重要属性:

  • 变量对象(Variable object,VO)
  • 作用域链(Scope chain)
  • this

然后分别在《JavaScript深入之变量对象》《JavaScript深入之作用域链》《JavaScript深入之从ECMAScript规范解读this》中讲解了这三个属性。

阅读本文前,如果对以上的概念不是很清楚,希望先阅读这些文章。

因为,这一篇,我们会结合着所有内容,讲讲执行上下文的具体处理过程。

思考题

《JavaScript深入之词法作用域和动态作用域》中,提出这样一道思考题:

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f();
}
checkscope();
var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}
checkscope()();

两段代码都会打印'local scope'。虽然两段代码执行的结果一样,但是两段代码究竟有哪些不同呢?

紧接着就在下一篇《JavaScript深入之执行上下文栈》中,讲到了两者的区别在于执行上下文栈的变化不一样,然而,如果是这样笼统的回答,依然显得不够详细,本篇就会详细的解析执行上下文栈和执行上下文的具体变化过程。

具体执行分析

我们分析第一段代码:

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f();
}
checkscope();

执行过程如下:

1.执行全局代码,创建全局执行上下文,全局上下文被压入执行上下文栈

    ECStack = [
        globalContext
    ];

2.全局上下文初始化

    globalContext = {
        VO: [global],
        Scope: [globalContext.VO],
        this: globalContext.VO
    }

2.初始化的同时,checkscope 函数被创建,保存作用域链到函数的内部属性[[scope]]

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

3.执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 函数执行上下文被压入执行上下文栈

    ECStack = [
        checkscopeContext,
        globalContext
    ];

4.checkscope 函数执行上下文初始化:

  1. 复制函数 [[scope]] 属性创建作用域链,
  2. 用 arguments 创建活动对象,
  3. 初始化活动对象,即加入形参、函数声明、变量声明,
  4. 将活动对象压入 checkscope 作用域链顶端。

同时 f 函数被创建,保存作用域链到 f 函数的内部属性[[scope]]

    checkscopeContext = {
        AO: {
            arguments: {
                length: 0
            },
            scope: undefined,
            f: reference to function f(){}
        },
        Scope: [AO, globalContext.VO],
        this: undefined
    }

5.执行 f 函数,创建 f 函数执行上下文,f 函数执行上下文被压入执行上下文栈

    ECStack = [
        fContext,
        checkscopeContext,
        globalContext
    ];

6.f 函数执行上下文初始化, 以下跟第 4 步相同:

  1. 复制函数 [[scope]] 属性创建作用域链
  2. 用 arguments 创建活动对象
  3. 初始化活动对象,即加入形参、函数声明、变量声明
  4. 将活动对象压入 f 作用域链顶端
    fContext = {
        AO: {
            arguments: {
                length: 0
            }
        },
        Scope: [AO, checkscopeContext.AO, globalContext.VO],
        this: undefined
    }

7.f 函数执行,沿着作用域链查找 scope 值,返回 scope 值

8.f 函数执行完毕,f 函数上下文从执行上下文栈中弹出

    ECStack = [
        checkscopeContext,
        globalContext
    ];

9.checkscope 函数执行完毕,checkscope 执行上下文从执行上下文栈中弹出

    ECStack = [
        globalContext
    ];

第二段代码就留给大家去尝试模拟它的执行过程。

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}
checkscope()();

不过,在下一篇《JavaScript深入之闭包》中也会提及这段代码的执行过程。

下一篇文章

《JavaScript深入之闭包》

相关链接

《JavaScript深入之词法作用域和动态作用域》

《JavaScript深入之执行上下文栈》

《JavaScript深入之变量对象》

《JavaScript深入之作用域链》

《JavaScript深入之从ECMAScript规范解读this》

重要参考

《一道js面试题引发的思考》

本文写的太好,给了我很多启发。感激不尽!

深入系列

JavaScript深入系列目录地址:https://github.com/mqyqingfeng/Blog

JavaScript深入系列预计写十五篇左右,旨在帮大家捋顺JavaScript底层知识,重点讲解如原型、作用域、执行上下文、变量对象、this、闭包、按值传递、call、apply、bind、new、继承等难点概念。

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎star,对作者也是一种鼓励。

mqyqingfeng avatar Apr 26 '17 02:04 mqyqingfeng

checkscope 函数 和 f 函数,在代码执行这一阶段,没有对各自的 this 做任何操作,所以沿着作用域链,最终找到全局 this 的引用,即 globalContext.VO 对象,是这样吧?

zuoyi615 avatar May 27 '17 08:05 zuoyi615

@zuoyi615 this 是在函数执行的时候才确定下来的,checkscope 函数 和 f 函数的 this 的值跟作用域链没有关系,具体的取值规则还需要参照上一篇文章《JavaScript深入之从ECMAScript规范解读this》, 两者的 this 其实都是 undefined ,只是在非严格模式下,会转为全局对象。嗯,如果讲的不明白的话,就跟我说一下,我看怎么再表述下这个东西哈~

mqyqingfeng avatar May 27 '17 09:05 mqyqingfeng

从ECMAScript规范解读this,太不好理解了

zuoyi615 avatar May 27 '17 09:05 zuoyi615

作者您好!之前有道题,通过看您的文章,大致有了一个猜想,但是还是不能很清晰的说出原因,烦请您看一下,谢谢!

let nAdd;
let t = () => {
    let n = 99;
    nAdd = () => {
        n++;
    };
    let t2 = () => {
        console.log(n);
    };
    return t2;
};

let a1 = t();
let a2 = t();

nAdd();
a1();    //99
a2();    //100

不知是不是a2()的作用域置顶了,所以nAdd()修改的是a2()作用域里的变量,但闭包的话,同一个变量名难道不是指向同一个内存地址的值吗

flyerH avatar May 27 '17 09:05 flyerH

@flyerH 这真的是个好问题!我们先看个简单的例子:

var t = function() {
    var n = 99;
    var t2 = function() {
    	n++
    	console.log(n)
    }
    return t2;
};

var a1 = t();
var a2 = t();

a1(); // 100
a1(); // 101

a2(); // 100
a2(); // 101

我们会发现,n 的值都是从 99 开始,执行 一次a1() 的时候,值会加一,再执行一次,值再加一,但是 n 在 a1() 和 a2() 并不是公用的。你可以理解为:同一个函数形成的多个闭包的值都是相互独立的。

接下来看这道题目,关键在于 nAdd 函数

var nAdd;
var t = function() {
    var n = 99;
    nAdd = function() {
    	 n++;
    }
    var t2 = function() {
    	console.log(n)
    }
    return t2;
};

var a1 = t();
var a2 = t();

nAdd();

a1(); //99
a2(); //100

当执行 var a1 = t()的时候,变量 nAdd 被赋值为一个函数 ,这个函数是function (){n++},我们命名这个匿名函数为 fn1 吧。接着执行 var a = t()的时候,变量 nAdd 又被重写了,这个函数跟以前的函数长得一模一样,也是function (){n++},但是这已经是一个新的函数了,我们就命名为 fn2 吧。

所以当执行 nAdd 函数,我们执行的是其实是 fn2,而不是 fn1,我们更改的是 a2 形成的闭包里的 n 的值,并没有更改 a1 形成的闭包里的 n 的值。所以 a1() 的结果为 99 ,a2()的结果为 100。

mqyqingfeng avatar May 27 '17 12:05 mqyqingfeng

@mqyqingfeng 非常感谢您的解答,谢谢!

flyerH avatar May 27 '17 12:05 flyerH

@flyerH 哈哈,不用这么客气,有问题就留言讨论哈~

mqyqingfeng avatar May 27 '17 12:05 mqyqingfeng

@zuoyi615 哈哈,确实不好理解,因为涉及到很多规范上的内容,需要边查规范边读,但我也正是通过研究 this 第一次克服了对于全英文的规范的恐惧,希望你也去试一试~

mqyqingfeng avatar May 27 '17 12:05 mqyqingfeng

第一个函数查找上级作用域中scope 第二个函数式闭包,保存了父级函数中scope的引用 所以两个值相等;

lynn1824 avatar May 31 '17 04:05 lynn1824

博主,请问那个nAdd(); 什么时候调用的? 我看不懂

Flying-Eagle2 avatar Jun 02 '17 11:06 Flying-Eagle2

@Flying-Eagle2 当然是执行这个函数的时候调用的啦~

default

mqyqingfeng avatar Jun 02 '17 11:06 mqyqingfeng

同时 f 函数被创建,保存作用域链到 f 函数的内部属性[[scope]]

    checkscopeContext = {
        AO: {
            arguments: {
                length: 0
            },
            scope: undefined,
            f: reference to function f(){}
        },
        Scope: [AO, globalContext.VO],
        this: undefined
    }
   checkscope();

checkscope预编译阶段,形参、函数f声明、变量scope声明。 f 函数被创建的活动是在checkscope函数预编译阶段进行还是在函数执行阶段进行的?

suoz avatar Jun 08 '17 07:06 suoz

@suoz 我认为是在 checkscope 函数预编译阶段

mqyqingfeng avatar Jun 08 '17 09:06 mqyqingfeng

@mqyqingfeng 大大

function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f();
}

你看我这么理解对么~

执行函数checkscope时,分为预编译阶段和执行阶段,预编译阶段就是你所说的创建执行上下文、执行上下文初始化(复制函数[[scope]]属性创建作用域链、使用arguments创建活动对象、初始化活动对象{即形参、函数声明、变量声明}、将活动对象压入作用域链的顶端)。

当函数checkscope执行,处于预编译阶段中函数声明的时候,此时只是创建了f函数(只是创建了f函数的[[scope]]属性,这个属性只包含了checkscope函数的活动对象和全局变量对象,并不包含f函数的活动对象)

等到函数checkscope处于执行阶段时,就是return f();,此时调用f(),这时候才会创建f函数的上下文,以及上面所提到的相同四步骤。

suoz avatar Jun 08 '17 14:06 suoz

@suoz 是哒~ o( ̄▽ ̄)d

mqyqingfeng avatar Jun 08 '17 15:06 mqyqingfeng

全局上下文初始化里面的VO里面的global是什么情况啊? globalContext = { VO: [global, scope, checkscope], Scope: [globalContext.VO], this: globalContext.VO }

yh284914425 avatar Jun 21 '17 07:06 yh284914425

@yh284914425 这个 global 表示全局对象哈~

mqyqingfeng avatar Jun 21 '17 16:06 mqyqingfeng

大神,能不能帮我分析下 下面执行上下文的具体处理过程 谢谢 var b = 10; (function b(){ b = 20; console.log(b); })();

yh284914425 avatar Jun 25 '17 07:06 yh284914425

@yh284914425 非常好的问题!但这个问题涉及到的知识点,其实整个系列文章都没有讲到过,日后我一定补上。

具体原因可以参考汤姆大叔的文章,简单的说一说,是因为当解释器在代码执行阶段遇到命名的函数表达式时,会创建辅助的特定对象,然后将函数表达式的名称即 b 添加到特定对象上作为唯一的属性,因此函数内部才可以读取到 b,但是这个值是 DontDelete 以及 ReadOnly 的,所以对它的操作并不生效,所以打印的结果自然还是这个函数,而外部的 b 值也没有发生更改。

mqyqingfeng avatar Jun 27 '17 05:06 mqyqingfeng

@mqyqingfeng 好的,期待您的文章,您说的创建辅助的特定对象还是执行上下文不?

yh284914425 avatar Jun 27 '17 06:06 yh284914425

@yh284914425 具体我还没有研究过,我的猜想就是一个对象,储存了函数表达式的名称,然后将其添加到了 b 函数的作用域链中,大致类似于 Scope: [globalContext, {特殊对象}, AO]

mqyqingfeng avatar Jun 27 '17 06:06 mqyqingfeng

博主:帮忙分析一下这个具体执行过程,我很难看懂啊!谢谢

 var p = (function (a) {
       this.a = a;
       return function (b) {
            return this.a + b;
      }
}(function (a, b) {
     return a;
 }(1, 2)));
console.log(p(4))

Flying-Eagle2 avatar Jul 04 '17 05:07 Flying-Eagle2

@Flying-Eagle2

我们先看这段代码的结构,简化一下就是:

var p = (function _a(){

}(function _b(){

}()))

相当于先执行 _b 函数,然后将函数的执行结果作为参数传入 _a 函数

_b 函数为:

function (a, b) {
     return a;
 }

_b 函数执行

(function (a, b) {
     return a;
 }(1, 2))

函数返回 1,然后将 1 作为参数传入 _a,相当于:

function (a) {
       this.a = a;
       return function (b) {
            return this.a + b;
      }
}(1)

变量 p 的值就是一个函数为:

function (b) {
     return 1 + b
}

p(4) 的结果自然是 5

mqyqingfeng avatar Jul 04 '17 05:07 mqyqingfeng

我就是这块没看懂

 return function (b) {
            return this.a + b;
 }

第一次返回的话函数a的值是1, this.a的值也应该是1吧;

function (b) {
     return 4 + b
}

你这个我更没看懂呢;4又是哪里传的,b 又是 谁传的?????啊啊啊啊啊,我真没看懂

Flying-Eagle2 avatar Jul 04 '17 05:07 Flying-Eagle2

@Flying-Eagle2 写错了,应该是 1 哈~

mqyqingfeng avatar Jul 04 '17 05:07 mqyqingfeng

@Flying-Eagle2

function (a) {
       this.a = a;
       return function (b) {
            return this.a + b;
      }
}(1)

执行这个函数,返回一个函数为:

function (b){
    return this.a + b
}

this.a 的值为 1,这是因为,两次出现的 this 都指向了全局,你可以在最外层是直接打印 a 的值。

mqyqingfeng avatar Jul 04 '17 05:07 mqyqingfeng

感谢感谢!我好像觉悟了, 也就是说它返回这个函数 function (b){ return this.a + b } 然后我传的p(4)就是b接收了;;;; 博主是大神,谢谢你;

Flying-Eagle2 avatar Jul 04 '17 05:07 Flying-Eagle2

@zuoyi615 this的值,只有在函数执行的时候才能确定

bestvayne avatar Jul 04 '17 09:07 bestvayne

checkscope 函数执行上下文初始化: 1、复制函数 [[scope]] 属性创建作用域链, 4、将活动对象压入 checkscope 作用域链顶端。

我其实没看明白这儿的作用域链是在整个程序执行过程中都只有一个呢,还是checkscope有个单独的作用域呢?

qujsh avatar Nov 07 '17 02:11 qujsh

@qujsh 函数创建的时候,就会创建一个函数自己的作用域链,所以 checkscope 有个单独的作用域链

mqyqingfeng avatar Nov 08 '17 03:11 mqyqingfeng