执行上下文
执行上下文又称为执行环境,每当 JS 引擎解析可执行代码时,就会进入一个执行环境。
本篇博客将在浏览器环境下讲述执行上下文。在浏览器环境中,执行上下文分为三种:
-
全局执行上下文 默认的、最基础的执行上下文。不在任何函数中的代码都位于全局执行上下文中。
-
函数执行上下文 每个函数都拥有自己的执行上下文,但是只有在函数被调用的时候才会被创建。一个程序中可以存在任意数量的函数执行上下文。每当一个新的执行上下文被创建,它都会按照特定的顺序执行一系列步骤。
-
Eval 函数执行上下文 这种上下文一般不考虑
执行上下文是什么?
可以把执行上下文看成一个对象 {} 来理解,有三个重要属性,
- 变量对象(Variable Object, 简称 VO),存入声明的变量,也是一个对象 {},里面存有
- 函数的所有形参 (如果是函数执行上下文)
- 函数声明
- 变量声明
-
作用域链([[scope]]),扩连作用域链
作用域链是由当前环境与上层环境的一系列变量对象组成,它保证了对执行环境有权访问的所有变量和函数的有序访问
上文的作用域中讲到过函数的作用域在函数定义的时候就决定了,因为函数有一个内部属性 [[scope]],当函数创建 的时候,就会保存所有父变量对象到其中,当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到, 就会从自己的 [[scope]] 中保存的父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对 象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。
-
确定 this 的指向 见 [this 到底是什么](#this 到底是什么 "#jump")
执行上下文的生命周期
创建阶段(生成变量对象, 建立作用域链, 确定 this 指向) => 执行阶段(变量赋值,函数引用、执行其它的代码) => 回收阶段
js 引擎如何管理执行上下文
html
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<title>浏览器环境下的执行上下文</title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
<script>
// 全局执行上下文
var author = "qinghuanI";
function foo() {
var author = "qinghuanI";
// 函数执行上下文
}
foo();
// eval 函数创建新执行上下文
eval("var author = 'qinghuanI'");
</script>
</body>
</html>
javascript 引擎创建了执行上下文栈(Execution context stack, ECS) 来管理执行上下文。以 index.html
为例,
- 当 JS 引擎读取 script 标签时,会创建一个全局执行上下文,并将其推入当前的执行上下文栈中。创建变量对象,里面有未初始化的 author 变量,作用域链此时没有,确定 this 指向 window 对象。
- 引擎继续往下执行,执行 foo() 时,会为 foo 函数创建一个新的执行上下文并将其推到当前执行栈的顶端。此时会创建变量对象,建立作用域链,确定 this 指向 window 对象。
用伪代码解释上述过程:
js
// 解析过程从
[
{
AO: { this: window },
[[scope]]: {}
}
];
// 变为
[
{
AO: {},
this: window,
[[scope]]: {},
},
{
AO: {
this: window, // 函数执行时动态绑定
arguments: [], // 为类数组
author: "qinghuanI";
},
[[scope]]: {
AO: {
this: window,
arguments: [], // 为类数组
author: "qinghuanI";
},
GO: {
this: window,
window: {...},
author: "qinghuanI";
}
}
}
]
这种标识符(变量)的解析过程,与函数的生命周期有关。函数的生命周期可以分为创建和激活(调用时)两个阶段。在函数创建时,函数的内部存在一个 [[scope]] 属性(内部属性),[[scope]]是所有变量对象的层级链。[[scope]] 属性在函数创建时被存储,永远不变,直到函数被销毁。函数可以不被调用,但该属性一直存在。
csharp
作用域链的长度与函数嵌套有关,比如在递归场景下。
this 指向与函数被调用时的所属对象有关
变量对象与函数体内声明变量的关键字有关(var 与 const let 有区别)
闭包
当函数可以记住并访问所在的词法作用域时,即时函数是在当前词法作用域之外执行,这时就产生了闭包
用 index.html
为例讲解闭包
html
<!-- index.html -->
<script>
function foo() {
var author = "qinghuanI";
function bar() {
console.log(this); // window
return author;
}
return bar;
}
var fn = foo(); // fn === function bar() {/* code */}
fn();
</script>
调用 foo 函数,返回值是一个函数,由上文提到的执行上下文可知,每调用一个函数,就会创建一个执行上下文,如果声明了一个函数,没有调用,但函数本身依旧有一个作用域,作用域链由词法环境决定,只要调用了 fn 函数,即为依旧与 foo 的函数作用域构成作用域链,依旧可以访问 author 变量。
什么情况下会产生闭包?
- 为创建内部作用域而调用了一个包装函数;
- 包装函数的返回值必须至少包括一个对内部函数的引用
this 到底是什么
this 提供了一种更优雅的方式来隐式"传递"上下文对象
this 是在运行时进行绑定的,并不是在编写时绑定的,它的指向取决于函数调用时的各种 条件,确定 this 的指向和函数声明的位置没有任何关系,只取决于函数的调用方式
我把 this 指向分为两种情况:
- 能找到调用该函数的对象,即 this 指向该对象 (new 调用一个函数, this 指向实例)
- 不能找到调用该函数的对象,即 this 执行 window 这个兜底对象
html
<!-- index.html -->
// ...
<script>
console.log(this); // window
const blog = {
title: "再谈 this 指向和执行上下文",
author: function () {
console.log(this); // blog
},
};
blog.author();
const foo = function () {
console.log(this); // window
};
foo();
</script>
ES6 中的箭头函数
- 箭头函数没有自己的 this,只能继承上一个执行上下文的 this 指向;
- call/apply/bind 无法改变箭头函数中 this 的指向;
- 箭头函数不能作为构造函数使用,没有 arguments,prototype 属性;
- 箭头函数不能作 Generator 函数,不能使用 yield 关键字。
call、apply 和 bind
call、apply 和 bind 为了改变某个函数运行时的上下文而存在,换句话说,就是为了改变函数体内部 this 指向。
html
// index.html //...
<script>
const blog = {
title: "再谈 this 指向和执行上下文",
};
const getTitle = function () {
console.log("title:", this.title); // '再谈 this 指向和执行上下文'
console.log("arguments:", [arguments]); // [1, 2]
};
getTitle.call(blog, 1, 2);
</script>
//...