mqyqingfeng / Blog

冴羽写博客的地方,预计写四个系列:JavaScript深入系列、JavaScript专题系列、ES6系列、React系列。

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

JavaScript深入之执行上下文

mqyqingfeng opened this issue · 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,对作者也是一种鼓励。

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

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

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

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

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 这真的是个好问题!我们先看个简单的例子:

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 非常感谢您的解答,谢谢!

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

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

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

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

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

default

commented

同时 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 我认为是在 checkscope 函数预编译阶段

commented

@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 是哒~ o( ̄▽ ̄)d

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

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

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

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

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

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

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

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

 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

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

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

我就是这块没看懂

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

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

function (b) {
     return 4 + b
}

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

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

@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 的值。

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

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

commented

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

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

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

=。= 这一章是复习章节吗 哈哈哈哈哈哈哈

@zanqianqvxifu 对的,这一篇就是结合着之前的内容,分析执行上下文的具体处理过程。

commented

@mqyqingfeng @suoz
其实我也有一样的疑惑,而且假如说[[scope]]对于函数声明是在代码执行前就加入的属性,那么对于函数赋值给变量这种情况呢?我在前一节也是提出了这个问题,有些疑惑~~当然,这并不影响,因为在进入该函数的执行上下文之前,肯定是代码运行到了这里,执行了函数调用。此时,对于该函数来讲,这个属性肯定是已经有了,而且保存的是其所在执行环境的作用域链。
在函数执行上下文初始化的时候,其实我想,是不是顺序执行了一下三样,新建一个scope属性(赋值函数的[[scope]]),VO属性和this属性,然后当前上下文压栈,arguments激活VO成为AO,然后AO放入scope前端,最后绑定this属性。
所以,我认为,作者大大的第四步

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

这里的this应该是已经绑定了全局对象的~因为这个函数只有在被调用执行时才会进入执行上下文。
不知道我的理解是否正确?

commented

忽略我上面说的第二点,作者大大是对的,这里是独立调用,指向的确实是undefined,只不过非严格模式将其指向全局而已。莫怪莫怪哈哈

@lovedd 函数也是一种对象,即便将函数赋值给变量,这个变量也只是储存了一个指向函数的引用,最终执行的还是函数,所以没有什么影响吧~

commented

@mqyqingfeng 嗯,我的意思是指这么一句代码:

var fn = function(){};

那么此时在执行上下文的第一阶段,只是对fn做了变量提升等准备操作。函数赋值给该变量应该是在代码执行阶段了,也就是这个函数创建的时候应该已经到了执行阶段,此时才有[[scope]]属性。

不过后来我想,其实函数创建和调用方式并不止一种。但是不同的创建方式在执行前始终能把这个[[scope]]属性给到其执行上下文,这才是重点,而且编译器和引擎的真正原理这一块并没有去深究,所以就干脆不再想这块,等合适的实际研究引擎吧。

对于作者大大写的this,给了我很大的启发。其实对于(obj.fn = obj.fn)()这种this指向全局,我的理解是其实(obj.fn = obj.fn)隐式复制给了一个变量,然后立即调用了这个变量。类似于var temp = (obj.fn = obj.fn); tenp();不知道这样是否会好理解一些。看了楼主的系列文章,真的很受启发,开始慢慢尝试把函数,执行上下文,执行上下文栈,作用域链和this关联起来。隐隐约约感觉函数是一座桥梁,函数定义时,就保存了当前环境的作用域链,函数执行时就生成了新的执行环境的作用域链,而调用函数的对象决定了this的绑定,还在慢慢阅读作者大大的文章,体会梳理中, 再次感谢作者的分享和认真~~~~

commented

晕,怎嘛最后一段多了一段删除线,尴尬啊😓😓😓

这些天过的度日如年……没能及时回复大家的留言,很是抱歉~

@lovedd 哈哈, Github 的 issue 支持 markdown 格式,它会将两个 ~ 之间的字符产成一个删除线~

大神你好,我对全局上下文初始化有点疑问,

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

根据你在变量对象那章的描述:

image

image

按照你之前说的,全局变量上下文应该按照下面的描述才对。

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

请问我的理解有问题吗?

commented

