-
Notifications
You must be signed in to change notification settings - Fork 0
Description
执行上下文
执行上下文又称为执行环境,每当 js 引擎解析可执行代码时,就会进入一个执行环境。
本篇博客将在浏览器环境下讲述执行上下文。在浏览器环境中,执行上下文分为三种:
-
全局执行上下文
默认的、最基础的执行上下文。不在任何函数中的代码都位于全局执行上下文中。 -
函数执行上下文
每个函数都拥有自己的执行
上下文,但是只有在函数被调用的时候才会被创建。一个程序中可以存在任意数量的
函数执行上下文。每当一个新的执行上下文被创建,它都会按照特定的顺序执行一
系列步骤。 -
Eval 函数执行上下文
这种上下文一般不考虑
执行上下文是什么?
可以把执行上下文看成一个对象 {} 来理解,有三个重要属性,
- 变量对象(Variable Object, 简称 VO),存入声明的变量,也是一个对象 {}
- 函数的所有形参 (如果是函数执行上下文)
- 函数声明
- 变量声明
-
作用域链([[scope]]),扩连作用域链
作用域链是由当前环境与上层环境的一系列变量对象组成,它保证了对执行环境有权访问的所有变量和函数的有序访问
上文的作用域中讲到过函数的作用域在函数定义的时候就决定了,因为函数有一个内部属性 [[scope]],当函数创建
的时候,就会保存所有父变量对象到其中,当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,
就会从自己的 scope 中保存的父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对
象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。 -
确定 this 的指向 见 this 到底是什么
执行上下文的生命周期
创建阶段(生成变量对象, 建立作用域链, 确定 this 指向) => 执行阶段(变量赋值,函数引用、执行其它的代码) => 回收阶段
js 引擎如何管理执行上下文
<!-- 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 对象。
用伪代码解释上述过程:
// 解析过程从
[
{
AO: { this: window },
[[scope]]: {}
}
];
// 变为
[
{
AO: {},
this: window,
[[scope]]: {},
},
{
AO: {
this: window, // 函数执行时动态绑定
arguments: [], // 为类数组
author: "qinghuanI";
},
[[scope]]: {
AO: {
arguments: [], // 为类数组
author: "qinghuanI";
},
GO: {
window: {...},
author: "qinghuanI";
}
}
}
]这种标识符(变量)的解析过程,与函数的生命周期有关。函数的生命周期可以分为创建和激活(调用时)两个阶段。在函数创建时,函数的内部存在一个 [[scope]] 属性(内部属性),[[scope]]是所有变量对象的层级链。[[scope]]属性在函数创建时被存储,永远不变,直到函数被销毁。函数可以不被调用,但该属性一直存在。
作用域链的长度与函数嵌套有关,
this 指向与函数被调用时的所属的对象有关
变量对象与函数体内声明变量的关键字有关(var 与 const let 有区别)
闭包
当函数可以记住并访问所在的词法作用域时,即时函数是在当前词法作用域之外执行,
这时就产生了闭包
<!-- index.html -->
<script>
function foo() {
var author = "qinghuanI";
function bar() {
return author;
}
return bar;
}
var fn = foo(); // fn === function bar() {/* code */}
</script>调用 foo 函数,返回值是一个函数,由上文提到的执行上下文可知,每调用一个函数,就会创建一个
执行上下文,如果声明了一个函数,没有调用,但函数本身依旧有一个作用域,作用域链由词法环境
决定,只要调用了 fn 函数,即为依旧与 foo 的函数作用域构成作用域,依旧可以访问 author
变量。
什么情况下会产生闭包?
- 为创建内部作用域而调用了一个包装函数;
- 包装函数的返回值必须至少包括一个对内部函数的引用
this 到底是什么
this 提供了一种更优雅的方式来隐式"传递"上下文对象
this 是在运行时进行绑定的,并不是在编写时绑定的,它的指向取决于函数调用时的各种
条件,确定 this 的指向和函数声明的位置没有任何关系,只取决于函数的调用方式
我把 this 指向分为两种情况:
- 能找到调用该函数的对象,即 this 指向该对象 (new 调用一个函数, this 指向实例)
- 不能找到调用该函数的对象,即 this 指向 window 这个兜底对象
<!-- 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 指向。
- fn.call(thisArg, ...args)
第一个参数是指定的对象。将 fn 方法绑定到该对象上, args 是不定参数
// 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>
//...- fn.apply(thisArg, args)
第一个参数是指定的对象。将 fn 方法绑定到该对象上, args 是数组
// index.html // ...
<script>
const blog = {
title: "再谈 this 指向和执行上下文"
};
const getTitle = function() {
console.log("title:", this.title); // '再谈 this 指向和执行上下文'
console.log("arguments:", [arguments]); // [[1, 2]]
};
getTitle.apply(blog, [1, 2]);
</script>
// ...- fn.bind(thisArg, ...args)(...args)
bind()方法会返回执行上下文被改变的函数而不会立即执行,而前两者是直接执行该函数,args 是不定参数
// index.html // ...
<script>
const blog = {
title: "再谈 this 指向和执行上下文"
};
const getTitle = function() {
console.log("title:", this.title); // '再谈 this 指向和执行上下文'
console.log("arguments:", [arguments]); // [[1, 2]]
};
getTitle.bind(blog)(1, 2);
</script>
//...underscore 中的 bind 方法实现
// underscore.js v1.9.2
var executeBound = function(
sourceFunc,
boundFunc,
context,
callingContext,
args
) {
if (!(callingContext instanceof boundFunc))
return sourceFunc.apply(context, args);
var self = baseCreate(sourceFunc.prototype);
var result = sourceFunc.apply(self, args);
if (_.isObject(result)) return result;
return self;
};
_.bind = restArguments(function(func, context, args) {
if (!_.isFunction(func))
throw new TypeError("Bind must be called on a function");
var bound = restArguments(function(callArgs) {
return executeBound(func, bound, context, this, args.concat(callArgs));
});
return bound;
});