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

JavaScript深入之执行上下文 #8

Open
mqyqingfeng opened this issue Apr 26, 2017 · 79 comments
Open

JavaScript深入之执行上下文 #8

mqyqingfeng opened this issue Apr 26, 2017 · 79 comments

Comments

@mqyqingfeng
Copy link
Owner

mqyqingfeng commented Apr 26, 2017

前言

《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,对作者也是一种鼓励。

@zuoyi615
Copy link

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

@mqyqingfeng
Copy link
Owner Author

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

@zuoyi615
Copy link

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

@flyerH
Copy link

flyerH commented May 27, 2017

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

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()作用域里的变量,但闭包的话,同一个变量名难道不是指向同一个内存地址的值吗

@mqyqingfeng
Copy link
Owner Author

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

@flyerH
Copy link

flyerH commented May 27, 2017

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

@mqyqingfeng
Copy link
Owner Author

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

@mqyqingfeng
Copy link
Owner Author

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

@lynn1824
Copy link

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

@Flying-Eagle2
Copy link

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

@mqyqingfeng
Copy link
Owner Author

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

default

@suoz
Copy link

suoz commented Jun 8, 2017

同时 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函数预编译阶段进行还是在函数执行阶段进行的?

@mqyqingfeng
Copy link
Owner Author

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

@suoz
Copy link

suoz commented Jun 8, 2017

@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函数的上下文,以及上面所提到的相同四步骤。

@mqyqingfeng
Copy link
Owner Author

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

@yh284914425
Copy link

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

@mqyqingfeng
Copy link
Owner Author

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

@yh284914425
Copy link

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

@mqyqingfeng
Copy link
Owner Author

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

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

@yh284914425
Copy link

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

@mqyqingfeng
Copy link
Owner Author

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

@Flying-Eagle2
Copy link

Flying-Eagle2 commented Jul 4, 2017

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

 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))

@mqyqingfeng
Copy link
Owner Author

mqyqingfeng commented Jul 4, 2017

@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

@Flying-Eagle2
Copy link

Flying-Eagle2 commented Jul 4, 2017

我就是这块没看懂

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

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

function (b) {
     return 4 + b
}

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

@yangtao2o
Copy link

分析第二段代码

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

@StonePang
Copy link

函数被创建时,保存作用域链到函数的内部属性[[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中我的这个想法貌似能说通。


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

@DerekRayna
Copy link

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

@foolseafish
Copy link

foolseafish commented Sep 26, 2020

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

@smallwebbird
Copy link

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

@panzhangguo
Copy link

@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

@anjina
Copy link

anjina commented Oct 29, 2020

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) 结果也不一样

@anjina
Copy link

anjina commented Oct 29, 2020

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]]属性引用,这也就是闭包吧

@anjina
Copy link

anjina commented Oct 29, 2020

@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,进行赋值,而这个变量又是不可改变的, 自然不会产生任何效果,输出都是命名函数表达式了

@anjina
Copy link

anjina commented Oct 29, 2020

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

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

@Bluestar123
Copy link

命名的函数表

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

@tanjian917
Copy link

命名的函数表

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

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

@xieyezi
Copy link

xieyezi commented Feb 24, 2021

分析第二段代码

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
];

@sign-ux
Copy link

sign-ux commented Aug 31, 2021

@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

@chinaOrange
Copy link

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

1 similar comment
@chinaOrange
Copy link

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

@lazylwz
Copy link

lazylwz commented Mar 31, 2022

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

	//执行全局代码
	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中出栈

@philling
Copy link

philling commented Sep 8, 2022

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

@ry928330
Copy link

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

@tinyblckc0000al
Copy link

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

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

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