初识

JavaScript是一种由ECMAScript标准规范定义的编程语言。它的实现可以是开源的。

在浏览器中,JavaScript 是单线程执行的。这意味着在任何给定时间点,JavaScript 代码只能由一个线程执行。这个单线程通常称为主线程或 UI 线程。

NodeJS中JavaScript 也可以实现多线程编程,例如Worker Threads API 允许开发者在 Node.js 中创建独立的线程,这些线程可以执行 CPU 密集型任务、并行处理数据或执行其他需要并发执行的操作。这些线程是由操作系统调度和管理的。

但是,需要注意的是,Node.js 是单线程的事件驱动模型,主线程上的事件循环仍然是单线程的,因此在任何给定时刻只有一个事件在主线程上执行。而 Worker 线程的执行是在独立的 JavaScript 执行环境中进行的,它们可以并行执行代码,它们之间通过线程间的消息传递机制进行通信,但不会影响主线程的事件循环。

启动每个 Worker 线程都会消耗一定的系统资源,包括内存和 CPU 资源。每个 Worker 线程都有自己的 JavaScript 执行环境和相关的资源,因此在启动大量的 Worker 线程时需要考虑系统资源的限制。

引擎

JavaScript引擎(或称为JavaScript解释器),遵循ECMAScript规范,并将其转换为可在浏览器或其他环境中执行的机器码或字节码。

V8和SpiderMonkey引擎(开源)在执行JavaScript代码时,首先会对代码进行解析和编译,然后生成优化的中间表示(Intermediate Representation,IR),作用就是为了优化过程涉及诸如内联函数、去除冗余代码、变量的寄存器分配等技术,垃圾回收优化,最后将IR转换为本地机器代码,旨在提高JavaScript代码的执行效率。

参考:

V8引擎

引擎实现

通常通常所说的浏览器引擎就是指用于解释和执行JavaScript代码的软件组件。

  • JavaScript的主要开源实现之一是Mozilla基金会的SpiderMonkey引擎,它是Mozilla Firefox浏览器的JavaScript引擎
  • V8引擎是由Google开发和维护的,用于Chrome浏览器和Node.js等项目

ECMAScript

ECMAScript(简称ES)是一种脚本语言标准,用于定义JavaScript语言的语法和语义。它由Ecma国际(前身为欧洲计算机制造商协会)制定,并且定期进行更新和修订。

参考:

ECMAScript® 2025 Language Specification

基础

数据类型

基本数据类型

当将一个基本数据类型的变量赋值给另一个变量时,会直接将原始数据的值复制给新变量,而新变量与原始变量是完全独立的,修改其中一个变量的值不会影响另一个变量。

  • 包括数字(Number)
  • 字符串(String)
  • 布尔值(Boolean)
  • null
  • undefined
  • Symbol
1
2
3
4
5
javascriptCopy codelet a = 10;
let b = a; // b 获得了 a 的值,而不是 a 的引用
b = 20; // 修改 b 的值,a 的值不受影响
console.log(a); // 输出 10
console.log(b); // 输出 20

引用数据类型

当将一个引用类型的变量赋值给另一个变量时,只是复制了原始数据的引用地址,而不是复制实际数据。因此,新变量与原始变量共享同一个对象(或数组、函数等),修改其中一个变量的属性或元素,会影响到另一个变量。

  • 对象(Object)
  • 数组(Array)
  • 函数(Function)
  • 正则(RegExp)
  • 日期(Date)
1
2
3
4
5
let arr1 = [1, 2, 3];
let arr2 = arr1; // arr2 指向了 arr1 的引用地址,两者指向同一个数组
arr2.push(4); // 修改 arr2,同时也会影响 arr1
console.log(arr1); // 输出 [1, 2, 3, 4]
console.log(arr2); // 输出 [1, 2, 3, 4]

var、let、const

参考:

深入理解JS:var、let、const的异同

菜鸟教程:JavaScript let 和 const

MDN web docs:const

