深入理解JS | 青训营笔记

2024-07-29 16:50:12 浏览数 (2)

1. JS 的基本概念

JavaScript(简称JS)是一门脚本语言,用于为网页添加交互效果动态功能。它由三个基本部分组成: ECMAScriptDOMBOM

  • ECMAScript 是 JavaScript 的核心语言,规范了 JavaScript 的基本语法、数据类型、流程控制等。
  • DOM(文档对象模型) 是针对 XML 文档的一个 API(应用程序编程接口),也适用于 HTML 文档。它把整个页面映射为一个多层节点结构,通过 DOM API 可以对页面上的任何元素进行操作
  • BOM(浏览器对象模型) 是指浏览器提供的一组 JavaScript API,通过它们可以获取和控制浏览器窗口和标签页等浏览器本身的功能。BOM 包含了很多对象,比如 window、location、navigator 等。

除了这些基本概念,JavaScript 还有一些特性和技术,比如闭包、原型链、异步编程等,这些都是深入学习 JavaScript 需要了解的内容。

1.1 动态 , 弱类型

JavaScript 的变量是动态的,因为在声明变量时不需要指定变量类型,变量的类型是在程序运行过程中自动推断出来的。也就是说,同一个变量在不同的时候可以存储不同类型的值。例如,一个变量可以先存储数字类型的值,然后再存储字符串类型的值。

JavaScript 的变量是弱类型的,是因为它们的类型可以随时发生改变,而且不需要进行类型转换就能进行运算。比如,在 JavaScript 中,一个变量可以存储数字类型的值,另一个变量可以存储字符串类型的值,但是它们仍然可以进行加法操作,并且会自动将字符串转换成数字再进行计算。

这种动态和弱类型的特性使得 JavaScript 在编写灵活性高的应用程序时非常有优势,但也存在一些问题。如果不小心让变量存储了错误类型的值,可能会导致程序出错或者产生意料之外的结果,因此要格外小心处理变量类型的问题。

1.2 变量提升

在使用var时,下面的代码不会报错. 这是因为这个关键字声明的变量会自动提升到函数作用域顶部

代码语言:javascript复制
function test(){
    console.log(age);
    var age = 13;
}
test() // undefined

之所以不会报错,是因为ECMAScript 运行的时候会把它看成等价于如下代码:

代码语言:javascript复制
function test1(){
    var age;
    console.log(age);
    age = 13
}
test1()

这就是所谓的变量提升,就是把所有变量声明都拉到函数作用域的顶部.

函数声明也会被提升到作用域顶部,如下所示:

代码语言:javascript复制
foo(); // 支持调用,输出 "bar"
function foo() {
  console.log("bar");
}

以上代码中,函数 foo 虽然在调用前定义了,但是在代码执行时已经被提升到了作用域顶部,因此可以正常调用并输出 "bar"。

let 和 const 不存在变量提升

2.JS是怎么执行的

2.1 讲解执行大概过程

JavaScript的执行分为两个阶段:编译和执行。

  1. 编译阶段

当JavaScript代码被加载时,JavaScript引擎会首先把它编译成字节码或机器码。编译阶段包括:

  • 词法分析:将代码按照语法规则分解成一个个单独的词汇单元(例如变量名、操作符、关键字等),这些词汇单元称为 Token。
  • 语法分析:将词法单元转换为 AST(Abstract Syntax Tree,抽象语法树)。AST是一种用于表示程序代码的树形数据结构,每个节点代表程序中的一个语言单元(如函数、语句、表达式等),可以方便地对代码进行分析和优化。
  • 代码生成:将AST转换为可执行代码(机器码或字节码),并将其存储在内存中以供后续执行。

在编译过程中,JavaScript 引擎会执行一些静态检查,如语法检查和类型检查。如果发现代码存在错误,编译阶段会立即停止并抛出错误信息。

  1. 执行阶段