@EtheriousNatsu 我的理解是此时scope和checkscope都挂载在全局变量下

@genJio 那你和我的理解一样,希望作者解答下。

@EtheriousNatsu @yashikyou 哈哈,感谢指出哈~ 当时我应该是想体现出 VO 中不仅包括了原本的全局对象,还有新创建的 scope, checkscope,所以才会这样写,没有想到注意到它们毕竟还是属于包含关系~ 再次感谢,已经修改了~

commented

看了你的文章受益匪浅

function a() {
var aaa = 123;
function b(){
console.log(aaa); // 123
aaa = 234;
};
console.dir(b);
};
a();

function a() {
var aaa = 123;
function b(){
console.log(aaa); // 123
var aaa = 234;
};
console.dir(b);
};
a();

博主,以上两段代码输出不一样,请问用这个作用域链该如何解释?

commented

大佬看了权威指南感觉有点疑问,权威指南里说 每次调用javascript函数时,都会为之创建一个新的对象用来保存局部变量,把这个对象添加至作用域链中。当函数返回的时候,就从作用域链中将这个绑定变量的对象删除。
这么说的话,那函数作用域应该是定义时就创建完毕,当执行到此函数时,将此函数内的活动对象保存在一个新的对象中并压至作用域链顶端,然后执行完之后再删除此对象。
但是看大佬这里的4、checkscope 函数执行上下文初始化:复制函数 [[scope]] 属性创建作用域链。。。

好吧。。写着写着想明白了突然。。

image

对这个问题还是有点疑惑 按照博主说的 应该是都是两个 能解释一下吗

@liuzeyang 是因为第二个b没有引用外面a的变量,上面有人说过,作用域链越短越好,这样可以优化性能,不晓得是这样吗

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: undefined
}

初始化上下文的同时,创建checkscope函数,保存作用域到函数内部[[scope]]属性。

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

3.执行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.checkscope 函数执行完毕,checkscope 执行上下文从执行上下文栈中弹出。

ECStack=[
    globalContext
 ];

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

ECStack=[
    fContext,
    globalContext
 ];

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

fContext = {  
     AO:  {
         arguments: {
             length: 0
         }
     },
     Scope: [AO, checkscopeContext.AO, globalContext.VO],
     this: undefined
}

8.f 函数执行,沿着作用域链查找 scope 值,返回 scope 值
9.f 函数执行完毕,f 函数上下文从执行上下文栈中弹出

ECStack = [
        globalContext
    ];

@mqyqingfeng 大神,步骤4最后应该还有一个改变checkscopeContext.AO(其实就是执行scope = "local scope"),然后才开始步骤5的f的执行。

作者您好,请教两个问题。在执行 f 函数时,执行过程并没有提到用f函数的内部属性[[scope]]保存作用域链,这个步骤是有益省略还是不存在呢?用内部属性保存作用域链可以认为是在函数的执行上下压入作用域栈前吗?

第二题的过程可以看作这样吗

  1. 创建全局上下文
// 全局执行上下文
ECStack = [GlobalEC]
  1. 执行checkscope, 将checkscope压入执行上下文栈
ECStack = [checkscope GlobalEC]

3.初始化函数checkscope给出该函数的执行上下文

checkscope = {
    VO: {
        arguements:{
            length: 0
        },
        scope: undefiend,
        f: function() reference
    }, 
    SCOPE: [A0, ECStack]
    this: undefiend
}
  1. 执行过程中更新AO
checkscope = {
    AO: {
        arguements:{
            length: 0
        },
        scope: "localscope",
        f: function() reference
    }, 
    SCOPE: [A0, ECStack]
    this: undefiend
}
  1. 返回f后checkscope出栈结束,从执行上下文环境中弹出
ECStack = [GlobalEC]
  1. 执行f 将f入栈
ECStack = [f Global]

7.生成f的执行环境

checkscope = {
    AO: {
        arguements:{
            length: 0
        },
    }, 
    SCOPE: [f, ECStack]
    this: undefiend
}

8.结束运行并使f的上下文出栈

ECStack = [Global]

为什么在第五步和第七步都执行了f函数呢,

“接着执行 var a = t()的时候”,应该是“接着执行 var a2 = t()的时候”

