从底层出发深入理解 JavaScript

384 阅读6分钟

引言

JavaScript 是一种高级的、解释型的编程语言,主要用于 Web 开发,它允许开发者在网页中添加动态功能,例如响应用户交互、操作 DOM(文档对象模型)、发送异步请求等。随着 Web 技术的发展,JavaScript 的应用范围已经远远超出了浏览器,它现在可以用于服务器端开发(例如 Node.js)、移动应用开发(例如 React Native)、桌面应用开发(例如 Electron)等。JavaScript 是世界上最流行的编程语言之一,它的社区非常活跃,有大量的开源库和框架可供使用,这使得开发者可以更加高效地构建各种应用程序。

JavaScript的特点

JavaScript 是一门弱语言也是一种脚本语言,这意味着它不需要编译,可以直接在浏览器中运行。它具有面向对象、函数式和命令式编程的特性,支持多种编程范式。JavaScript 的语法与 C 语言和 Java 语言类似,但它也有自己独特的特性,例如动态类型、弱类型、基于原型的继承等。

案例引入

首先看一段代码,让我们思考一下它的运行结果是什么?

console.log(name)
var name="wql"

输出的是wql?

答案揭晓

屏幕截图 2024-11-21 083652.png

微信图片_20241121084045.jpg

undefined?

好好好,让我们来看看是为什么! 首先我们要理解一个概念,JavaScript没有独立的编译过程,但其实JavaScript的执行还是分编译和执行两个步骤。

在js运行之前有个短暂的编译过程 那么就是说 var name="wql" 分为 var name 和 name="wql"两个阶段

所以在代码运行的编译过程中,定义了一个变量name,然后将它变量提升,提升到了当前作用域的最上方,但是在执行阶段打印name的语句在name赋值语句的前面,所以这段代码的结果就是undefined。

微信图片_20241121091501.jpg

嗯?等下!变量提升? 这是什么意思?

变量提升是指 JavaScript 引擎在代码执行之前,会先解析变量和函数声明,并将它们提升到其所在作用域的顶部。这意味着,无论你在代码的哪个位置声明变量或函数,它们都会被视为在作用域的顶部被声明。

但提到变量提升就不得不说一下执行上下文了

执行上下文是 JavaScript 引擎为代码执行创建的一个抽象环境,它包含了代码执行所需的所有信息,如变量环境、作用域链和 this 绑定。每个执行上下文都有三个阶段:创建阶段、执行阶段和销毁阶段。 执行上下文可以分为三种类型:

  • 全局执行上下文:每个 JavaScript 程序都有一个全局执行上下文,它是程序的入口点。
  • 函数执行上下文:每当一个函数被调用时,都会创建一个函数执行上下文。
  • Eval 执行上下文:当 eval 函数被调用时,会创建一个 eval 执行上下文。由于 eval 的使用不推荐,本文将重点讨论全局执行上下文和函数执行上下文。

而变量提升就是发生在执行上下文的创建阶段,在创建阶段,JavaScript 引擎会解析代码中的变量和函数声明,并将它们添加到变量对象中。这一过程实际上是变量提升的具体实现。

function foo() {
  var a = 1;
  function b() {}
}

foo();

foo 函数的创建阶段,变量对象如下:

javascript
深色版本
VO = {
  arguments: { 0: undefined, length: 0 },
  a: undefined,
  b: function b() {}
}
2.1.2 作用域链(Scope Chain)

作用域链是一个指向变量对象的指针列表,用于查找变量。每个执行上下文都有自己的作用域链,它包含了当前执行上下文的变量对象及其外部执行上下文的变量对象。

javascript
深色版本
function outer() {
  var a = 1;

  function inner() {
    var b = 2;
    console.log(a); // 1
  }

  inner();
}

outer();

inner 函数的创建阶段,作用域链如下:

javascript
深色版本
Scope Chain = [
  AO of inner, // { arguments: {}, b: undefined }
  AO of outer, // { arguments: {}, a: 1 }
  Global Object // { window: ..., global: ... }
]
2.1.3 this 绑定

this 的值在创建阶段确定,取决于函数的调用方式。常见的 this 绑定规则包括:

  • 全局上下文:在非严格模式下,this 指向全局对象(如 window),在严格模式下,this 为 undefined
  • 函数上下文:普通函数调用时,this 指向全局对象;作为对象方法调用时,this 指向该对象;使用 new 关键字调用时,this 指向新创建的对象;使用 callapply 或 bind 调用时,this 指向指定的对象。
javascript
深色版本
function foo() {
  console.log(this);
}

foo(); // window (非严格模式) 或 undefined (严格模式)

const obj = {
  method: function() {
    console.log(this);
  }
};

obj.method(); // obj

new foo(); // 新创建的对象
2.2 执行阶段

在执行阶段,JavaScript 引擎会逐行执行代码,解析变量和函数声明,并执行相应的操作。

javascript
深色版本
function foo() {
  var a = 1;
  function b() {}
  console.log(a); // 1
}

foo();

foo 函数的执行阶段,变量对象如下:

javascript
深色版本
AO = {
  arguments: { 0: undefined, length: 0 },
  a: 1,
  b: function b() {}
}
2.3 销毁阶段

当代码执行完毕后,执行上下文会被销毁,释放相关资源。销毁阶段包括:

  • 清除变量对象:删除变量对象中的变量和函数声明。
  • 断开作用域链:断开作用域链中的指针,释放内存。
3. 调用栈(Call Stack)

调用栈是 JavaScript 引擎管理执行上下文的一种数据结构,它遵循后进先出(LIFO)原则。每当一个函数被调用时,其执行上下文会被压入调用栈;当函数执行完毕后,其执行上下文会被弹出调用栈。

javascript
深色版本
function foo() {
  console.log("foo");
  bar();
}

function bar() {
  console.log("bar");
}

foo();

在上述代码中,调用栈的变化如下:

  1. 全局执行上下文被压入调用栈。
  2. foo 函数被调用,其执行上下文被压入调用栈。
  3. bar 函数被调用,其执行上下文被压入调用栈。
  4. bar 函数执行完毕,其执行上下文被弹出调用栈。
  5. foo 函数执行完毕,其执行上下文被弹出调用栈。
  6. 全局执行上下文执行完毕,其执行上下文被弹出调用栈。

补充

在最新的ES6 引入了 letconst 关键字,用于在块级作用域中声明变量

  • et 和 const 声明let 和 const 声明的变量不会被提升到其所在作用域的顶部,而是在声明之前处于暂时性死区(Temporal Dead Zone, TDZ)。
console.log(a); // undefined

var a = 1;

console.log(b); // ReferenceError: Cannot access 'b' before initialization

let b = 2;

console.log(c); // ReferenceError: Cannot access 'c' before initialization

const c = 3;

总结

执行上下文是 JavaScript 代码执行的基础,理解其生命周期和工作原理对于编写高效、可维护的代码至关重要。通过本文的介绍,我们了解了执行上下文的创建、执行和销毁阶段,以及调用栈和作用域的概念

微信图片_20241121084105.jpg