编译阶段完成后,引擎开始执行代码。代码执行的具体流程是:

  • 作用域创建:在进入执行上下文时,JavaScript 引擎会创建一个新的作用域(即执行上下文),并将其加入到执行上下文栈中。
  • 变量提升:JavaScript 引擎会扫描当前作用域的所有变量和函数声明,并将它们提升到作用域的顶部,以便正确地处理变量访问和函数调用。这就是前面提到的“变量提升”机制。
  • 代码执行:JavaScript 引擎按照编译阶段生成的可执行代码进行执行,逐行解释执行代码,并根据当前状态来更新变量和对象的值。

在执行过程中,JavaScript引擎还会进行一些性能优化,如 JIT(Just-In-Time)编译、内联缓存等,以提高代码的运行速度和效率。

总体来说,JavaScript 的执行流程比较复杂,但是了解其基本原理和流程对于我们编写高效、健壮的 JavaScript 代码非常重要。

2.2 JIT(Just-In-Time)讲解

JIT(Just-In-Time)编译是一种动态编译技术,可以提高JavaScript代码的执行效率。在 JIT 编译中,JavaScript 引擎会将频繁执行的代码动态地编译成机器码并缓存起来,以便后续快速执行。

具体来说,JIT 编译分为三个阶段:

  1. 解释执行

当 JavaScript 代码被加载时,JavaScript 引擎首先对其进行解释执行,即逐行读取代码并执行相应的操作。由于解释执行需要频繁地解析和执行代码,因此效率较低。

  1. 编译阶段

在解释执行过程中,JavaScript 引擎会监测代码的执行频率,并对频繁执行的代码进行编译优化。编译过程包括 AST 解析、基础块分析、控制流分析、数据流分析等步骤,最终生成优化后的代码。这些代码被称为机器码。

  1. 优化阶段

优化阶段是 JIT 编译的核心。在每次执行频繁的代码时,JavaScript 引擎会对其进行优化,并重新生成机器码。这些优化包括内联缓存、函数内联、去除未使用的代码等。优化的目标是尽可能地减少不必要的计算、内存访问等操作,以提高代码的执行效率。

总体来说,JIT 编译技术可以大大提高 JavaScript 代码的执行效率。由于 JIT 编译需要根据代码的实际执行情况进行优化,因此它对于频繁执行的函数和循环特别有效。不过,在少量执行的代码中,JIT 编译可能会增加执行时间,因此需要按需使用。

3. JS的进阶知识点

3.1 闭包

JavaScript 中的闭包是一个非常强大的概念,很多开发者在学习 JavaScript 时都会遇到这个问题。本篇文章将介绍 JavaScript 中的闭包,同时提供一些例子来帮助您更好地理解。

什么是闭包?

首先,我们需要明白闭包是什么。简单的说,闭包是指可以访问独立变量的函数。具体来说,当一个内部函数引用了其外部函数的变量时,就形成了一个闭包。

下面的代码示例将更好地说明这一点:

代码语言:javascript复制
// 外部函数
function outerFunction() {
  const outerVariable = '123';
    
  // 内部函数
  function innerFunction() {
    console.log(outerVariable);
  }

  return innerFunction;
}

const myInnerFunction = outerFunction();
myInnerFunction(); // 输出:123

在上面的示例代码中,outerFunction 函数返回了 innerFunction 函数。这意味着 innerFunction 可以访问 outerFunction 中定义的变量。因此,当 myInnerFunction() 被调用时,它会输出 I am outside!

闭包的优点

闭包的最大优点是它们可以帮助我们隐藏或封装数据。这使得我们可以编写很多高效和安全的代码。其中一个优点是,闭包可以“记住”其父级函数中的数据,即使该函数已经退出并且不再存在。

下面是一个例子,它使用闭包来实现私有变量:

代码语言:javascript复制
function createCounter() {
  let count = 0;
  return function() {
    count  ;
    console.log(count);
  }
}

const counterA = createCounter();
counterA(); // 输出:1
counterA(); // 输出:2