关键字 作用域 同一作用域重复声明 绑定全局对象
var 如果:函数内,则只作用于函数内,跟随函数生命周期
如果:函数外,则脚本内全局,跟随脚本生命周期
允许,将覆盖之前的定义。 绑定
let 所处的代码块{}内有效,外代码块与被包含代码块内外不互相影响,内的let只有效内,外let只有效外。 不允许 不绑定
(即:console.log(this.[let变量]) // undefined)
const(声明只读,声明必须初始化) 与let相同 不允许,不可被改写覆盖

需要注意的是,constlet 是在 ES6 (ECMAScript 2015) 中引入的,而 var 是旧版本 JavaScript 中使用的声明变量的方式。在现代的 JavaScript 开发中,推荐使用 constlet 来声明变量,根据具体的需求选择合适的关键字。const 适用于声明不需要重新赋值的常量或引用类型,而 let 适用于需要重新赋值的变量,同时提供了更好的作用域控制。var 在大多数情况下已经不推荐使用,但在特定情况下仍然可以使用,例如需要兼容旧版本 JavaScript 或在全局作用域中声明变量。

var 与 let作用域:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function varTest() {
var a = 1;

{
var a = 2; // 函数块中,同一个变量
console.log(a); // 2
}

console.log(a); // 2
}

function letTest() {
let a = 1;

{
let a = 2; // 代码块中,新的变量
console.log(a); // 2
}

console.log(a); // 1
}

varTest();
letTest();

var与let同一作用域:

  • var示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    var a = 1;
    var a = 2;

    console.log(a) // 2

    function test() {
    var a = 3;
    var a = 4;
    console.log(a) // 4
    }

    test()

  • let示例:

    1
    2
    3
    4
    if(false) {
    let a = 1;
    let a = 2; // SyntaxError: Identifier 'a' has already been declared
    }

let 与 const :

  • 相同

    • 二者都是块级作用域
    • 都不能和它所在作用域内的其他变量或函数拥有相同的名称
  • 异同

    • const声明的常量必须初始化,而let声明的变量不用

    • const 定义常量的值不能通过再赋值修改,也不能再次声明(数组、对象内部数据可修改)。而 let 定义的变量值可以修改。

1
2
3
4
5
let a;
const b = "constant"

a = "variable"
b = 'change' // TypeError: Assignment to constant variable

开发常用

JS数组合并

参考:

CSDN-IMJCW:JS数组合并(5种)

  • ES6 的语法,简单实用

    1
    2
    3
    4
    5
    6
    7
    8
    let arr = [1, 2]
    let arr2 = [3, 4]

    // ...可去除对象大括号、数组中括号
    arr = [...arr, ...arr2]

    console.log(arr)
    // [1, 2, 3, 4]
  • push 结合 ...[]

    arr.push.apply(arr, arr2) 的作用是将 arr2 数组中的元素依次添加到 arr 数组中。这里使用了 push 方法来向 arr 数组中添加元素,apply 方法将 arr2 数组中的元素作为参数传递给 push 方法,并使用 arr 数组作为 this 值绑定到 push 方法中。这样就可以将 arr2 数组中的元素添加到 arr 数组中了。

    1
    2
    3
    4
    5
    6
    7
    let arr = [1, 2]
    let arr2 = [3, 4]

    arr.push.apply(arr, arr2)

    console.log(arr)
    // [1, 2, 3, 4]
    1
    2
    3
    4
    5
    6
    7
    let arr = [1, 2]
    let arr2 = [3, 4]

    arr.push(...arr2)

    console.log(arr)
    // [1, 2, 3, 4]

${}在 Javascript 的字符串作用

参考:

爱码网:${}(美元符号和花括号)在 Javascript 中的字符串中是什么意思?

字符串插值

1
2
3
var foo = 'bar';
console.log(`Let's meet at the ${foo}`);
// Let's meet at the bar
1
2
3
4
const one = 1;
const two = 2;
const result = `One add two is ${one + two}`;
console.log(result); // output: One add two is 3

打印多行

1
2
3
4
console.log(`foo
bar`);
// foo
// bar

模板文字执行隐式类型转换

1
2
3
4
let fruits = ["mango","orange","pineapple","papaya"];

console.log(`My favourite fruits are ${fruits}`);
// My favourite fruits are mango,orange,pineapple,papaya

find()修改对象属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 var u = [
{
name: 'andy',
age: 18
}
, {
name: 'bob',
age: 19
}
]

var p = u.find(function (curr) {
return curr.name === 'bob'
})
p.age = 20

//也可以这样写
u.find( curr => curr.name === 'bob' )?.age = 20

find找到了数组中符合条件的对象后返回给p,直接更改p即可准确更改元素中的值。

some()

some方法同样用于检测是否有满足条件的元素,如果有,则不继续检索后面的元素,直接返回true,如果都不符合,则返回一个false。

1
2
3
4
5
6
7
let arr = [100,20,50,58,6,69,36,45,78,66,45]
// some
let result = arr.some(ele => ele === 45) //true
if (result) {
//do something...
};
console.log(result)

数组添加元素

splice()

它用于在数组中添加或删除元素,并返回被删除的元素。

1
2
3
const arr = [1, 2, 3, 4, 5];
arr.splice(2, 2, 'a', 'b'); // 从下标为 2 的位置开始删除 2 个元素,然后插入 'a', 'b'
console.log(arr); // [1, 2, 'a', 'b', 5]
1
array.splice(start[, deleteCount[, item1[, item2[, ...]]]])
  • start:指定添加或删除元素的起始位置,可以为负数,表示从数组末尾开始算起的位置。
  • deleteCount:可选参数,表示要删除的元素个数,如果省略该参数或为 0,则不删除任何元素。
  • item1, item2, ...:可选参数,表示要添加到数组中的元素。

过滤数组空元素

过滤去除数组的空元素,返回false会删除,即null或者undefine。

参考:

CSDN:js去除数组中的null空值

例:

1
2
3
4
5
6
var arr = ['2','3','',null,undefined,'7',' ','9'];
var r = arr.filter(function (s) {
return s && s.trim(); // 注:IE9(不包含IE9)以下的版本没有trim()方法
});
console.log(r);
// 将会返回非空元素:[‘2’,‘3’,‘7’,‘9’]

柯里化

在柯里化中,原始函数接受多个参数,但通过柯里化的转换,它可以被分解成一系列嵌套的函数,每个函数只接受一个参数。每次调用这些嵌套的函数之一,它们会部分应用之前的参数,并返回一个新的函数,等待传入下一个参数。JavaScript 中的函数可以通过手动编写或使用库(如 Lodash、Ramda)来进行柯里化。

它很灵活,可以避免重复传入参数,当你传入第一个参数的时候,该函数就已经具有了第一个参数的状态(闭包)。

举例

(state) => (gCode) => {} 是一个函数的箭头函数表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const setState = (state) => (gCode) => {
// 在这里可以执行具体的操作
console.log(state);
console.log(gCode);
};

const myState = 'Initial state';
const myGCode = 123;

const myFunction = setState(myState); // 调用外部函数,并传入 myState 参数
myFunction(myGCode); // 调用内部函数,并传入 myGCode 参数
// 输出:
// Initial state
// 123

Lodash

Lodash 的设计目标是提供高性能、模块化和易于使用的实用函数。它封装了许多常见的操作,帮助开发人员减少编写重复代码的工作量,提高开发效率。Lodash 的函数库被广泛应用于前端开发和后端开发,可以在浏览器环境和 Node.js 环境中使用。

参考:

Lodash:官方文档

  1. 数组操作:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
javascriptCopy codeimport { chunk, filter, map } from 'lodash';

const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

// 将数组分割成多个子数组
const chunkedArray = chunk(numbers, 3);
console.log(chunkedArray); // 输出 [[1, 2, 3], [4, 5, 6], [7, 8, 9], [10]]

// 过滤数组中的偶数
const filteredArray = filter(numbers, (num) => num % 2 === 0);
console.log(filteredArray); // 输出 [2, 4, 6, 8, 10]

// 将数组中的每个元素翻倍
const doubledArray = map(numbers, (num) => num * 2);
console.log(doubledArray); // 输出 [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
  1. 对象操作:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
javascriptCopy codeimport { pick, omit } from 'lodash';

const user = {
name: 'John',
age: 30,
email: 'john@example.com',
address: '123 Main St',
};

// 从对象中选择特定的属性
const selectedProps = pick(user, ['name', 'age']);
console.log(selectedProps); // 输出 { name: 'John', age: 30 }

// 从对象中排除特定的属性
const omittedProps = omit(user, ['email', 'address']);
console.log(omittedProps); // 输出 { name: 'John', age: 30 }
  1. 函数式编程:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
javascriptCopy codeimport { curry, flow } from 'lodash';

// 柯里化函数,将多参数函数转化为接受单个参数的函数
const add = (a, b, c) => a + b + c;
const curriedAdd = curry(add);

const increment = curriedAdd(1);
const result = increment(2)(3);
console.log(result); // 输出 6

// 函数组合,依次执行多个函数
const square = (x) => x * x;
const double = (x) => x * 2;

const squareAndDouble = flow(square, double);
const finalResult = squareAndDouble(5);
console.log(finalResult); // 输出 50

Promise

当 JavaScript 主线程在执行过程中遇到 async 函数或者包含 await 关键字的操作时,主线程会被暂停,等待 await 后面的 Promise 对象解析完成。这种暂停是异步的,意味着主线程会继续执行其他同步任务,直到遇到 await 处的异步操作完成为止。一旦 Promise 被解析完成,主线程会恢复执行 await 后面的代码。这种机制确保了异步操作在执行过程中不会阻塞主线程的运行。

同步:

  • 同步操作意味着任务按顺序执行,每个任务必须等待前一个任务完成后才能开始。
  • 在同步模式下,如果一个任务正在执行,程序将会等待(阻塞)直到任务完成,然后才继续执行下一个任务。
  • 这种方式简单直观,但可能导致程序效率低下,特别是在等待某些耗时操作(如文件读写、网络请求)时。

异步:

  • 异步操作允许任务在等待另一个任务完成时开始执行,不需要按顺序等待每个任务完成。
  • 在异步模式下,程序可以发起一个任务,然后立即转而执行其他任务,而不是等待第一个任务完成。
  • 异步通常通过回调函数、事件监听、Promise、async/await 等机制实现,提高了程序的效率和响应性。

传统异步和async/await异步

传统异步会导致两个主要的问题分别是:

  1. 代码的可读性:通过使用 async/await,代码的结构会更加清晰,易于理解,易于try...catch。传统嵌套在then()的方式复杂可读性差。
  2. 一般情况下传统方式只能返回Promise状态,不能返回Promise对象,特殊情况看最后传统方式返回Promise对象的代码。

参考:

博客园-jsgoshu:深入理解await与async

掘金-噜噜彩:promise的三种状态解析

简书-没了提心吊胆的稗子:async/await

传统方式异步

假设我们要实现一个异步操作,首先发起 HTTP 请求,然后再将请求结果传递给另一个异步操作处理,最后返回最终的处理结果。这个过程涉及到异步回调函数的嵌套,一般会使用 Promise 的 then 方法来处理。下面是一个简单的示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
function main() {
return httpRequest()
.then(response => {
const result = processResult(response)
return anotherAsyncOperation(result)
})
.then(finalResult => {
return finalResult
})
.catch(error => {
console.error(error)
})
}

function httpRequest() {
return new Promise((resolve, reject) => {
// 模拟一个 API 请求
setTimeout(() => {
if (Math.random() > 0.5) {
resolve('API 请求成功')
} else {
reject(new Error('API 请求失败'))
}
}, 1000)
})
}

function processResult(response) {
// 将请求结果处理成一个对象
return {
message: response,
data: {
foo: 'bar'
}
}
}

function anotherAsyncOperation(data) {
return new Promise((resolve, reject) => {
// 模拟另一个异步操作
setTimeout(() => {
resolve(`另一个异步操作完成,数据:${JSON.stringify(data)}`)
}, 1000)
})
}

可见以上情况 Promise 对象有可能会变成 reject 状态,如果不是 Promise 对象,无法利用 Promise 所带来的传播性质来有效地抛出错误。

async/await异步

  • async:当一个函数被标记为 async 时,它就没了提心吊胆的稗子会返回一个 Promise 对象。
  • await:如果有需要等待异步操作完成的代码,可以使用 await 关键字来等待结果。在等待的过程中,函数的执行会暂停,直到异步操作完成并返回结果后再继续执行。

而如果使用异步函数直接返回 Promise 值的方式,就可以避免传统的异步嵌套问题,代码更加清晰易读。下面是使用异步函数的示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
async function main() {
try {
const response = await httpRequest()
const result = processResult(response)
const finalResult = await anotherAsyncOperation(result)
return finalResult
} catch (error) {
console.error(error)
}
}

function httpRequest() {
return new Promise((resolve, reject) => {
// 模拟一个 API 请求
setTimeout(() => {
if (Math.random() > 0.5) {
resolve('API 请求成功')
} else {
reject(new Error('API 请求失败'))
}
}, 1000)
})
}

function processResult(response) {
// 将请求结果处理成一个对象
return {
message: response,
data: {
foo: 'bar'
}
}
}

function anotherAsyncOperation(data) {
return new Promise((resolve, reject) => {
// 模拟另一个异步操作
setTimeout(() => {
resolve(`另一个异步操作完成,数据:${JSON.stringify(data)}`)
}, 1000)
})
}

注意此段代码对比传统方式出现了try...catch

传统方式返回Promise对象

在很多 Node.js 应用中,仍然会使用这种传统的异步嵌套方式,并且会将异步操作封装成 Promise 对象,以便能够在 Promise 上继续使用 Promise 相关的 API,如 Promise chaining、try-catch 错误处理等。

下面是一个例子,展示了如何使用 Promise 封装一个传统异步嵌套的异步操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function traditionalAsyncOperation(callback) {
setTimeout(() => {
callback(null, '异步操作完成')
}, 1000)
}

function promisify(traditionalAsyncOperation) {
return function(...args) {
return new Promise((resolve, reject) => {
traditionalAsyncOperation(...args, (err, result) => {
if (err) {
return reject(err)
}
resolve(result)
})
})
}
}

const promisifiedOperation = promisify(traditionalAsyncOperation)
promisifiedOperation().then(result => {
console.log(result) // '异步操作完成'
})

好难啊。。。

JS类&自执行函数

先看一段TS编译后的JS代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Student {
fullName: string;
constructor(public firstName, public middleInitial, public lastName) {
this.fullName = firstName + " " + middleInitial + " " + lastName;
}
}

interface Person {
firstName: string;
lastName: string;
}

function greeter(person : Person) {
return "Hello, " + person.firstName + " " + person.lastName;
}

let user = new Student("Jane", "M.", "User");

document.body.innerHTML = greeter(user);

编译后js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var Student = /** @class */ (function () {
function Student(firstName, middleInitial, lastName) {
this.firstName = firstName;
this.middleInitial = middleInitial;
this.lastName = lastName;
this.fullName = firstName + " " + middleInitial + " " + lastName;
}
return Student;
}());
function greeter(person) {
return "Hello, " + person.firstName + " " + person.lastName;
}
var user = new Student("Jane", "M.", "User");
document.body.innerHTML = greeter(user);

其中this.的意思是,哪行代码newStudent,此行代码new的这个对象中就会被赋值成员变量firstName、middleInitial、lastName、fullName。

引入.html

1
2
3
4
5
6
7
<!DOCTYPE html>
<html>
<head><title>TypeScript Greeter</title></head>
<body>
<script src="test.js"></script>
</body>
</html>

执行结果是Hello, Jane User

立即调用函数表达式

立即调用函数表达式(Immediately Invoked Function Expression,IIFE)

1
2
3
var Student = (function () {
/** 立即执行的代码块 */
}());

在这个 IIFE 内部,我们定义了一个名为 Student 的类作为返回值。这个类定义完成后立即返回,返回的值将会被赋给 var Student 变量。这种方式可以让我们在不污染全局作用域的情况下使用这个 Student 类。