开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 3 天,点击查看活动详情
Step1: 执行上下文
浏览器并不理解我们在应用中编写的JS代码。代码需要被转换成浏览器和计算机能够理解的格式:机器码。浏览器在读取HTML时,如果遇到了<script> 标签或包含JavaScript代码的属性如onClick,会发送给JavaScript引擎。浏览器的JavaScript引擎会创造一个特殊的环境来处理这些JavaScript代码的转换和执行。这个特殊的环境被称为执行上下文。
// 执行上下文代码示例
const ExecutionContextObj = {
VO: window, // 变量对象
ScopeChain: {}, // 作用域链
this: window
};
执行上下文 = 全局执行上下文 + 函数执行上下文
Step2:全局执行上下文
- Chrome浏览器打开控制台,输入this,控制台会输出该表达式的结果,如下图,输出值为Window对象,这就是全局执行上下文,它是由浏览器创建,可通过this直接访问。
- 全局对象window有很多预定义的方法和属性,在全局环境任意处都可直接访问它们。我们用var声明的全局变量也会存储在window全局对象中,可直接访问。
另外,变量前不加修饰符,视为全局变量
function f1(){
n=999;
}
f1(); // 执行后,n才赋值。
alert(window.n); // 输出 999,变量前不加修饰符,视为全局变量
- 每一个JavaScript文件只能有一个全局执行上下文。每当 JavaScript 引擎接收到脚本文件时,它首先会创建一个默认的执行上下文,称为全局执行上下文,它是基础/默认的执行上下文,所有 不在函数内部的JavaScript代码 都在这里执行。
Step3:函数执行上下文
每当函数被调用时,JavaScript引擎就会在全局执行上下文内部创建另一种执行上下文,称为函数执行上下文,并在函数执行上下文中评估和执行函数中的代码。因为每个函数调用都创建自己的函数执行上下文,所以在脚本运行期间会有多个函数执行上下文。
- 函数执行上下文可以有很多个;
- 每当函数被调用时都会创建一个函数上下文;
Step4:执行栈
JS代码首次运行,都会先创建一个全局执行上下文并压入到执行栈中,之后每当有函数被调用,都会创建一个新的函数执行上下文并压入栈内;当调用一个函数的时候,函数体内的代码会被编译,并创建函数执行上下文,一般情况下,函数执行结束之后,创建的函数执行上下文会被销毁。整个程序执行完毕,全局执行上下文会被销毁。
Step5:创建执行上下文
- 绑定 this
- 创建词法环境组件 LexicalEnvironment
- 创建变量环境组件 VariableEnvironment
// 创建执行上下文
ExecutionContext = {
// 确定this的值
ThisBinding = <this value>,
// 创建词法环境组件
LexicalEnvironment = {},
// 创建变量环境组件
VariableEnvironment = {},
};
绑定this
- 全局执行上下文中,this总是指向全局对象,在浏览器环境下,this指向window对象
- 函数执行上下文中,如果函数被对象调用,那么this指向该对象,否则,指向全局对象window或者undefined(严格模式)
Step6: 词法环境
V8里JS的编译执行过程
-
V8引擎刚拿到
执行上下文的时候,会把代码从上到下一行一行的先做分词/词法分析(Tokenizing/Lexing)。分词是指:比如
var a = 2;这段代码,会被分词为:vara2和;这样的原子符号(atomic token);词法分析是指:登记变量声明、函数声明、函数声明的形参。后续代码执行的时候就知道去哪里拿变量的值和函数了,这个登记的地方就是
Lexical Environment(词法环境) -
在分词结束以后,会做代码解析,引擎将 token 解析翻译成一个AST(抽象语法树), 在这一步的时候,如果发现语法错误,就会直接报错不会再往下执行。
var greeting = "Hello"; console.log(greeting); greeting = ."Hi"; // SyntaxError: unexpected token . // 没有打印出 hello,而是先报错,说明JS引擎在真正执行代码之前,会做代码解析。 -
引擎生成CPU可以执行的机器码。
// 输出
2
6
// 全局词法环境
GlobalEnvironment = {
outer: null, //全局环境的外部环境引用为null
GlobalEnvironmentRecord: {
//全局this绑定指向全局对象
[[GlobalThisValue]]: ObjectEnvironmentRecord[[BindingObject]],
//声明式环境记录,除了全局函数和var,其他声明都绑定在这里
DeclarativeEnvironmentRecord: {
x: 1,
y: 5
},
//对象式环境记录,绑定对象为全局对象
ObjectEnvironmentRecord: {
a: 2,
foo:<< function>>,
baz:<< function>>,
isNaNl:<< function>>,
isFinite: << function>>,
parseInt: << function>>,
parseFloat: << function>>,
Array: << construct function>>,
Object: << construct function>>
...
...
}
}
}
//foo函数词法环境
fooFunctionEnviroment = {
outer: GlobalEnvironment,//外部词法环境引用指向全局环境
FunctionEnvironmentRecord: {
[[ThisValue]]: GlobalEnvironment,//this绑定指向全局环境
bar:<< function>>
}
}
//bar函数词法环境
barFunctionEnviroment = {
outer: fooFunctionEnviroment,//外部词法环境引用指向foo函数词法环境
FunctionEnvironmentRecord: {
[[ThisValue]]: GlobalEnvironment,//this绑定指向全局环境
b: 3
}
}
//baz函数词法环境
bazFunctionEnviroment = {
outer: GlobalEnvironment,//外部词法环境引用指向全局环境
FunctionEnvironmentRecord: {
[[ThisValue]]: GlobalEnvironment,//this绑定指向全局环境
a: 10
}
}
我们可以看到词法环境和我们代码的定义一一对应,每个词法环境都有一个outer指向上一层的词法环境,当运行上面代码,函数bar的词法环境里没有变量a,所以就会到它的上一层词法环境(foo函数词法环境)里去找,foo函数词法环境里也没有变量a,就接着去foo函数词法环境的上一层(全局词法环境)去找,在全局词法环境里var a=2,沿着outer一层一层词法环境找变量的值就是作用域链。在沿着作用域链向上找变量的时候,找到第一个就停止往上找,如果到全局词法环境里还是没有找到,因为全局词法环境里的outer是null,没办法再往上找,就会报ReferenceError。
变量提升 函数提升
V8引擎执行代码的大致可以分为三步,先做分词和词法分析,然后解析生成AST,最后生成机器码执行代码。在词法分析的时候会生成
词法环境登记变量,对于变量声明和函数声明,词法环境的处理是不一样的。
- 对于变量声明
var a=2;let x=1;,给变量分配内存并初始化为undefined,赋值语句是在第三步生成机器码真正执行代码的时候才执行。 - 对于函数声明
function foo(){...},会在内存里创建函数对象,并且直接初始化为该函数对象。
Step7 : 闭包
有时候我们需要从外部得到函数内部的局部变量,我们可以通过在函数内部再定义一个函数来做到。如下示例:
- 我们的需求:在f1外部词法环境中,获取f1词法环境中的局部变量 n
- 因为,f2的父外部词法环境是f1,所以,f2可以获取到 f1中的 n
- 思路:定义
f2,通过f2将n返回,在f1中将f2函数作为返回值 - 在f1的外部词法环境中,调用执行f1即可得到f2函数,执行f2函数,拿到f1的的局部变量
f2就是闭包。闭包就是能够读取其它函数内部变量的函数。进一步的可简单理解为,
闭包是定义在一个函数内部的函数
function f1(){
var n=999;
function f2(){
return n;
}
return f2;
}
var result=f1();
console.log(result());
参考文献
JavaScript Execution Context – How JS Works Behind The Scenes
开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 3 天,点击查看活动详情