const counterB = createCounter();
counterB(); // 输出:1

在这个例子中,createCounter 函数返回一个函数,该函数可以递增计数器并输出值。由于 count 变量只在 createCounter 函数内部定义,因此外部代码无法直接访问它。这使得我们可以安全地保护数据,并确保对其进行处理的代码仅在闭包范围内。

闭包的缺点

虽然闭包非常有用,但它们也有一些缺点。其中,最大的问题是它们可能会浪费内存。JavaScript 中的垃圾收集器将不会回收闭包中未使用的变量。如果你创建了很多这样的闭包,那么就可能导致内存泄漏和性能问题

下面是一个稍微复杂的例子,它演示了在 JavaScript 中使用闭包的一些缺点:

代码语言:javascript复制
function createBigObject() {
  const bigObject = new Array(70000).fill('x').join('');
  return function() {
      console.log(bigObject);
  }
}

const myBigObjectFunction = createBigObject();
myBigObjectFunction();

在这个例子中,createBigObject 函数返回了一个包含大量数据的闭包。每次调用 myBigObjectFunction 都会输出这个巨大的字符串。由于 JavaScript 不会回收未使用的闭包变量,因此可能会导致内存泄漏和性能问题

结论

在本文中,我们介绍了 JavaScript 中的闭包概念,并提供了几个示例来更好地理解它们。闭包是强大而有用的,但也需要小心使用,以避免出现内存泄漏和性能问题。在正确使用闭包的情况下,它们可以使代码更加灵活,可重用,并且能够实现很多高效、安全的功能

3.2 This

普通函数this 指向Window

this 在 JavaScript 中是一个非常重要的概念。它指的是函数所属的对象,具体取决于函数的调用方式。下面是一些常见的使用方式

1. 作为对象的方法 :

代码语言:javascript复制
const person = {
  name: 'Alice',
  sayHello() {
    console.log(`你好,我的名字是 ${this.name}`);
  }
};

person.sayHello(); // 输出 "你好,我的名字是 Alice"

在这个例子中,this 指的是 person 对象

2. 作为构造函数:

代码语言:javascript复制
function Person(name) {
  this.name = name;
}

const alice = new Person('Alice');
console.log(alice.name); // 输出 "Alice"

在这个例子中,this 指的是 Person 构造函数创建的新对象。

3. 使用 call 或 apply 显式地设置 this:

代码语言:javascript复制
function sayHello() {
  console.log(`你好,我的名字是 ${this.name}`);
}

const person1 = { name: 'Alice' };
const person2 = { name: 'Bob' };

sayHello.call(person1); // 输出 "你好,我的名字是 Alice"
sayHello.apply(person2); // 输出 "你好,我的名字是 Bob"

在这个例子中,call 和 apply 被用来将 this 分别设置为 person1 和 person2。

3.3 事件循环

当 JavaScript 运行时,它会将代码分为两类:同步代码和异步代码。同步代码是按顺序执行的,而异步代码则是在后台执行的,不会阻塞主线程。事件循环是 JavaScript 处理异步代码的机制

事件循环是一个不断运行的循环,它会检查消息队列中是否有待处理的消息。如果有,它会将消息从队列中取出并执行。如果没有,它会等待新的消息到达。

下面是一个简单的例子,演示了事件循环的工作原理:

代码语言:javascript复制
console.log('start');

setTimeout(() => {

  console.log('timeout');

}, 0);

Promise.resolve().then(() => {

  console.log('promise');

});

console.log('end');

输入内容:

start ==> end ==> promise ==> timeout

解释:

首先输出 start 和 end,然后遇到 Promise.resolve(),将其放入微任务队列中,继续执行,遇到 setTimeout,将其放入宏任务队列中,最后输出 promise。由于 setTimeout 的延时时间为 0,因此会在当前宏任务执行完毕后立即执行,输出 timeout。

结论:

微任务队列先于 宏任务队列先执行

4. 课程总结

0 人点赞