JavaScript 深入之执行上下文

发布于 2022-07-04 08:21:48 字数 2881 浏览 1123 评论 42

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

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

思考题

这样一道思考题:

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。虽然两段代码执行的结果一样,但是两段代码究竟有哪些不同呢?

两者的区别在于执行上下文栈的变化不一样,然而,如果是这样笼统的回答,依然显得不够详细,本篇就会详细的解析执行上下文栈和执行上下文的具体变化过程。

具体执行分析

我们分析第一段代码:

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

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(42

巡山小妖精 2022-05-04 13:51:27

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


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

时光清浅 2022-05-04 13:51:27

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

日久见人心 2022-05-04 13:51:27

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

无语# 2022-05-04 13:51:27

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

两个我 2022-05-04 13:51:27

@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

夜声。 2022-05-04 13:51:27

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

纵情客 2022-05-04 13:51:27

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]]属性引用,这也就是闭包吧
痴情 2022-05-04 13:51:27

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

情深如许 2022-05-04 13:51:27

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

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

陌若浮生 2022-05-04 13:51:27

命名的函数表

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

携君以终年 2022-05-04 13:51:27

命名的函数表

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

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

新一帅帅 2022-05-04 13:51:27

分析第二段代码

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
];
傲性难收 2022-05-04 13:51:27

@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

洒一地阳光ヽ 2022-05-04 13:51:27

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

宁愿没拥抱 2022-05-04 13:51:27

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

幸福不弃 2022-05-04 13:51:27

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

	//执行全局代码
	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中出栈
第几種人 2022-05-04 13:51:26

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

记忆で 2022-05-04 13:51:26

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

却一份温柔 2022-05-04 13:51:26

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

静谧幽蓝 2022-05-04 13:51:26

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

default

桃扇骨 2022-05-04 13:51:26

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

反话 2022-05-04 13:51:26

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

帅哥哥的热头脑 2022-05-04 13:51:26

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

筱武穆 2022-05-04 13:51:26

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

请帮我爱他 2022-05-04 13:51:26

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

冷︶言冷语的世界 2022-05-04 13:51:26

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

玉环 2022-05-04 13:51:26

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

帥小哥 2022-05-04 13:51:26

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

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

牵强ㄟ 2022-05-04 13:51:26

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

回忆那么伤 2022-05-04 13:51:26

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

听不够的曲调 2022-05-04 13:51:26

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

 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))
安穩 2022-05-04 13:51:26

@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

樱娆 2022-05-04 13:51:26

我就是这块没看懂

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

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

function (b) {
     return 4 + b
}

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

冷月断魂刀 2022-05-04 13:51:26

好难理解呀~

享受孤独 2022-05-04 13:51:26

分析第二段代码

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

暮倦 2022-05-04 13:51:25

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

两人的回忆 2022-05-04 13:50:45

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

反话 2022-05-04 13:50:06

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

___失温。 2022-05-04 13:49:01

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

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

╮执着的年纪 2022-05-04 13:48:48

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

墟烟 2022-05-04 13:43:32

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

波浪屿的海角声 2022-05-04 11:39:55

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

~没有更多了~

关于作者

七度光

暂无简介

0 文章
0 评论
21 人气
更多

推荐作者

策马西风

文章 0 评论 0

柠檬心

文章 0 评论 0

1331

文章 0 评论 0

七度光

文章 0 评论 0

qq_oc2LaO

文章 0 评论 0

野却迷人

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文