JavaScript执行机制之执行上下文

本文深入探讨JavaScript执行上下文的概念,包括全局与函数执行上下文的特性,以及变量与词法环境的区别。通过实例讲解作用域、this绑定及变量提升机制。

title: js执行上下文

前言:JavaScript没有块级作用域

开始之前,必须先声明,JavaScript 没有块级作用域,只有执行上下文。这也是这篇文章的输出原因之一,讲清楚作用域和执行上下文的关系。

请不要简单的把一个 {} 当成一个块级作用域,这是错误的!

多数人的入门语言是 C语言, 它可以在 for循环 里定义变量不影响外部环境。 但在 JavaScript 中却不可以。

JavaScript 中只有全局作用域和函数作用域。而执行上下文,就是函数作用域中的 JavaScript 代码的运行过程。

JavaScript 代码的执行过程分为两个阶段:

  • 编译阶段:由编译器完成,将代码翻译成可执行代码
  • 执行阶段:由引擎完成,主要任务是执行可执行代码

其中可执行代码主要有三种类型:全局对象,局部对象,Eval对象。(函数即对象)

Eval 函数对象存在安全性问题,不建议使用

一、对象和执行上下文的关系?

执行上下文是 javascript 代码被解析和执行时,所在环境的抽象概念。

执行上下文,是对象和运行环境绑定看待时,才出现的概念。并不独立存在。

二、javascript 中“执行上下文”的类型?

  • 全局执行上下文:只有一个,也就是浏览器对象(即window对象),this指向的就是这个全局对象。
  • 函数执行上下文:有无数个,只有在函数被调用时才会被创建,每次调用函数都会创建一个新的执行上下文。
  • Eval函数执行上下文jseval函数执行其内部的代码会创建属于自己的执行上下文, 很少用而且不建议使用。

三、执行上下文的特点

  1. 单线程,只在主线程上运行;
  2. 同步执行,从上向下按顺序执行;
  3. 全局上下文只有一个,也就是window对象;
  4. 函数执行上下文没有限制;
  5. 函数每调用一次就会产生一个新的执行上下文环境。

五、执行上下文的生命周期

  1. 创建阶段
  2. 执行阶段
  3. 销毁阶段

创建阶段

  1. 创建变量对象
  2. 建立作用域链
  3. 确定this的指向

执行阶段

  1. 完成变量赋值
  2. 函数引用
  3. 执行其他代码

销毁阶段

执行完毕,推出执行栈,等待 V8 垃圾回收(释放内存)。

图片总结

执行上下文的生命周期

六、变量环境与词法环境(含题)

变量环境与词法环境同属于执行上下文的知识点,但由于个人理解不够透彻,这里只做简要介绍!

根据ECMA-262规范,我们可以暂且简单进行归纳为

词法环境:letconst,它们创建的对象与当前词法环境上下文绑定,不会进行变量提升。

变量环境:var 创建的对象,会进行变量提升。同时,函数提升的优先级高于变量提升。

一张图方便你理解 🤔

executionContext1

This 绑定

通过上面的介绍我们知道实际开发主要用到两种执行上下文为全局函数, 那么绑定this在这两种上下文中也不同.

  • 全局执行上下文中, this指的就是全局对象, 浏览器环境指向window对象, nodejs 中因为 commonjs 模块化规范,指向当前文件的module对象.
  • 函数执行上下文较为复杂, this的值取决于函数的调用方式. 具体有: 默认绑定、隐式绑定、显式绑定、new绑定、箭头函数.

词法环境

结合上图,现在我们已经知道, 词法环境是由两个部分组成的:

  1. 环境记录: 存储变量和函数声明的实际位置;
  2. 对外部环境的引用: 用于访问其外部词法环境.

同样的, 词法环境 也主要有两种类型:

  1. 全局环境: 拥有一个全局对象(window对象)及其关联的所有属性和方法(比如数组的方法splice、concat等), 同时也包含了用户自定义的全局变量. 但是全局环境中没有外部环境的引用, 也就是外部环境引用为null.
  2. 函数环境: 用户在函数中自定义的变量和函数存储在环境记录中, 包含了arguments对象. 而对外部环境的引用可以是全局环境, 也可以是另一个函数环境(比如一个函数中包含了另一个函数).
来一段词法绑定代码🍊
console.log(a) // undefined
var a = 0
// 下面两行注释掉,因为报错就不会继续运行,这里只是说明没有声明就不会变量提升,也不会绑定词法环境
// console.log(b) // Uncaught ReferenceError: b is not defined
// b = 1
console.log(c)  // Cannot access 'c' before initialization 绑定了词法位置,不能再 let 之前调用
let c = 1

变量环境

变量环境 继承了词法环境, 因此它具有上面定义的词法环境的所有属性.

而在实际工作中使用的 ES6 代码里,我们可以理解为,词法 环境和 变量 环境的区别在于 词法(letconst)与环境绑定变量( var )不绑定环境

接下来,我们看看关于 JavaScript 入门级别的面试题,函数提升和变量提升。(为什么是入门级?因为这是 JavaScript 运行机制的第一个前置知识点)

变量声明提升🍓

日常使用:

var a = 10;

实际执行过程:

var a;
a = 10;

因此有了下面这段代码的执行结果:

console.log(a); // 声明,先给一个默认值undefined;
var a = 10; // 赋值,对变量a赋值了10
console.log(a); // 10

上面的代码👆在第一行中并不会报错Uncaught ReferenceError: a is not defined, 是因为声明提升, js引擎读取整个执行上下文后,先给了 a 一个默认值 undefined.

这就是最简单的变量声明提升.

函数声明提升🍉

定义函数有两种方法:

  • 构造函数: function foo () {};
  • 函数表达式: var foo = function () {}.

函数表达式的声明方式更像是给一个变量foo赋值一个匿名函数.

而两者的区别,暂时不用深究,超纲了。

console.log(f1) // function f1(){}
function f1() {} // 函数声明
console.log(f2) // undefined
var f2 = function() {} // 函数表达式
声明优先级: 函数大于变量

这里不写代码,也请忘掉网上的一些不知道多少手的知识。

根据函数声明提升的示例代码,我们问自己一个问题:假如存在一个变量和函数同名,会发生什么?

已知变量声明提升后,变量会预设为undefined。函数声明提升后,还是函数本身。

为什么变量会预设?为什么它们不能同名?

这里不写答案,有经验的同学会想到堆内存。基础稍差的萌新们,可能不知道堆内存是什么,但带着问题,可以在下一章通过更深入了解 JavaScript 的底层知识,得到解答。

总之,在了解JavaScript引擎会预先解析代码的前提下,我们通过思考执行过程,可以发现声明提升的实际过程如下:

在函数作用域的创建阶段,整个上下文会被读取,然后对于声明,functionvar 会自动提升到执行上下文的顶部。

而对于词法环境绑定的声明,会保持未初始化(在letconst )。

所以这就是为什么可以在声明之前访问 var 定义的变量(尽管是 undefined ),但如果在声明之前访问 letconst 定义的变量就会提示引用错误的原因。这就是所谓的变量提升。

总结

JavaScript没有块级作用域(再次强调)

词法环境:letconst,它们创建的对象与当前词法环境上下文绑定,不会进行变量提升。

变量环境:var 创建的对象和 function 创建的函数,会进行变量/函数提升。

参考文章:

霖呆呆的blog

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值