-
Notifications
You must be signed in to change notification settings - Fork 4.7k
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
Comments
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 当执行 所以当执行 nAdd 函数,我们执行的是其实是 fn2,而不是 fn1,我们更改的是 a2 形成的闭包里的 n 的值,并没有更改 a1 形成的闭包里的 n 的值。所以 a1() 的结果为 99 ,a2()的结果为 100。 |
@mqyqingfeng 非常感谢您的解答,谢谢! |
@flyerH 哈哈,不用这么客气,有问题就留言讨论哈~ |
@zuoyi615 哈哈,确实不好理解,因为涉及到很多规范上的内容,需要边查规范边读,但我也正是通过研究 this 第一次克服了对于全英文的规范的恐惧,希望你也去试一试~ |
第一个函数查找上级作用域中scope |
博主,请问那个nAdd(); 什么时候调用的? 我看不懂 |
@Flying-Eagle2 当然是执行这个函数的时候调用的啦~ |
同时 f 函数被创建,保存作用域链到 f 函数的内部属性[[scope]]
checkscope预编译阶段,形参、函数f声明、变量scope声明。 |
@suoz 我认为是在 checkscope 函数预编译阶段 |
@mqyqingfeng 大大
你看我这么理解对么~ 执行函数checkscope时,分为预编译阶段和执行阶段,预编译阶段就是你所说的创建执行上下文、执行上下文初始化(复制函数[[scope]]属性创建作用域链、使用arguments创建活动对象、初始化活动对象{即形参、函数声明、变量声明}、将活动对象压入作用域链的顶端)。 当函数checkscope执行,处于预编译阶段中函数声明的时候,此时只是创建了f函数(只是创建了f函数的[[scope]]属性,这个属性只包含了checkscope函数的活动对象和全局变量对象,并不包含f函数的活动对象) 等到函数checkscope处于执行阶段时,就是 |
@suoz 是哒~ o( ̄▽ ̄)d |
全局上下文初始化里面的VO里面的global是什么情况啊? |
@yh284914425 这个 global 表示全局对象哈~ |
大神,能不能帮我分析下 下面执行上下文的具体处理过程 谢谢 |
@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)) |
我们先看这段代码的结构,简化一下就是: 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 又是 谁传的?????啊啊啊啊啊,我真没看懂 |
分析第二段代码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 函数被创建,保存作用域链到函数的内部属性 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 函数的内部属性 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 函数的内部属性 fContext = {
Scope: [AO, checkscopeContext.AO, globalContext.VO],
} 所以,闭包的概念产生了,定义:
6.f 函数执行完毕,f 函数上下文从执行上下文栈中弹出 ECStack = [globalContext]; Over,hahahah |
函数被创建时,保存作用域链到函数的内部属性[[scope]]这一步的一点个人理解,不知是否正确最近才看到这个博客,好多概念有了全新的认识,感谢博主的分享。
checkscope.[[scope]] 就是ECStack中最上层上下文globalContext.Scope的引用
f.[[scope]] 就是此时ECStack中最上层上下文checkscopeContext.Scope的引用 在代码1中我的这个想法貌似能说通。 以上两个疑问还请聚聚们赐教 |
总感觉执行函数的作用域链长度 == 执行上下文栈的长度,不知道这样理解对不对 |
@mqyqingfeng博主大大你好: |
建议把调用函数的时候创建活动变量时,会存在变量提升,这个时候变量提升又分为let,var,我感觉把这部分加进去会更好,多谢楼主分享 |
我的理解:在表达是内部有一个隐式的var b的变量(跟表达式同名)。因此,在内部如果再次显式的var b =20,那显然会覆盖掉隐式的。但是,没有var的关键字声明,那么b=20只不过是修改这个隐式的b,而同时,这个b是只读的,不可修改的。因此输出的还是Function b. 其实你只要在开头加上'use strict',那么执行就会直接报错:b=20. 因为b是constant variable |
第一个,函数b里面引用了函数a中的变量 aaa,所以形成了闭包, 第二个,函数b 没有引用函数a中的变量aaa,因为b函数内部用var声明了aaa,打印应该是undefined, 没有形成闭包, 所以 console.dir(b) 结果也不一样 |
和我的想法有点差异,欢迎交流。
|
你自己也说了呀 。如果一个函数定义表达式包含名称,函数的局部作用域将会包含一个绑定到函数对象的名称。实际上,函数的名称将成为函数内部的一个局部变量。 既然这个变量b(指向命名函数表达式的引用)存在于函数内部, 那么进行 b = 20 的赋值操作时,首先会找到 指向命名函数表达式引用的变量b,进行赋值,而这个变量又是不可改变的, 自然不会产生任何效果,输出都是命名函数表达式了 |
假如我们用JS数组这种数据结构来模拟栈的话,根据栈“先进先出“的特点,在进栈时可以理解为 要到 整个栈(数组)的头部去,也就是用unshift方法 ,当然也看你怎么来定义 头部和尾部对应数组的开头还是结尾了。。 |
function 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]] = [
globalContext.VO
]; 4.开始执行 ECStack = [
checkscopeContext,
globalContext
]; 5.
checkscopeContext = {
AO: {
arguments: {
length: 0
},
scope: undefined,
f: reference to function f(){}
},
Scope: [AO, globalContext.VO],
this: undefined
} 初始化的同时, f.[[scope]] = [checkscopeContext.AO, globalContext.VO] 6. checkscopeContext = {
AO: {
arguments: {
length: 0
},
scope: "local scope",
f: reference to function f(){}
},
Scope: [AO, globalContext.VO],
this: checkscopeContext.AO
} 接着返回了 7. ECStack = [
globalContext
]; 8.开始执行 ECStack = [
fContext,
globalContext
]; 9.
fContext = {
AO: {
arguments: {
length: 0
}
},
Scope: [AO, checkscopeContext.AO, globalContext.VO],
this: undefined
} 10. 可是当 这是因为 f.[[scope]] = [checkscopeContext.AO, globalContext.VO] 所以在
11. ECStack = [
globalContext
]; |
大佬,这个虽然你解读的很合理,但是还是有点疑惑,函数不是在调用时才执行嘛?你解读的怎么会先执行var a1=t()这样的函数呢?如果把例子改成把nAdd()放在var a1=t() 的上一行呢 @mqyqingfeng |
关于 this 有个疑问: 箭头函数的执行上下文是没有自己 this 的,继承定义时执行上下文的 this 。 那问题来了,定义时的上下文执行完成就销毁了,那么当箭头函数执行时。是怎么确定this的指向的, 我目前的理解是把this看成一个普通属性,保存在AO中,函数访问this就像查找普通变量一样,通过原型链来访问。 |
1 similar comment
关于 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步对这个上下文进行初始化,初始化完成后才真正开始执行。 |
前言
在《JavaScript深入之执行上下文栈》中讲到,当 JavaScript 代码执行一段可执行代码(executable code)时,会创建对应的执行上下文(execution context)。
对于每个执行上下文,都有三个重要属性:
然后分别在《JavaScript深入之变量对象》、《JavaScript深入之作用域链》、《JavaScript深入之从ECMAScript规范解读this》中讲解了这三个属性。
阅读本文前,如果对以上的概念不是很清楚,希望先阅读这些文章。
因为,这一篇,我们会结合着所有内容,讲讲执行上下文的具体处理过程。
思考题
在《JavaScript深入之词法作用域和动态作用域》中,提出这样一道思考题:
两段代码都会打印'local scope'。虽然两段代码执行的结果一样,但是两段代码究竟有哪些不同呢?
紧接着就在下一篇《JavaScript深入之执行上下文栈》中,讲到了两者的区别在于执行上下文栈的变化不一样,然而,如果是这样笼统的回答,依然显得不够详细,本篇就会详细的解析执行上下文栈和执行上下文的具体变化过程。
具体执行分析
我们分析第一段代码:
执行过程如下:
1.执行全局代码,创建全局执行上下文,全局上下文被压入执行上下文栈
2.全局上下文初始化
2.初始化的同时,checkscope 函数被创建,保存作用域链到函数的内部属性[[scope]]
3.执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 函数执行上下文被压入执行上下文栈
4.checkscope 函数执行上下文初始化:
同时 f 函数被创建,保存作用域链到 f 函数的内部属性[[scope]]
5.执行 f 函数,创建 f 函数执行上下文,f 函数执行上下文被压入执行上下文栈
6.f 函数执行上下文初始化, 以下跟第 4 步相同:
7.f 函数执行,沿着作用域链查找 scope 值,返回 scope 值
8.f 函数执行完毕,f 函数上下文从执行上下文栈中弹出
9.checkscope 函数执行完毕,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,对作者也是一种鼓励。
The text was updated successfully, but these errors were encountered: