文章是两年前写的,执行上下文的部分出现了不小的变化,下面补上一些较新的知识
执行上下文的过程
全局上下文创建
- 求this:浏览器中指向window
- 创建全局的词法环境:let/const/函数
- 创建全局变量环境,var
函数被调用,创建上下文,打入执行栈
- 求this:指向调用者
- 创建词法环境
- 记录变量/函数/参数
- 创建外部环境引用,全局变量或是父级(函数声明的时候的父级)的词法环境
- 创建环境变量:var
函数执行
- 运行代码,分配变量
- 离开函数
- 销毁上下文,出栈
—- 相关知识已过期 —- 请谨慎参考 —-
闭包和this,是两个相当高频的考点,然而你有没有想过,实际上他们两个都跟同一个知识点相关?
有请我们的这篇文章的主角,执行上下文
执行上下文
执行上下文是什么
可以简单理解执行上下文是js代码执行的环境,当js执行一段可执行代码时,会创建对应的执行上下文。他的组成如下
|
|
由于JS是单线程的,一次只能发生一件事情,其他事情会放在指定上下文栈中排队。js解释器在初始化执行代码时,会创建一个全局执行上下文到栈中,接着随着每次函数的调用都会创建并压入一个新的执行上下文栈。函数执行后,该执行上下文被弹出。
五个关键点:
- 单线程
- 同步执行
- 一个全局上下文
- 无限制函数上下文
- 每次函数调用创建新的上下文,包括调用自己
执行上下文建立的步奏
创建阶段
- 初始化作用域链
- 创建变量对象
- 创建arguments
- 扫描函数声明
- 扫描变量声明
- 求this
执行阶段
- 初始化变量和函数的引用
- 执行代码
this
在函数执行时,this 总是指向调用该函数的对象。要判断 this 的指向,其实就是判断 this 所在的函数属于谁。
指向调用对象
|
|
指向全局对象
|
|
注意
|
|
通过这个例子可以更加了解this是函数调用时才确定的
再绕一点
|
|
而
|
|
这是为什么呢?是因为优先读取foo中设置的a,类似作用域的原理吗?
通过打印foo和doFoo的this,可以知道,他们的this都是指向window的,他们的操作会修改window中的a的值。并不是优先读取foo中设置的a
因此如果把代码改成
|
|
上面的代码结果可以证实我们的猜测。
用new构造就指向新对象
|
|
apply/call/bind
大家应该都很熟悉,令this指向传递的第一个参数,如果第一个参数为null,undefined或是不传,则指向全局变量
箭头函数
箭头函数比较特殊,没有自己的this,它使用封闭执行上下文(函数或是global)的 this 值。this总是指向定义时所在的对象
|
|
事件监听函数
指向被绑定的dom元素
|
|
HTML
HTML标签的属性中是可能写JS的,这种情况下this指代该HTML元素。
|
|
变量对象
变量对象是与执行上下文相关的数据作用域,存储了上下文中定义的变量和函数声明。
变量对象式一个抽象的概念,在不同的上下文中,表示不同的对象
全局执行上下文的变量对象
全局执行上下文中,变量对象就是全局对象。
在顶层js代码中,this指向全局对象,全局变量会作为该对象的属性来被查询。在浏览器中,window就是全局对象。
|
|
函数执行上下文的变量对象
函数上下文中,变量对象VO就是活动对象AO。
初始化时,带有arguments属性。
函数代码分成两个阶段执行
进入执行上下文时
此时变量对象包括- 形参
- 函数声明,会替换已有变量对象
- 变量声明,不会替换形参和函数
函数执行
根据代码修改变量对象的值
举个例子
|
|
来分析一下过程
1.创建执行上下文时
VO = {
arguments: {0:5},
a: 5,
b: undefined,
c: [Function], //函数C覆盖了参数c,但是变量声明c无法覆盖函数c的声明
d: undefined, // 函数表达式没有提升,在执行到对应语句之前为undefined
}
- 执行代码时
通过最后的console可以发现,函数声明可以被覆盖
作用域链
先了解一下作用域
作用域
变量与函数的可访问范围,控制着变量及函数的可见性与生命周期。分为全局作用域和局部作用域。
全局作用域:
在代码中任何地方都能访问到的对象拥有全局作用域,有以下几种:
在最外层定义的变量;
全局对象的属性
任何地方隐式定义的变量(未定义直接赋值的变量),在任何地方隐式定义的变量都会定义在全局作用域中,即不通过 var 声明直接赋值的变量。
局部作用域:
JavaScript的作用域是通过函数来定义的,在一个函数中定义的变量只对这个函数内部可见,称为函数(局部)作用域
作用域链
作用域链是一个对象列表,用以检索上下文代码中出现的标识符。
标识符可以理解为变量名称,参数,函数声明。
函数在定义的时候会把父级的变量对象AO/VO的集合保存在内部属性[[scope]]中,该集合称为作用域链。
自由变量指的是不在函数内部声明的变量。
当函数需要访问自由变量时,会顺着作用域链来查找数据。子对象会一级一级的向上查找父对象的变量,父对象的变量对子对象是可见的,反之不成立。
作用域链就是在所有内部环境中查找变量的链式表。
可以直接的说,JS采用了词法作用域(静态作用域),JS的函数运行在他们被定义的作用域中,而不是他们被执行的作用域。可以举一个例子说明:
|
|
如果js采用动态作用域,打印出来的应该是6而不是3,这个例子说明了js是静态作用域。
函数作用域链的伪代码:
|
|
函数在运行激活的时候,会先复制[[scope]]属性创建作用域链,然后创建变量对象VO,然后将其加入到作用域链。
|
|
闭包
闭包是什么
闭包按照mdn的定义是可以访问自由变量的函数。自由变量前面提到过,指的是不在函数内部声明的变量。
闭包的形式
|
|
闭包的过程
写的不是很严谨。可能省略了一些过程
- 运行函数a
- 创建函数a的VO,包括变量num和函数b
- 定义函数b的时候,会保存a的变量对象VO和全局变量对象到[[scope]]中
- 返回函数b,保存到c1
- 运行c1
- 创建c1的作用域链,该作用域链保存了a的变量对象VO
- 创建c1的VO
- 运行c1,这是发现需要访问变量num,在当前VO中不存在,于是通过作用域链进行访问,找到了保存在a的VO中的num,对它进行操作,num的值被设置成2
- 再次运行c1,重复第二步的操作,num的值设置成3
一些问题
通过上面的运行结果,我们可以观察到,c2所访问num变量跟c1访问的num变量不是同一个变量。我们可以修改一下代码,来确认自己的猜想
|
|
因此我们可以确定,闭包所访问的变量,是每次运行父函数都重新创建,互相独立的。
注意,同一个函数中创建的自由变量是可以在不同的闭包共享的
|
|
补充一个查看作用域链和闭包的技巧
打开chrome控制台
|
|
闭包的作用
- 定时器
- debounce/throttle
最后
最后,我们再来总结一下执行上下文的过程,加深下印象
|
|
创建全局上下文执行栈
创建全局变量globalContext.VO.
创建checkscope函数
将全局变量VO保存为作用域链,设置到函数的内部属性[[scope]]
|
|
执行checkscope函数
创建函数执行上下文,将checkscope函数执行上下文压入执行上下文栈
|
|
函数执行上下文创建阶段
第一步是复制[[scope]],创建作用域链
|
|
第二步是创建活动对象AO
|
|
第三步是将活动对象AO放入作用域链顶端
|
|
第四步,求出this,上下文创建阶段结束
这里的this等于window
进入函数执行阶段
随着函数执行,修改AO的值
|
|
函数执行完毕
函数上下文从执行上下文栈弹出
|
|
文章写的比较长,涉及的范围也比较广,可能有不少的错误,希望大家可以指正。
本文章为前端进阶系列的一部分,
欢迎关注和star本博客或是关注我的github