commented

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

这个我刚好看了相关的规范,这个“对象”是 Function Object,这个 Function Object 不是我们平日讲的函数,而是在执行函数前提前准备好的一个对象,包含有很多内部属性。
其中有一个内部属性叫做[[Environment]],这个属性是一个“词法环境”(词法环境其实就是我们平日说的“作用域”),这个词法环境用于干嘛呢?用于在真正执行的时候成为届时的”运行执行上下文“的词法环境。在函数执行时,变量的查找就是从这个词法环境开始的。
这里稍微有点绕,可以参考 ES2019 规范中的 9.2.3 FunctionAllocate。

如果被执行的语句是一个**具名的函数表达式(FunctionExpression:functionBindingIdentifier(FormalParameters){FunctionBody})**则这里的函数名会被作为 Function Object 的 [[Environment]] 内部属性的字段之一。
在执行时,函数若是访问该函数名,访问的就是这个函数本身,而不会再向上查找。
可以参考 ES2019 规范中的 14.1.22 Runtime Semantics: Evaluation。

希望能有帮助。

@yh284914425
@mqyqingfeng
关于很久之前的整个问题
var b = 10;
(function b(){
b = 20;
console.log(b);
})();
这个问题真是一个宝藏问题,查询途中发现了好多知识点。
立即执行表达式的本质:定义了一个函数表达式,然后立即执行该表达式。
大多数情况下我们会使用匿名函数表达式,如果用命名表达式——《javascript权威指南》8.1章节的函数定义部分,有这样一段话:“如果一个函数定义表达式包含名称,函数的局部作用域将会包含一个绑定到函数对象的名称。实际上,函数的名称将成为函数内部的一个局部变量”。
下面是规范中的说明: The BindingIdentifier in a FunctionExpression can be referenced from inside the FunctionExpression's FunctionBody to allow the function to call itself recursively. However, unlike in a FunctionDeclaration, the BindingIdentifier in a FunctionExpression cannot be referenced from and does not affect the scope enclosing the FunctionExpression.
还有一个被称为JSbug的问题。一般情况下,如果出现temp=10;这样的给未声明变量赋值的操作,temp会声明为全局变量。但是如果这样的情况出现在立即表达式中,temp会被声明为局部变量而不是全局变量。。这个首先就解决您的问题,b=20并不会影响到全局变量中的b。

接下来的是我自己不明白的问题,立即表达式的局部变量b难道不会被修改为20吗,为什么打印出现的是b函数本身,而不是20呢?
var b = 10;
(function b() {
var b = 20;
console.log(b);//20
})();
(function b() {
console.log(b);//[Function b]
})();
(function b() {
b=20;
console.log(b);//[Function b]
})();

博主大大or各位路过大大,您们好!有一个疑惑也可能是犯二。。。,就是这篇文章的正文和评论部分,在ECStack的添加时都是按照unshift方法压入的,但是在博主大大的《JavaScript深入之执行上下文栈》一文中,是按照push和pop来分别压入和弹出函数执行上下文的,我现在有点懵,到底哪个是对的啊,怎么大家都是按照unshift压入的,应该以哪个为正确啊。。。哭唧唧.jpg

你好,在第七步沿着作用域链查找scope值的时候,是不是在第四步到第七步之间,随着函数的执行,checkscopeContext的AO里面的scope值已经被修改赋值了,不然执行f函数的时候查scope值应该是undefined吧,在作用域链那篇文章中写的是,“准备工作完毕,执行函数,随着函数执行,修改AO的属性值”

好难理解呀~

分析第二段代码

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

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

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

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

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

2.执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 函数执行上下文被压入执行上下文栈,并初始化函数上下文

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

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

f.[[scope]] = [AO, checkscopeContext.AO, globalContext.VO];

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

ECStack = [globalContext];

4.执行 f 函数,创建 f 函数执行上下文,并压入执行上下文栈,将其初始化

ECStack = [fContext;, globalContext];
fContext = {
  AO: {
    arguments: {
      length: 0
    }
  },
  Scope: [AO, checkscopeContext.AO, globalContext.VO],
  this: undefined
};

5.f 函数执行,沿着作用域链查找 scope 值,返回 scope 值。正是因为 checkscope 函数执行上下文初始化时,f 函数同时被创建,保存作用域链到 f 函数的内部属性[[scope]],所以即使checkscope函数执行完毕,被弹出执行上下文栈,但是checkscopeContext.AO 依然存在于 f 函数维护的[scope]]

fContext = {
    Scope: [AO, checkscopeContext.AO, globalContext.VO],
}

所以,闭包的概念产生了,定义:

  • 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
  • 在代码中引用了自由变量

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

ECStack = [globalContext];

Over,hahahah

函数被创建时,保存作用域链到函数的内部属性[[scope]]这一步的一点个人理解,不知是否正确

最近才看到这个博客,好多概念有了全新的认识,感谢博主的分享。
有几个问题不知道自己的理解是否正确,以代码1举例说明

function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f();
}
checkscope();
  1. 函数的创建和执行的关系。
    全局中创建的函数是在全局上下文执行的时候创建的;函数a内部创建的函数b是在函数a执行的时候创建的。
    在代码1中:
    checkscope函数是在全局上下文执行的时候创建的,f函数是在checkscope函数执行时被创建的。
    疑问1:如果没有最后一句checkscope();执行checkscope函数,f函数就不会被创建,那么f函数的[[scope]]以及fContext均不会生成?

  2. 函数创建时[[scope]]属性的生成。
    疑问2:函数创建时[[scope]]属性是否是此时执行上下文栈中最上层的执行上下文对象的Scope的引用?
    在代码1中:

  • checkscope函数创建时是全局上下文执行时,此时
    ECStack = [
        globalContext
    ];
    globalContext = {
        VO: [global],
        Scope: [globalContext.VO],
        this: globalContext.VO
    }
    checkscope.[[scope]] = [
      globalContext.VO
    ];

checkscope.[[scope]] 就是ECStack中最上层上下文globalContext.Scope的引用

  • f函数创建时是checkscope函数上下文执行时,此时
    ECStack = [
        checkscopeContext,
        globalContext
    ];
    checkscopeContext = {
        AO: {
            arguments: {
                length: 0
            },
            scope: undefined,
            f: reference to function f(){}
        },
        Scope: [AO, globalContext.VO],
        this: undefined
    }
    f.[[scope]] = [
      checkscopeContext.AO
      globalContext.VO
    ];

f.[[scope]] 就是此时ECStack中最上层上下文checkscopeContext.Scope的引用

在代码1中我的这个想法貌似能说通。


以上两个疑问还请聚聚们赐教

总感觉执行函数的作用域链长度 == 执行上下文栈的长度,不知道这样理解对不对

@mqyqingfeng博主大大你好:
我有以下几个问题没有弄清楚,如果博主能提供解答将不胜感激。
1.函数执行上下文中的作用域链[[scope]]表示是将链表转成数组的形式方便理解还是本身就是以数组的形式?
2.如果1中是链表的形式,那头节点是不是还会有一个类似next指针的属性。如果按我的理解应该函数创建时的词法作用域应该包含创建环境的VO和[scope]作用域。也就是每个节点有个[[scope]]属性可以向上追述查找。
3.可执行代码的作用域和作用域链的概念有些模糊。作用域是指的函数创建时的词法作用域(创建时执行环境的AO集合)?函数执行时的执行上下文(当前执行时的AO集合)?函数的作用域链能访问到的所有AO集合?

建议把调用函数的时候创建活动变量时,会存在变量提升,这个时候变量提升又分为let,var,我感觉把这部分加进去会更好,多谢楼主分享

@yh284914425
@mqyqingfeng
关于很久之前的整个问题
var b = 10;
(function b(){
b = 20;
console.log(b);
})();
这个问题真是一个宝藏问题,查询途中发现了好多知识点。
立即执行表达式的本质:定义了一个函数表达式,然后立即执行该表达式。
大多数情况下我们会使用匿名函数表达式,如果用命名表达式——《javascript权威指南》8.1章节的函数定义部分,有这样一段话:“如果一个函数定义表达式包含名称,函数的局部作用域将会包含一个绑定到函数对象的名称。实际上,函数的名称将成为函数内部的一个局部变量”。
下面是规范中的说明: The BindingIdentifier in a FunctionExpression can be referenced from inside the FunctionExpression's FunctionBody to allow the function to call itself recursively. However, unlike in a FunctionDeclaration, the BindingIdentifier in a FunctionExpression cannot be referenced from and does not affect the scope enclosing the FunctionExpression.
还有一个被称为JSbug的问题。一般情况下,如果出现temp=10;这样的给未声明变量赋值的操作,temp会声明为全局变量。但是如果这样的情况出现在立即表达式中,temp会被声明为局部变量而不是全局变量。。这个首先就解决您的问题,b=20并不会影响到全局变量中的b。

接下来的是我自己不明白的问题,立即表达式的局部变量b难道不会被修改为20吗,为什么打印出现的是b函数本身,而不是20呢?
var b = 10;
(function b() {
var b = 20;
console.log(b);//20
})();
(function b() {
console.log(b);//[Function b]
})();
(function b() {
b=20;
console.log(b);//[Function b]
})();

我的理解:在表达是内部有一个隐式的var b的变量(跟表达式同名)。因此,在内部如果再次显式的var b =20,那显然会覆盖掉隐式的。但是,没有var的关键字声明,那么b=20只不过是修改这个隐式的b,而同时,这个b是只读的,不可修改的。因此输出的还是Function b.

其实你只要在开头加上'use strict',那么执行就会直接报错:b=20. 因为b是constant variable

function a() {

var aaa = 123;
function b(){
console.log(aaa); // 123
aaa = 234;
};
console.dir(b);
};
a();
function a() {
var aaa = 123;
function b(){
console.log(aaa); // 123
var aaa = 234;
};
console.dir(b);
};
a();

博主,以上两段代码输出不一样,请问用这个作用域链该如何解释?

第一个,函数b里面引用了函数a中的变量 aaa,所以形成了闭包, 第二个,函数b 没有引用函数a中的变量aaa,因为b函数内部用var声明了aaa,打印应该是undefined, 没有形成闭包, 所以 console.dir(b) 结果也不一样

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: undefined
}

初始化上下文的同时,创建checkscope函数,保存作用域到函数内部[[scope]]属性。

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

3.执行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.checkscope 函数执行完毕,checkscope 执行上下文从执行上下文栈中弹出。

ECStack=[
    globalContext
 ];

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

ECStack=[
    fContext,
    globalContext
 ];

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

fContext = {  
     AO:  {
         arguments: {
             length: 0
         }
     },
     Scope: [AO, checkscopeContext.AO, globalContext.VO],
     this: undefined
}

8.f 函数执行,沿着作用域链查找 scope 值,返回 scope 值
9.f 函数执行完毕,f 函数上下文从执行上下文栈中弹出

ECStack = [
        globalContext
    ];

和我的想法有点差异,欢迎交流。

  1. 创建checkscope函数的时候 ,不是保存 作用域到[[scope]]属性,而是保存所有父级的变量对象到[[scope]]属性,感觉也可以说成作用域。。
  2. 执行f函数的初始化过程, 应该是将函数f执行上下文中的AO压入自身上下文中的作用域链前端, 接着函数f执行, 沿着作用域链查找 scope属性,在自身AO中没有找到,再去上一层,也就是函数f创建时候复制自身[[scope]]属性得到的作用域链,应该是 [checkscope.AO, globalContext.VO] 在函数checkscope中找到了, 此处checkscope函数虽然出栈了,但是变量对象并没有销毁, 因为还被 函数f 的 [[scope]]属性引用,这也就是闭包吧

@yh284914425
@mqyqingfeng
关于很久之前的整个问题
var b = 10;
(function b(){
b = 20;
console.log(b);
})();
这个问题真是一个宝藏问题,查询途中发现了好多知识点。
立即执行表达式的本质:定义了一个函数表达式,然后立即执行该表达式。
大多数情况下我们会使用匿名函数表达式,如果用命名表达式——《javascript权威指南》8.1章节的函数定义部分,有这样一段话:“如果一个函数定义表达式包含名称,函数的局部作用域将会包含一个绑定到函数对象的名称。实际上,函数的名称将成为函数内部的一个局部变量”。
下面是规范中的说明: The BindingIdentifier in a FunctionExpression can be referenced from inside the FunctionExpression's FunctionBody to allow the function to call itself recursively. However, unlike in a FunctionDeclaration, the BindingIdentifier in a FunctionExpression cannot be referenced from and does not affect the scope enclosing the FunctionExpression.
还有一个被称为JSbug的问题。一般情况下,如果出现temp=10;这样的给未声明变量赋值的操作,temp会声明为全局变量。但是如果这样的情况出现在立即表达式中,temp会被声明为局部变量而不是全局变量。。这个首先就解决您的问题,b=20并不会影响到全局变量中的b。

