javascript运行机制
写在前面
看下面两段代码,控制台上会输出什么?
function foo() {
var a = 2;
this.bar();
}
function bar() {
console.log('this.a', this.a);
console.log('a', a);
}
foo();
function foo() {
var a = 2;
bar();
}
function bar() {
console.log('this.a', this.a);
console.log('a', a);
}
foo();
答案是:
两段代码结果相同
this.a undefined
ReferenceError: a is not defined
如果你答对了,恭喜你已经对作用域、this 有一定认识,如果你想更深入了解代码执行过程中发生了什么,请往下看。
阅读本文,你将搞清楚以下概念:
- this 到底是什么
- 执行上下文 Execution Context
- 调用栈 Context Stack
- 作用域链 Scope Chain
- 变量对象 Virable Object(VO) 和 活动对象 Active Object(AO)
- 词法环境 Lexical Environment 和 变量环境 Variable Environment
阅读本文前,你需要以下知识储备
var
和函数声明
具有提升的特性,而const
let
函数表达式
没有- js 中作用域的只有
全局作用域
、函数作用域
,ES6 中新增了块级作用域
- 每个函数在执行时会创建一个活动对象,记录函数调用位置、变量等等
1. 什么是执行上下文
当一个函数被调用时,会创建一个活动记录
。这个记录会包 含函数在哪里被调用(调用栈)、函数的调用方法、传入的参数等信息。这个活动记录
又称为执行上下文、执行环境
小结:执行上下文和执行环境是一个概念,记录了很多信息
1.1 执行上下文 / 执行环境的分类
- 全局执行上下文 / 全局执行环境
- 函数执行上下文 / 函数执行环境
1.2 执行栈
在分析执行上下文内容之前,还需要了解执行栈
在我们的代码执行之前,引擎初始化一个执行栈
,初始化完毕后,将全局执行环境
压入执行栈,当每当一个函数开始执行,就创建一个函数执行环境
压入栈,函数执行完毕后,将该函数执行环境
弹出,关闭浏览器时,弹出全局执行环境
function foo(i) {
if (i < 0) return;
console.log('begin:' + i);
foo(i - 1);
console.log('end:' + i);
}
foo(2);
/*
begin:2
begin:1
begin:0
end:0
end:1
end:2
*/
图片及代码来自 https://juejin.im/post/5c2052...想有更多理解,请访问 https://www.cnblogs.com/wangf...
2. 执行环境的创建与激活
执行环境有两个阶段
- 创建阶段
- 激活 / 执行阶段
ES3 执行环境生成规范
创建阶段
- 创建作用域链 Scope Chain
- 创建变量对象 Variable Object
- 为 this 赋值
激活 / 执行阶段
- 完成变量分配,变 VO 为 AO,执行代码
ES6 执行环境生成规范
创建阶段
- 为 this 赋值
- 创建词法环境 Lexical Environment
- 创建变量环境 Variable Environment
执行阶段
- 完成变量分配,执行代码
3. ES3 中的 Execution Context
3.1 作用域链 Scope Chain
我的理解是,生成Scope Chain
时,会扫描函数作用域,注意要与执行上下文区分,函数中访问父级作用域的操作,就是顺着Scope Chain
实现的。举个例子
var a = 1;
function foo() {
var a = 2;
bar();
}
function bar() {
console.log('a', a);
}
foo();
打印结果如何?
1
为什么是这个结果?
在执行 bar() 时,生成的执行上下文中没有变量 a 存在,于是,顺着 scope chain 向上寻找父级作用域,于是,找到了 bar 父级作用域 window 的变量 a,而不是 foo 作用域中的变量 a
现在你大概明白作用域是如何在函数执行时起作用的了吧
也许你对作用域的理解产生动摇,请访问 深入理解javascript原型和闭包(12)——简介【作用域】
Scope Chain 连接的是作用域,而不是执行上下文!
PS:这里的作用域在《你不知道的js》中被叙述为词法作用域,对以上打印结果有质疑的同学,可以检索下动态作用域。也可以翻阅《你不知道的js 上卷》第58 59页
3.2 变量对象 VO,活动对象 AO
- 变量对象:存储 js 执行上下文中的函数标志符、形参 argument、变量声明,这个对象在 js 环境下是不可访问的。
- 活动对象:存储变量对象 VO 中的声明、形参 argument、函数标识符,与 VO 不同的是,AO 是“激活版”的 VO,其存储的内容是可以被访问到的
对于二者区别,下面将中伪代码展示
创建 VO 分三步
- 创建 argument 对象
- 扫描上下文中的函数声明,以函数名为 key,在堆中对应的地址为 value,记录在 VO 中(涉及概念
提升
、堆栈模型)。如果有两个同名函数,以后出现的为准,VO 中后出现的函数覆盖先出现的函数 - 扫描上下文中 var 变量声明,在以变量名为 key,并初始化 value 为 undefined。若同 var 变量出现第二个声明,则忽略并继续扫描
// 以上可以理解
function foo(){ console.log('foo声明第一次') }
function foo(){ console.log('foo声明第二次') }
foo()
// 'foo声明第二次'
ES3 时期变量声明只有 var,还没有 const let
3.3 this
其实 this 的绑定规则很简单,this指向调用该函数的对象
,如
var a = 1;
var obj = {
a: 2,
foo: foo
}
function foo() {
console.log(this.a);
}
obj.foo(); // 2
foo();
按规则,执行 obj.foo() 时,this 指向obj。执行 foo() 等价于 window.foo() ,所以 this 指向 window
当然,bind apply call 可以改变函数内部的 this 指向,本文不做讨论。我想表明的是,this 很简单,只是执行函数时,生成执行上下文中的一个步骤。this 赋值很有规律,this指向调用该函数的对象
,并不是什么牛鬼蛇神,只是一个小步骤
3.4 实战演习一波
function foo(param1, param2) {
var name = 'cregskin';
var age = 20;
function bar() {
var name = 'jellyFish'
var age = 20;
}
bar();
}
foo();
// 1. 初始化,创建全局执行上下文
GlobalExecutionContext = {
ScopeChain: null,
VariableObject: { // 创建(js 不可访问)
foo: pointer to foo
},
this: window
}
GlobalExecutionContext = {
ScopeChain: null,
ActiveObject: { // 激活(js 可以访问)
foo: pointer to foo
},
this: window
}
// 2. 执行foo() ,创建 foo() 执行上下文
fooExecutionContext = {
ScopeChain: Global{},
VariableObject: { // 创建(js 不可访问)
name: undefined,
age: undefined,
bar: pointer to bar
}
}
fooExecutionContext = {
ScopeChain: Global{},
ActiveObject: { // 激活(js 可以访问)
name: undefined,
age: undefined,
bar: pointer to bar
}
}
// 3. 执行 bar(),创建 bar() 执行上下文
barExecutionContext = {
ScopeChain: foo{},
VariableObject: { // 创建(js 不可访问)
name: undefined,
age: undefined
},
this: window
}
barExecutionContext = {
ScopeChain: foo{},
ActiveObject: { // 激活(js 可以访问)
name: undefined,
age: undefined
},
this: window
}
怎么样!是不是很通透了,我再插一句话
ES3 中只有全局作用域和函数作用域,执行上下文是函数执行时才生成的。请再次回忆一下,执行上下文的 VO AO 包含的内容,不就是函数作用域自身中的变量声明和函数声明吗!Scope Chain 连接的作用域,不就是函数定义时的作用域嵌套关系吗!执行上下文和作用域竟然有如此高度的一致性!妙不可言
3.5 小结一下
列一下 ES3 中执行上下文的结构
- Scope Chain
Variable Object / Active Object
- var 变量声明
- 函数声明
- this
结构简单。下面我将介绍 ES5 中的执行上下文,大同小异
4. ES6 中的 Execution Context
4.1 词法环境 Lexical Environment
4.1.1 概念
官方 给出的概念是:词法环境是一种规范类型,基于 ECMAScript 代码的词法嵌套结构来定义标识符与特定变量和函数的关联关系。词法环境由环境记录(environment record)和可能为空引用(null)的外部词法环境组成。
词法环境在 js 中也有很多别称:词法作用域、作用域是不是能串起来了!相比 ES3, ES6 中的词法环境和作用域在命名上有更深的羁绊!
4.1.2 组成
Laxical Environment 词法环境
Environment Record 环境记录器
- 变量声明
- 函数声明
- Outer 外部环境引用
类比一下之前讲到的 ES3 版本的执行上下文,你的脑中可能有大致的对应关系了
ES3 | ES6 |
---|---|
Scope Chain | Outer |
Viriable Object / Active Object | Environment Record |
哈哈好厉害,我第一看的时候都缕了好久
下面我将介绍Environment Record ,本质上与 VO AO 还是有一些区别的
注意 ES6 引入了块级作用域,const let 没有提升
,在这里是要有体现的
4.1.3 Environment Record 环境记录器
对应两种执行环境(全局执行环境、函数执行环境),环境记录器也有两种,
声明式环境记录器(在函数执行环境中使用)
- 储存 const let 变量
- 储存 argument 参数
- 储存函数声明
对象环境记录器(在全局执行环境中使用)
- 变量声明
- 函数声明
var 声明呢?有不少 var const let 混用的场景啊。别急,下文中会介绍
4.1.4 Outer 外部环境引用
这个就不多介绍了,与 ES3 中 Scope Chain 一样。
作用是访问父级作用域
4.2 Variable Environment 变量环境
还记得我们之前埋的坑吗——var 变量存储在哪里?
4.2.1 组成
Variable Environment 变量环境
Environment Record 环境记录器(在函数执行环境中使用)
- 存储 var 变量声明
Object environment records 对象环境记录器(在全局执行环境中使用)
- 变量声明
- 函数声明
在 ES6 中,词法环境和变量环境的一个不同就是前者被用来存储函数声明和变量(let
和const
)绑定,而后者只用来存储var
变量绑定。
4.3 实战演练
function foo() {
const a = 2;
this.bar();
}
function bar() {
console.log('this.a', this.a);
console.log('a', a);
}
foo();
创建作用域链之前需要了解一下作用域
深入理解javascript原型和闭包(12)——简介【作用域】
步骤如下
看到这里,文章开头的那两道题你应该可以解了。 我先给出执行环境的内容
// 1. 初始化,创建全局执行上下文
GlobalExecutionContext = {
Outer: null,
LexicalEnvironment: { // 创建(js 不可访问)
foo: pointer to foo
arguments: []
},
this: window
}
GlobalExecutionContext = {
Outer: null,
LexicalEnvironment: { // 激活(js 可以访问)
foo: pointer to foo,
arguments: []
},
this: window
}
// 2. 执行foo() ,创建 foo() 执行上下文
fooExecutionContext = {
Outer: Global{},
LexicalEnvironment: { // 创建(js 不可访问)
a: <undefined>, // 注意 const 没有变量提升
bar: pointer to bar,
arguments: []
}
}
fooExecutionContext = {
Outer: Global{},
LexicalEnvironment: { // 激活(js 可以访问)
a: <undefined>, // 注意 const 没有变量提升
bar: pointer to bar,
arguments: []
},
this: window
}
// 3. 执行 bar(),创建 bar() 执行上下文
barExecutionContext = {
Outer: Global{},
LexicalEnvironment: { // 创建(js 不可访问)
arguments: []
},
this: window
}
barExecutionContext = {
Outer: Global{},
LexicalEnvironment: { // 激活(js 可以访问)
arguments: []
},
this: window
}
为方便阅读,我把代码拉下来
function foo() {
const a = 2;
this.bar();
}
function bar() {
console.log('this.a', this.a);
console.log('a', a);
}
foo();
- 创建全局执行环境
- 执行 foo(),创建 foo 函数执行环境
执行 bar(),创建 bar 函数执行环境
- 访问 this.a => window.a,打印
undefined
- 访问 a,bar 函数执行环境中找不到,于是顺着 Outer 向上寻找到 window 的执行环境,没有 a 的声明。抛出错误
ReferenceError: a is not defined
- 访问 this.a => window.a,打印
注意,这里3中2说顺着 Outer 向上寻找到 window 的执行环境
,指的是window的作用域和执行环境已经高度一致了。 严格来说因该是顺着 Outer 向上寻找到 window 的词法作用域
*
通透!
别忘点赞!?非常感谢
转载请标明出处!
Reference
[[译] 理解 JavaScript 中的执行上下文和执行栈 - 掘金翻译计划](https://juejin.im/post/5ba321...
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。