接下来的是我自己不明白的问题,立即表达式的局部变量b难道不会被修改为20吗,为什么打印出现的是b函数本身,而不是20呢?
var b = 10;
(function b() {
var b = 20;
console.log(b);//20
})();
(function b() {
console.log(b);//[Function b]
})();
(function b() {
b=20;
console.log(b);//[Function b]
})();

你自己也说了呀 。如果一个函数定义表达式包含名称,函数的局部作用域将会包含一个绑定到函数对象的名称。实际上,函数的名称将成为函数内部的一个局部变量。 既然这个变量b(指向命名函数表达式的引用)存在于函数内部, 那么进行 b = 20 的赋值操作时,首先会找到 指向命名函数表达式引用的变量b,进行赋值,而这个变量又是不可改变的, 自然不会产生任何效果,输出都是命名函数表达式了

博主大大or各位路过大大,您们好!有一个疑惑也可能是犯二。。。,就是这篇文章的正文和评论部分,在ECStack的添加时都是按照unshift方法压入的,但是在博主大大的《JavaScript深入之执行上下文栈》一文中,是按照push和pop来分别压入和弹出函数执行上下文的,我现在有点懵,到底哪个是对的啊,怎么大家都是按照unshift压入的,应该以哪个为正确啊。。。哭唧唧.jpg

假如我们用JS数组这种数据结构来模拟栈的话,根据栈“先进先出“的特点,在进栈时可以理解为 要到 整个栈(数组)的头部去,也就是用unshift方法 ,当然也看你怎么来定义 头部和尾部对应数组的开头还是结尾了。。

命名的函数表

function b () {
b = 20
console.log(b)
}
b() // 20
大大,还是这里,如果不用立即执行函数,打印的b就是20, 使用立即函数打印的就是函数b

commented

命名的函数表

function b () {
b = 20
console.log(b)
}
b() // 20
大大,还是这里,如果不用立即执行函数,打印的b就是20, 使用立即函数打印的就是函数b

function b(){} 等于window.b,当执行函数b后把window.b重新赋值为20,所以打印会是20。立即执行函数中函数名优先级高些,函数体中给函数同名变量赋值不会覆盖原有操作。

分析第二段代码

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
}

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

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

4.开始执行checkscope函数,创建checkscope函数执行上下文,并将checkscope函数上下文压入执行上下文栈:

ECStack = [
    checkscopeContext,
    globalContext
];

5.checkscope函数上下文初始化:

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

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

f.[[scope]] = [checkscopeContext.AO, globalContext.VO]

6.checkscope函数执行,随着函数的执行,修改AO的值,所以此时checkscopeContext变更为:

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

接着返回了f函数.

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

ECStack = [
    globalContext
];

8.开始执行f 函数,创建f 函数执行上下文,并将f 函数上下文压入执行上下文栈:

ECStack = [
    fContext,
    globalContext
];

9.f 函数上下文初始化:

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

10.f 函数执行,沿着作用域链查找scope 的值,找到并返回了scope.

可是当 f 函数执行的时候,checkscope 函数上下文已经被销毁了(即从执行上下文栈中被弹出),怎么还会读取到 checkscope 作用域下的 scope 值呢?

这是因为 checkscope 函数执行上下文初始化时,f 函数同时被创建,保存作用域链到 f 函数的内部属性[[scope]],所以即使checkscope函数执行完毕,被弹出执行上下文栈,但是checkscopeContext.AO 依然存在于 f 函数的内部属性[scope]]中:

f.[[scope]] = [checkscopeContext.AO, globalContext.VO]

所以在f 函数执行的时候仍然可以通过 f 函数的作用域链能找到scope.所以这里就产生了闭包:

  • 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
  • 在代码中引用了自由变量

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

ECStack = [
    globalContext
];

@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。

大佬,这个虽然你解读的很合理,但是还是有点疑惑,函数不是在调用时才执行嘛?你解读的怎么会先执行var a1=t()这样的函数呢?如果把例子改成把nAdd()放在var a1=t() 的上一行呢 @mqyqingfeng

关于 this 有个疑问: 箭头函数的执行上下文是没有自己 this 的,继承定义时执行上下文的 this 。 那问题来了,定义时的上下文执行完成就销毁了,那么当箭头函数执行时。是怎么确定this的指向的, 我目前的理解是把this看成一个普通属性,保存在AO中,函数访问this就像查找普通变量一样,通过原型链来访问。

关于 this 有个疑问: 箭头函数的执行上下文是没有自己 this 的,继承定义时执行上下文的 this 。 那问题来了,定义时的上下文执行完成就销毁了,那么当箭头函数执行时。是怎么确定this的指向的, 我目前的理解是把this看成一个普通属性,保存在AO中,函数访问this就像查找普通变量一样,通过原型链来访问。

对于第二个例子,请教一下,各位大佬看下理解的对吗?

	//执行全局代码
	1.初始化全局对象
		GO = {
			scope: undefined;
			checkscope: reference to function checkscope(){} // 内存地址
		}
		// checkscope 函数被创建,保存作用域链到函数的内部属性[[scope]]
		checkscope.[[scope]] = [
			globalContext.VO
		]
		
	2.构建全局上下文
		globalContext = {
			VO: GO,
			scope: [globalContext.VO],
			this: window
		}
	3.压入ECS
		ECS = [
			globalContext
		]
	4.全局执行上下文赋值
		globalContext = {
			VO: {
				arguments:{
					length: 0
				},
				scope: 'global scope',
				checkscope: reference to function checkscope(){},
				}
			scope: [globalContext.VO],
			his: window
		}
	// 执行全局代码遇到函数执行
	5. 压入ECS
		ECS = [
			checkscopeContext,
			globalContext
		]
	6.函数执行上下文初始化
		checkscopeContext = {
			AO: {
				arguments:{
					length: 0
				}
				scope: "local scope",
				f: reference to function f(){}
			},
			scope:[AO, globalContext.VO]
			this: undefined
		}
		//f 函数被创建,保存作用域链到函数的内部属性[[scope]]
		f.[[scope]] = [
			checkscopeContext.AO, globalContext.VO
		]
	7.函数 checkscope 执行完毕,返回 函数 f(){return scope;},checkscope 从ECS中出栈
		ECS = [
			globalContext
		]
	8.此时函数 f 执行 ,fContext压入栈中
		ECS = [
			fContext,
			globalContext
		]
	9.函数执行上下文初始化
		fContext = {
			AO: {
				arguments:{
					length: 0
				}
			},
			scope:[AO, checkscopeContext.AO, globalContext.VO]
			this: undefined
		}
	10. f 函数执行,在作用域链 checkscopeContext.AO 中找到 scope ,返回 'local scope'
	11. f 函数执行完毕, fContext 从ECS中出栈
	12. globalContext f 从ECS中出栈

博主,请问下这个应该是ES3的上下文标准吧?查了下ES6/ES5的上下文已经是把变量对象这些概念去掉,引进词法环境、变量环境新的概念,以及外部词法环境引用。理解不到位的地方还烦请指正下~

个人感觉第3,第4步是不是反了,应该现实checkscope函数执行上下文初始化,然后再是checkscope函数的执行。这样的话在执行的过程中才会去将checkscopeContext中活动对象(AO)的部分值给赋上,比如AO.scope = 'local scope'

个人感觉第3,第4步是不是反了,应该现实checkscope函数执行上下文初始化,然后再是checkscope函数的执行。这样的话在执行的过程中才会去将checkscopeContext中活动对象(AO)的部分值给赋上,比如AO.scope = 'local scope'

第3步应该只是把这个创建的执行上下文压入栈,压入栈不等于立刻执行。第4步对这个上下文进行初始化,初始化完成后才真正开始执行。