本节课继续学习javascript进阶内容!


本文目标

通过本文的学习,我们将:

  • 了解现代javascript的常用简化操作,了解新的方法和数据结构;
  • 学习函数、数组的进阶知识,包括一等公民、闭包、数组转换方法等难点;
  • 熟悉数字、日期、计时器的使用方法。

现代Js

拆包(Destructuring)

什么是拆包?拆包是将数组或对象中的值解构并赋值到独立的变量中,使代码更简洁和易读。

先来看一个直观易懂的例子:

1
2
3
4
5
6
7
8
9
10
11
//数组拆包
const numbers = [1, 2, 3];
const [first, second, third] = numbers;
console.log(first); // 输出:1
console.log(second); // 输出:2
console.log(third); // 输出:3
//对象拆包
const person = { name: "Alice", age: 25 };
const { name, age } = person;
console.log(name); // 输出:Alice
console.log(age); // 输出:25

等号前面是括号,里面对应要瓜分原对象的元素的名称。反正就是有这么个倒反天罡的意思在这儿就对了。

当然,用拆包还有一些很花的玩法,比如:

跳过元素

1
2
let [main, , secondary] = [1,2,3];
console.log(main, secondary); //1 2

交换变量

1
2
3
// ......
[main, secondary] = [secondary, main];
console.log(main, secondary);

嵌套解构

1
2
3
4
const nested = [2, 4, [5, 6]];
const [i, , [j, k]] = nested;
console.log(i, j, k);
// 输出: 2 5 6

最最常见的应用场景就是:当一个函数的返回对象是一个键值对或者多个值时,用拆包去处理它会大大简化代码流程!

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const restaurant = {
name: 'Italiano Delights',
openingHours: {
mon: { open: 10, close: 22 },
fri: { open: 11, close: 23 },
},
categories: ['Italian', 'Pizzeria', 'Vegetarian', 'Organic'],
};

// 提取对象的属性
const { name, openingHours, categories } = restaurant;
console.log(name, openingHours, categories);
// 输出: Italiano Delights { mon: {...}, fri: {...} } [ 'Italian', 'Pizzeria', 'Vegetarian', 'Organic' ]

*扩散算子与剩余算子(…)

没错,这两货的语法都是使用 ..., 但是它们出现的位置和功能截然不同:

  • 扩展运算符(spread) 用于展开数组或对象。

应用场景:合并数组或对象

1
2
3
4
5
6
const user = { name: 'John', age: 28 };
const additionalInfo = { email: 'john@example.com', city: 'New York' };

const fullUserInfo = { ...user, ...additionalInfo };
console.log(fullUserInfo);
// 输出: { name: 'John', age: 28, email: 'john@example.com', city: 'New York' }
  • 剩余参数(rest) 用于将多个独立的参数组合为数组。

应用场景:在不确定形参数量时使用效果极佳

1
2
3
4
5
6
7
8
function calculateTotalPrice(...prices) {
return prices.reduce((total, price) => total + price, 0);
}

console.log(calculateTotalPrice(10, 20, 30));
// 输出: 60
console.log(calculateTotalPrice(15, 25));
// 输出: 40

虽然它们只占了很短的篇幅,但是实际开发中它们几乎到处都在。所以,罚你再读一遍!😈😈😈

运算符缩写

这一部分你会看见很多老朋友,咱们会一笔带过;当然也有新同学要重点介绍,比如空值合并运算符(??),还有||=&&= 之类的缝合怪。

短路运算符

||

|| 从左到右依次检查每个值并返回第一个为真的值。

1
2
3
console.log(3 || 'Jonas'); // 输出: 3
console.log('' || 'Jonas'); // 输出: 'Jonas'
console.log(undefined || 0 || '' || 'Hello' || 23 || null); // 输出: 'Hello'

&&

&& 从左到右依次检查每个值并返回第一个为假的值。

1
2
3
console.log(0 && 'Jonas'); // 输出: 0
console.log(7 && 'Jonas'); // 输出: 'Jonas'
console.log('Hello' && 23 && null && 'jonas'); // 输出: null

空值合并运算符(??

空值合并运算符(??)用于判断值是否为 nullundefined,如果是,则返回右侧的值。

它与 || 的不同在于: || 会将 0 或空字符串 '' 视为假值,而 ?? 不会。

示例:

1
2
3
4
5
6
restaurant.numGuests = 0;
const guests = restaurant.numGuests || 10;
console.log(guests); // 输出: 10

const guestCorrect = restaurant.numGuests ?? 10;
console.log(guestCorrect); // 输出: 0

在这段代码中,numGuests 的值为 0。使用 || 运算符会将 0 当作假值返回 10,而使用 ?? 运算符则会保留 0 的值,因为 0 不属于 nullundefined

逻辑赋值运算符

其实就是条件判断+赋值呗。。。😅

||= 运算符(或赋值)

||= 运算符用于在左侧值为假(false0nullundefined'')时,将右侧值赋给左侧变量。

示例:

1
2
3
4
5
6
7
8
const rest1 = { name: 'Capri', numGuests: 0 };
const rest2 = { name: 'La Piazza' };

rest1.numGuests ||= 10;
rest2.numGuests ||= 10;

console.log(rest1.numGuests); // 输出: 10
console.log(rest2.numGuests); // 输出: 10

&&= 运算符(与赋值)

&&= 运算符用于在左侧值为真时,将右侧值赋给左侧变量。这常用于仅在属性已存在的情况下进行更新。

示例:

1
2
3
4
5
6
7
8
const rest1 = { name: 'Capri' };
const rest2 = { name: 'La Piazza', owner: 'Giovanni Rossi' };

rest1.owner &&= '<ANONYMOUS>';
rest2.owner &&= '<ANONYMOUS>';

console.log(rest1.owner); // 输出: undefined
console.log(rest2.owner); // 输出: '<ANONYMOUS>'

??= 运算符(空值赋值)

??= 运算符用于在左侧值为 nullundefined 时,将右侧值赋给左侧变量。

示例:

1
2
3
4
5
6
7
8
const rest1 = { name: 'Capri', numGuests: 0 };
const rest2 = { name: 'La Piazza' };

rest1.numGuests ??= 10;
rest2.numGuests ??= 10;

console.log(rest1.numGuests); // 输出: 0
console.log(rest2.numGuests); // 输出: 10

for-of循环

在某些情况下,我们不仅需要数组的每一项内容,还需要每一项的索引。通过 for-of 循环和数组的 entries() 方法,我们可以同时获取到数组的索引和元素:

1
2
3
4
5
6
7
8
9
10
const menu = ['Pasta', 'Pizza', 'Salad', 'Soup'];
//entries() 方法返回一个新的 Array 对象,其中包含数组每一项的键值对[["foo", "arr"],[...]]
for (const [i, el] of menu.entries()) {
console.log(`${i + 1}: ${el}`);
}
// 输出:
// 1: Pasta
// 2: Pizza
// 3: Salad
// 4: Soup

由于entries方法已经创造了新的数组且为保持原数组完整,通常要加上const

又比如说,我们可以使用 for-of 循环结合 spread 运算符,将两个数组合并并进行遍历:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const restaurant = {
starterMenu: ['Bruschetta', 'Garlic Bread'],
mainMenu: ['Spaghetti', 'Margherita Pizza'],
};

const menu = [...restaurant.starterMenu, ...restaurant.mainMenu];

for (const item of menu) {
console.log(item);
}
// 输出:
// Bruschetta
// Garlic Bread
// Spaghetti
// Margherita Pizza

可选链(?.)

?.是 JavaScript 中一个非常实用的运算符,用于安全地访问对象的嵌套属性、方法和数组元素。它允许在访问对象属性时,不会因为属性不存在而引发错误。这对处理复杂或不确定结构的对象非常有用。

object.property?.subProperty,它表达的意思是:

  • 如果前面的属性不存在,它将返回 undefined而不会报错;
  • 如果存在,继续执行subProperty
1
2
3
4
5
6
7
8
9
10
11
const restaurant = {
name: 'Italian Bistro',
openingHours: {
thu: { open: 12, close: 22 },
fri: { open: 11, close: 23 },
},
};

// 使用可选链
console.log(restaurant.openingHours.thu?.open); // 12
console.log(restaurant.openingHours?.mon?.open); // undefined

配合上前面的空值合并运算符(??)和循环,还能玩的更花:

示例:检查每天的营业时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const days = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'];
//这里就是在上一篇中提到的:如果变量要作为对象的属性查询,则只能用括号表示法
for (const day of days) {
const open = restaurant.openingHours[day]?.open ?? 'closed';
console.log(`On ${day}, we open at ${open}`);
}
// 输出:
// On mon, we open at closed
// On tue, we open at closed
// On wed, we open at closed
// On thu, we open at 12
// On fri, we open at 11
// On sat, we open at closed
// On sun, we open at closed

Set和Map是现代Js中新出现的两种内置数据类型。它们都可迭代,即都可以在循环中被loop。学习它们时,请熟悉它们的功能并了解其和Array、Object的区别。

Set

Set 是一个无重复值的数据结构,一般来说的用法都是用来对数组去重的。

当创造的Set中有重复值时,Set会自动忽略:

1
2
const ordersSet = new Set(['Pasta', 'Pizza', 'Pizza', 'Risotto', 'Pasta']);
console.log(ordersSet); // Set(3) { 'Pasta', 'Pizza', 'Risotto' }

常用操作:

  • **size**:获取 Set 的大小。

  • **has**:检查某个值是否存在于 Set 中。

  • **add**:向 Set 中添加一个新元素。

  • **delete**:删除某个元素。

1
2
3
4
5
6
console.log(ordersSet.size); // 3
console.log(ordersSet.has('Pizza')); // true
console.log(ordersSet.has('Bread')); // false
ordersSet.add('Garlic Bread');
ordersSet.delete('Risotto');
console.log(ordersSet); // Set(3) { 'Pasta', 'Pizza', 'Garlic Bread' }

Set的运用其实并没有很多,反而是Map确确实实弥补了对象的键值只能是String类型的不足,因此出场率更高。

Map

Map 是键值对的集合,其中键可以是任意数据类型。相较于对象,Map 的键不限于字符串和符号,可以是函数、对象、数组等。

1
2
3
4
5
const rest = new Map();
rest.set('name', 'Classico Italiano');
rest.set(1, 'Firenze, Italy');
rest.set(2, 'Lisbon, Portugal');
console.log(rest); // Map(3) { 'name' => 'Classico Italiano', 1 => 'Firenze, Italy', 2 => 'Lisbon, Portugal' }

链式操作

我们可以连续使用 set 方法实现链式操作。

1
2
3
4
5
6
rest
.set('categories', ['Italian', 'Pizzeria', 'Vegetarian', 'Organic'])
.set('open', 11)
.set('close', 23)
.set(true, 'We are open :D')
.set(false, 'We are closed :(');

常用操作

  • set:设置键值对。

  • get:根据键取值。

  • **has**:检查 Map 中是否存在指定的键。

  • **delete**:删除指定键的键值对。

  • **size**:获取 Map 的大小。

1
2
3
4
5
console.log(rest.get('name')); // Classico Italiano
console.log(rest.get(true)); // We are open :D

const time = 8;
console.log(rest.get(time > rest.get('open') && time < rest.get('close'))); // We are closed :(

*转换对象和 Map

可以将对象转为 Map,或将 Map 转为数组。

1
2
3
4
5
6
7
8
// 对象转 Map
const hoursMap = new Map(Object.entries(restaurant.openingHours));
console.log(hoursMap);

// Map 转数组
console.log([...question]); // 转为数组形式
console.log([...question.keys()]); // 获取所有键
console.log([...question.values()]); // 获取所有值

总结:数据类型对比

高效处理String

这里将要介绍五花八门的String方法。对此,你只需要略微了解,然后在恰当的时机查询或使用它们即可,不用死记硬背(❁´◡`❁)

唯一需要警觉的是:请时刻注意这个方法是不是改变了原数组,还是新创建了一个处理后的数组?

为了让字符串操作更高效,JavaScript提供了一系列内置方法,用于分割、拼接、格式化、替换和查找等操作。以下是常用字符串操作方法及其应用示例。

基础字符串操作

获取字符和长度

使用字符串索引可以获取特定位置的字符,length 属性则返回字符串长度。

1
2
3
4
5
6
const airline = 'TAP Air Portugal';
const plane = 'A320';

console.log(plane[0]); // A
console.log(airline.length); // 16
console.log('B737'.length); // 4

查找字符位置

indexOflastIndexOf 方法用于查找字符或子字符串的起始位置。

1
2
3
console.log(airline.indexOf('r')); // 6
console.log(airline.lastIndexOf('r')); // 10
console.log(airline.indexOf('Portugal')); // 8

截取子字符串

使用 slice 方法截取字符串的一部分,支持负数索引。

1
2
3
4
console.log(airline.slice(4)); // Air Portugal
console.log(airline.slice(4, 7)); // Air
console.log(airline.slice(-2)); // al
console.log(airline.slice(1, -1)); // AP Air Portuga

示例:检查座位是否为中间座位

1
2
3
4
5
6
const checkMiddleSeat = function (seat) {
const s = seat.slice(-1);
if (s === 'B' || s === 'E') console.log('You got the middle seat 😬');
else console.log('You got lucky 😎');
};
checkMiddleSeat('11B'); // You got the middle seat 😬

字符串格式化

大小写转换

toLowerCasetoUpperCase 可用来进行大小写转换。

1
2
3
4
const passenger = 'jOnAS';
const passengerLower = passenger.toLowerCase();
const passengerCorrect = passengerLower[0].toUpperCase() + passengerLower.slice(1);
console.log(passengerCorrect); // Jonas

去除空格

trim 去除字符串首尾的空格,非常适合处理用户输入。

1
2
3
const loginEmail = '  Hello@Jonas.Io \n';
const normalizedEmail = loginEmail.toLowerCase().trim();
console.log(normalizedEmail); // hello@jonas.io

查找和替换

替换字符

使用 replace 和正则表达式替换字符串中的字符。

1
2
3
const priceGB = '288,97£';
const priceUS = priceGB.replace('£', '$').replace(',', '.');
console.log(priceUS); // 288.97$

示例:替换所有出现的子字符串

1
2
const announcement = 'All passengers come to boarding door 23. Boarding door 23!';
console.log(announcement.replace(/door/g, 'gate')); // 替换所有 'door'

判断字符串包含

includesstartsWithendsWith 可以检查字符串中是否包含特定的子字符串。

1
2
3
4
const plane = 'Airbus A320neo';
console.log(plane.includes('A320')); // true
console.log(plane.startsWith('Airb')); // true
console.log(plane.endsWith('neo')); // true

分割与拼接

分割字符串

split 方法可以将字符串分割成数组。

1
console.log('a+very+nice+string'.split('+')); // ['a', 'very', 'nice', 'string']

拼接字符串

join 方法将数组中的元素拼接成字符串。

1
2
3
const [firstName, lastName] = 'Jonas Schmedtmann'.split(' ');
const newName = ['Mr.', firstName, lastName.toUpperCase()].join(' ');
console.log(newName); // Mr. Jonas SCHMEDTMANN

示例:将每个单词首字母大写

1
2
3
4
5
6
const capitalizeName = function (name) {
const names = name.split(' ');
const namesUpper = names.map(n => n[0].toUpperCase() + n.slice(1));
console.log(namesUpper.join(' '));
};
capitalizeName('jessica ann smith davis'); // Jessica Ann Smith Davis

填充和重复

填充字符串

padStartpadEnd 用于在字符串两侧填充字符。

1
2
3
const message = 'Go to gate 23!';
console.log(message.padStart(20, '+').padEnd(30, '+'));
// +++++++Go to gate 23!+++++

示例:隐藏信用卡号码

1
2
3
4
5
6
const maskCreditCard = function (number) {
const str = number + '';
const last = str.slice(-4);
return last.padStart(str.length, '*');
};
console.log(maskCreditCard(43378463864647384)); // ************7384

重复字符串

repeat 可以将字符串重复多次。

1
2
3
const message2 = 'Bad weather... All Departures Delayed... ';
console.log(message2.repeat(3));
// Bad weather... All Departures Delayed... Bad weather... All Departures Delayed...

示例:显示排队的飞机数量

1
2
3
4
const planesInLine = function (n) {
console.log(`There are ${n} planes in line ${'🛩'.repeat(n)}`);
};
planesInLine(5); // There are 5 planes in line 🛩🛩🛩🛩🛩

总结

方法 功能说明 示例
length 获取字符串长度 'B737'.length4
[index] 获取特定位置的字符 'A320'[1]'3'
indexOf 查找子字符串的第一次出现位置 'TAP Air Portugal'.indexOf('r')6
lastIndexOf 查找子字符串的最后一次出现位置 'TAP Air Portugal'.lastIndexOf('r')10
slice 截取子字符串 'TAP Air Portugal'.slice(4, 7)'Air'
toLowerCase 转换为小写 'JonAS'.toLowerCase()'jonas'
toUpperCase 转换为大写 'Jonas'.toUpperCase()'JONAS'
trim 去除首尾空格 ' Hello '.trim()'Hello'
replace 替换子字符串 '288,97£'.replace('£', '$')'288,97$'
replaceAll 替换所有出现的子字符串 'door door'.replaceAll('door', 'gate')'gate gate'
includes 检查字符串是否包含子字符串 'Airbus A320'.includes('A320')true
startsWith 检查字符串是否以指定子字符串开头 'Airbus A320'.startsWith('Airb')true
endsWith 检查字符串是否以指定子字符串结尾 'Airbus A320neo'.endsWith('neo')true
split 分割字符串成数组 'a+nice+string'.split('+')['a', 'nice', 'string']
join 将数组元素拼接成字符串 ['a', 'nice', 'string'].join(' ')'a nice string'
padStart 在字符串开头填充指定字符 'Go to gate 23!'.padStart(20, '+')'++++++Go to gate 23!'
padEnd 在字符串末尾填充指定字符 'Go to gate 23!'.padEnd(20, '+')'Go to gate 23!++++++'
repeat 重复字符串多次 'All flights delayed... '.repeat(3)All flights delayed... All flights delayed... All flights delayed...

函数进阶

什么是一等公民(first-class citizen)?

一等公民(First-Class Citizen)是指在一个编程语言中,具有与其他数据类型相同地位和功能的数据类型。在JavaScript中,函数可以作为变量赋值、作为参数传递、作为返回值返回,以及可以作为对象的属性等。

我倒是感觉这个名字起的不太恰当,或者说和意思恰恰相反。原来函数这玩意儿挺高大上的,你这么一搞那我和Number还有什么区别?你什么档次还敢和我坐一桌?😡😡😡😡

开玩笑的。把函数作为一等公民能有以下几点好处:

  • 函数可作为变量
  • 函数作为参数
  • 函数作为返回值

这是Javascript自己最明显的特征,也是其函数式编程思想的基础。

*回调函数(Callback Functions)

在回调函数的使用中,通常一个函数会接受另一个函数作为参数并在合适的时机执行它。我们把作为参数的函数称为回调函数。

基本用法:

1
2
3
4
5
6
7
8
9
10
function greet(name) {
console.log(`Hello, ${name}!`);
}

function processUserInput(callback) {
const name = 'Alice';
callback(name); // 在内部调用回调函数
}

processUserInput(greet); // 输出:Hello, Alice!

说实话,在这个概念出现之前咱们早就用过不知道多少次了。比如,在事件监听中:

1
2
3
document.querySelector('button').addEventListener('click', function () {
console.log('Button clicked!');
});

只是当时没有点破而已。也许你现在会更加通透一些?

立即执行函数

立即执行函数表达式(Immediately Invoked Function Expression, IIFE)是一种特殊的函数表达方式,用于定义并立即执行一个函数。常用于在 JavaScript 中创建一个局部作用域,以避免变量污染全局作用域,尤其在有私有数据的情况下十分有用。

IIFE 主要有两种写法:

  1. 匿名函数 IIFE 使用 () 包裹函数定义,并紧随一对 () 以立即执行。

    1
    2
    3
    4
    (function () {
    console.log('This will never run again');
    const isPrivate = 23; // 局部作用域
    })();
  2. 箭头函数 IIFE 形式简洁。

    1
    (() => console.log('This will ALSO never run again'))();

IIFE 特点:

  • 立即执行:定义时立即执行,只会运行一次。
  • 局部作用域:通过 IIFE 创建的局部变量不会污染全局作用域。

**闭包

太好了,是闭包!我们没救了😫😫😫

作为国内大厂面试的高频必问考点之一,闭包的含金量自然不必多言;也有许多人认为,这就是Javascript中最难理解的概念。那么在本章,我们将亲手拆解属于闭包的谜团,帮助你彻底厘清闭包!

闭包奇怪在哪儿?

首先需要明确,闭包并不是我们可以手动执行的,它是程序内部自动发生的操作。因此,我们在这里先写一些代码来模拟闭包的环境:

1
2
3
4
5
6
7
8
9
10
const secureBooking = function(){
let passengerCount = 0;

return function(){
passengerCount ++;
console.log(`${passengerCount} passengers`);
}
}

const booker = secureBooking();

在这段代码中,secureBooking作为函数也返回了一个函数;而booker成功的调用了 secureBooking并获取secureBooking返回的结果,即return后的这个函数。因此,现在的booker也是一个函数。

那么当我们在全局作用域调用booker时,会发生什么呢?

显然,这会让程序在secureBooking函数的作用域中创建一个 passengerCount的变量,随后把函数返回给booker

这个时候,secureBooking的调用其实就已经结束了,其也从回调栈中弹出。

请注意:从事实上来说,secureBooking已经不在活跃环境中,就是已经消失了。这点很重要。

现在,从理论上讲,passengerCount变量应该也随着secureBooking一起消失了对吧?然而事实却是:

passengerCount仍然在随着booker的调用而增加!

这就是闭包的奇怪之处。简单来说,就是死去的变量开始攻击我

工作原理

所以我们的核心目标就是搞清楚,booker是如何获取到passengerAccount的。

其实核心就是一句话:

任意函数都有权利获取创造它环境时的执行上下文(execution context)。

在这个案例中,由于返回函数在secureBooking中被创造,因此它就有权利访问secureBooking函数块域中的所有值,其中包括passengerAccount变量。而这也正是所谓”闭包”概念的原理。

是的,通过闭包,即使作用域已经被销毁,当函数被调用时仍然可以永远携带被创造环境中的变量。它的优先级是高于作用域链的。也由于闭包的存在,函数不会轻易地失去与变量的链接,变量会永远存在于函数的诞生地。

现在我们回顾闭包的几种严格定义:

当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行的

闭包是指有权访问另一个函数作用域中变量的函数

怎么样,是不是感觉很轻松就理解了呢?🤗🤗🤗

数组进阶

我们继续来补充处理数组的各种进阶方法。


at 方法

at() 方法是 ECMAScript 2022 (ES13) 新增的一个字符串和数组方法,允许通过负整数索引从末尾访问数组或字符串的元素。负整数索引使得访问最后几个元素变得更加简洁,避免了使用 length - 1 这种方式。如果索引超出数组的范围,返回 undefined

语法

1
2
arr.at(index);
str.at(index);

示例:

1
2
3
4
5
6
const arr = [10, 20, 30, 40, 50];
console.log(arr.at(2)); // 输出 30
console.log(arr.at(-1)); // 输出 50
const str = "Hello, World!";
console.log(str.at(7)); // 输出 "W"
console.log(str.at(-6)); // 输出 "r"

forEach 遍历

与传统的 for 循环不同,forEach() 方法可以对数组中的每一个元素执行给定的回调函数。

语法

1
2
3
arr.forEach(callback(currentValue, index, array) {
// 执行的代码
});
  • callback: 用来处理数组元素的回调函数。
  • currentValue: 当前正在处理的元素。
  • index: 当前元素的索引(可选)。
  • array: 原始数组本身(可选)。

示例:

示例 1:基本用法

1
2
3
4
const numbers = [1, 2, 3, 4, 5];
numbers.forEach(function(number) {
console.log(number); // 输出 1 2 3 4 5
});

示例 2:带索引的遍历

1
2
3
4
5
6
7
8
const fruits = ['apple', 'banana', 'cherry'];
fruits.forEach(function(fruit, index) {
console.log(`${index}: ${fruit}`);
});
// 输出:
// 0: apple
// 1: banana
// 2: cherry

示例 3:箭头函数和 forEach 的结合

1
2
3
4
5
6
7
8
const colors = ['red', 'green', 'blue'];
colors.forEach((color, index) => {
console.log(`Color at index ${index}: ${color}`);
});
// 输出:
// Color at index 0: red
// Color at index 1: green
// Color at index 2: blue

比较 forEachfor 循环

特性 for 循环 forEach
可提前退出 可以使用 breakreturn 退出循环 无法中途退出,无法使用 break
适用场景 对数组元素进行复杂处理、需要控制循环流程 适合对所有元素执行相同操作
返回值 可以返回任意值 没有返回值,总是返回 undefined
执行性能 通常比 forEach 在性能要求较高的场景中不推荐使用
可操作索引和元素 手动访问索引、元素和条件判断 回调函数中自动访问元素及其索引

*三幻神:数组转换的三种方式

map 方法

map() 方法用于数组的转换,它会创建一个新数组,其中每个元素是调用回调函数后返回的结果。map() 不会修改原始数组。

语法

1
2
3
const newArray = arr.map(callback(currentValue, index, array) {
// return the transformed value
});
  • callback: 回调函数,接收三个参数:
    • currentValue: 当前元素的值。
    • index: 当前元素的索引(可选)。
    • array: 原始数组(可选)。

使用示例

示例 1:将数字数组的每个元素乘以 2

1
2
3
const numbers = [1, 2, 3, 4, 5];
const doubledNumbers = numbers.map(num => num * 2);
console.log(doubledNumbers); // 输出 [2, 4, 6, 8, 10]

示例 2:提取对象数组中的某个字段

1
2
3
4
5
6
7
const users = [
{ name: 'Alice', age: 25 },
{ name: 'Bob', age: 30 },
{ name: 'Charlie', age: 35 },
];
const names = users.map(user => user.name);
console.log(names); // 输出 ['Alice', 'Bob', 'Charlie']

filter 方法

filter() 方法用于筛选数组中的元素,返回一个新数组,包含满足条件的所有元素。filter() 不会修改原始数组。

语法

1
2
3
const newArray = arr.filter(callback(currentValue, index, array) {
// return true or false
});
  • callback: 回调函数,接收三个参数:
    • currentValue: 当前元素的值。
    • index: 当前元素的索引(可选)。
    • array: 原始数组(可选)。

使用示例

示例 1:筛选出大于 2 的数字

1
2
3
const numbers = [1, 2, 3, 4, 5];
const greaterThanTwo = numbers.filter(num => num > 2);
console.log(greaterThanTwo); // 输出 [3, 4, 5]

示例 2:筛选出年龄大于 30 的用户

1
2
3
4
5
6
7
const users = [
{ name: 'Alice', age: 25 },
{ name: 'Bob', age: 30 },
{ name: 'Charlie', age: 35 },
];
const adults = users.filter(user => user.age > 30);
console.log(adults); // 输出 [{ name: 'Charlie', age: 35 }]

reduce 方法

reduce() 方法用于将数组中的所有元素通过累加或其他计算方式,最终计算成一个单一的值。它接收一个回调函数,该回调函数会对数组中的每个元素执行累积操作。

语法

1
2
3
const result = arr.reduce(callback(accumulator, currentValue, index, array) {
// return updated accumulator
}, initialValue);
  • callback: 回调函数,接收四个参数:
    • accumulator: 累计器,记录当前的计算结果,最终返回给 reduce()
    • currentValue: 当前元素的值。
    • index: 当前元素的索引(可选)。
    • array: 原始数组(可选)。
  • initialValue: 可选,设置 accumulator 的初始值。如果省略,则第一次迭代时 accumulator 为数组的第一个元素。

使用示例

示例 1:求数组的和

1
2
3
const numbers = [1, 2, 3, 4, 5];
const sum = numbers.reduce((acc, num) => acc + num, 0);
console.log(sum); // 输出 15

示例 2:合并字符串

1
2
3
const words = ['Hello', 'world', 'from', 'JavaScript'];
const sentence = words.reduce((acc, word) => acc + ' ' + word, '');
console.log(sentence); // 输出 'Hello world from JavaScript'

示例 3:统计数组中每个元素的出现次数

1
2
3
4
5
6
const fruits = ['apple', 'banana', 'apple', 'orange', 'banana', 'apple'];
const fruitCount = fruits.reduce((acc, fruit) => {
acc[fruit] = (acc[fruit] || 0) + 1;
return acc;
}, {});
console.log(fruitCount); // 输出 { apple: 3, banana: 2, orange: 1 }

小结

方法 返回值 描述 适用场景
map() 新数组 对数组的每个元素执行相同操作并返回新数组 数组元素转换、字段提取
filter() 新数组 筛选数组中符合条件的元素 数组筛选、获取符合条件的元素
reduce() 单一值(任何类型) 将数组所有元素累加、合并成一个单一值 数组求和、生成对象或其他数据形式

补充:函数式编程思想与链式方法

函数式编程 + 链式方法就像搭积木,简单的积木组合起来可以创造出强大的功能,同时还能保持代码的整洁和优雅。

比如说,现在我们有一个需求:从一个包含用户年龄的数组中,筛选出大于 18 岁的用户年龄,然后将它们加倍,最后计算它们的总和。

传统的写法就是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const ages = [12, 19, 7, 25, 34];

// 筛选出大于 18 的年龄
const adults = [];
for (const age of ages) {
if (age > 18) adults.push(age);
}

// 加倍
const doubled = [];
for (const age of adults) {
doubled.push(age * 2);
}

// 求总和
let total = 0;
for (const age of doubled) {
total += age;
}
console.log(total); // 输出 156

它的问题是非常的冗长而且不直观,使用了额外的数组 adultsdoubled来 存储中间结果。

而函数式 + 链式方法呢?

1
2
3
4
5
6
7
8
const ages = [12, 19, 7, 25, 34];

const total = ages
.filter(age => age > 18) // 筛选出大于 18 的
.map(age => age * 2) // 每个年龄加倍
.reduce((sum, age) => sum + age, 0); // 从0开始求总和

console.log(total); // 输出 156

高下立判。


接下来又变得很简单了,你只需要:

1.知道有这么个方法;

2.知道这个方法大概干什么活。

留个印象就可以了。大不了忘了再查呗~

find & findIndex 方法

find 方法

find() 方法返回数组中第一个满足条件的元素。如果没有找到,返回 undefined

作用: 查找数组中符合条件的第一个元素。

示例:

1
2
3
const numbers = [5, 12, 8, 130, 44];
const found = numbers.find(num => num > 10);
console.log(found); // 输出: 12

findIndex 方法

findIndex() 方法返回数组中第一个满足条件的元素的索引。如果没有找到,返回 -1

作用: 查找数组中符合条件的第一个元素的索引。

示例:

1
2
3
const numbers = [5, 12, 8, 130, 44];
const index = numbers.findIndex(num => num > 10);
console.log(index); // 输出: 1 (因为 12 是第一个满足 num > 10 的元素)

some & every 方法

some 方法

some() 方法测试数组中的某些元素是否满足指定条件。只要有一个元素符合条件,就返回 true,否则返回 false

作用: 检查数组中是否至少有一个元素满足条件。

示例:

1
2
3
const numbers = [5, 12, 8, 130, 44];
const hasBigNumbers = numbers.some(num => num > 100);
console.log(hasBigNumbers); // 输出: true (因为 130 > 100)

every 方法

every() 方法测试数组中的所有元素是否都满足指定条件。只有所有元素都符合条件时,才返回 true,否则返回 false

作用: 检查数组中的所有元素是否都满足条件。

示例:

1
2
3
const numbers = [5, 12, 8, 130, 44];
const allBigNumbers = numbers.every(num => num > 4);
console.log(allBigNumbers); // 输出: true (因为数组中所有的数字都大于 4)

flat & flatMap 方法

flat 方法

flat() 方法用于将多维数组“拉平”,即将嵌套的数组转换为单一层级的数组。

作用: 展平多维数组。

示例:

1
2
3
const nestedArray = [1, [2, 3], [4, [5, 6]]];
const flattenedArray = nestedArray.flat(2);
console.log(flattenedArray); // 输出: [1, 2, 3, 4, 5, 6]

flat() 方法可以接收一个参数,表示要展开的层级数,默认为 1

flatMap 方法

flatMap() 方法首先对数组中的每个元素应用一个映射函数,然后将结果展开成一个新的数组。它相当于先做 map(),再做 flat()

作用: 对数组中的每个元素先进行映射操作,再将结果展平。

示例:

1
2
3
const arr = [1, 2, 3, 4];
const flattened = arr.flatMap(x => [x, x * 2]);
console.log(flattened); // 输出: [1, 2, 2, 4, 3, 6, 4, 8]

flatMap() 方法会将每个元素的返回值“拉平”到一个新数组中。

sort 方法

sort() 方法用于对数组的元素进行排序。排序是 原地修改 的,这意味着会更改原始数组。默认排序顺序是按照 字符串 Unicode 编码 升序。如果需要自定义排序,可以传入比较函数。

语法

1
arr.sort([compareFunction]);
  • compareFunction(可选):用来定义排序顺序的函数。
    • 如果 compareFunction(a, b) 返回值小于 0,则 a 会排在 b 前。
    • 如果返回值等于 0,则两者位置不变。
    • 如果返回值大于 0,则 b 会排在 a 前。

默认排序

默认按照字符串 Unicode 排序。

1
2
const arr = [100, 50, 2, 10];
console.log(arr.sort()); // 输出 [10, 100, 2, 50]

自定义排序

示例 1:数字排序(升序和降序)

1
2
3
4
5
6
7
// 结合语法部分仔细品味:
const numbers = [100, 50, 2, 10];
// 升序
console.log(numbers.sort((a, b) => a - b)); // 输出 [2, 10, 50, 100]

// 降序
console.log(numbers.sort((a, b) => b - a)); // 输出 [100, 50, 10, 2]

示例 2:字符串排序

1
2
3
4
5
6
7
const fruits = ['Banana', 'Apple', 'Orange'];

// 升序(默认)
console.log(fruits.sort()); // 输出 ['Apple', 'Banana', 'Orange']

// 降序
console.log(fruits.sort((a, b) => (a > b ? -1 : 1))); // 输出 ['Orange', 'Banana', 'Apple']

示例 3:按对象字段排序

1
2
3
4
5
6
7
8
9
10
11
12
13
const users = [
{ name: 'Alice', age: 25 },
{ name: 'Bob', age: 20 },
{ name: 'Charlie', age: 30 },
];

// 按年龄升序
console.log(users.sort((a, b) => a.age - b.age));
// 输出 [{name: 'Bob'}, {name: 'Alice'}, {name: 'Charlie'}]

// 按年龄降序
console.log(users.sort((a, b) => b.age - a.age));
// 输出 [{name: 'Charlie'}, {name: 'Alice'}, {name: 'Bob'}]

总结

  • 默认按照字符串 Unicode 排序,数字排序需要自定义比较函数。
  • 排序会修改原数组,因此需谨慎使用。
  • 常用于对数组元素进行排序(数字、字符串、对象等)。

from 方法

Array.from() 方法用于从 类数组对象可迭代对象 创建一个新的数组实例。

语法(看不懂可以先跳):

1
Array.from(arrayLike, [mapFunction, thisArg]);
  • arrayLike:必须。类数组对象或可迭代对象。
  • mapFunction(可选):一个函数,用于对每个元素执行操作。
  • thisArg(可选):执行回调时的 this 值。

基本用法

示例 1:从类数组对象创建数组

1
2
3
const str = 'hello';
const arr = Array.from(str);
console.log(arr); // 输出 ['h', 'e', 'l', 'l', 'o']

示例 2:从 arguments 创建数组

1
2
3
4
5
6
function test() {
console.log(arguments); // 类数组对象
const args = Array.from(arguments);
console.log(args); // 转为数组
}
test(1, 2, 3); // 输出 [1, 2, 3]

*示例 3:转换 HTML 集合

1
2
3
const listItems = document.querySelectorAll('li');
const itemTexts = Array.from(listItems, item => item.textContent);
console.log(itemTexts); // 输出每个 `<li>` 的文本内容

这种方法常用于处理 DOM 元素集合(如 NodeList)、生成指定长度的数组、对每个元素进行映射操作等。

小案例

将一组对象按某个字段排序,并从中提取某些属性:

1
2
3
4
5
6
7
8
9
10
11
12
const users = [
{ name: 'Alice', age: 25 },
{ name: 'Bob', age: 20 },
{ name: 'Charlie', age: 30 },
];

// 按年龄排序后提取名字
const sortedNames = Array.from(
users.sort((a, b) => a.age - b.age),
user => user.name
);
console.log(sortedNames); // 输出 ['Bob', 'Alice', 'Charlie']

*总结:23种数组处理方法

数字、时间、日期与计时器

数字相关操作

数字转换与检查

  • 数字转换:

    1
    2
    console.log(Number('23')); // 转换字符串为数字,输出:23
    console.log(+'23'); // 简写形式,同样输出:23
  • 解析字符串中的数字:

    1
    2
    console.log(Number.parseInt('30px', 10));  // 解析为 30
    console.log(Number.parseFloat(' 2.5rem ')); // 解析为 2.5
  • 检查数字有效性:

    1
    2
    console.log(Number.isNaN(+'20X'));    // 检查是否为 NaN
    console.log(Number.isFinite(23 / 0)); // 检查是否为有限数

通常来说,由于无穷大与无穷小并不属于NaN,因此isNaN方法的判定非常有可能是不严谨的。因此,isFinite方法会在实际场景中更常用一些。

数学操作与取整

  • 数学函数:

    1
    2
    3
    console.log(Math.sqrt(25));       // 开平方
    console.log(Math.max(5, '23')); // 最大值
    console.log(Math.PI * 10 ** 2); // 计算圆面积
  • 取整方法:

    1
    2
    3
    console.log(Math.round(23.9));    // 四舍五入
    console.log(Math.floor(23.9)); // 向下取整
    console.log(Math.trunc(23.9)); // 去掉小数部分
  • 生成随机数:

    1
    2
    3
    const randomInt = (min, max) =>
    Math.floor(Math.random() * (max - min + 1)) + min;
    console.log(randomInt(10, 20)); // 范围内随机整数

余数操作

  • 检查是否为偶数:

    1
    2
    const isEven = n => n % 2 === 0;
    console.log(isEven(8)); // true

大数字处理 (BigInt)

1
2
console.log(123456789012345678901234567890n); // 超大数字
console.log(1000n + 2000n); // 大数字运算

日期与时间操作

创建日期对象

  • 不同方式创建日期:
    1
    2
    3
    4
    5
    const now = new Date();
    console.log(now);

    const future = new Date(2037, 10, 19, 15, 23); // 年月日时分
    console.log(future);

日期操作

  • 获取日期信息:

    1
    2
    3
    console.log(future.getFullYear()); // 获取年份
    console.log(future.getMonth()); // 获取月份 (0-11)
    console.log(future.toISOString()); // 转为 ISO 格式
  • 设置日期:

    1
    2
    future.setFullYear(2040);
    console.log(future); // 更新年份
  • 日期差计算:

    1
    2
    3
    const calcDaysPassed = (date1, date2) =>
    Math.abs(date2 - date1) / (1000 * 60 * 60 * 24);
    console.log(calcDaysPassed(new Date(2037, 3, 4), new Date(2037, 3, 14)));

*计时器与时间间隔

设置定时器

  • 延时执行:

    1
    2
    3
    4
    5
    6
    7
    const ingredients = ['cheese', 'pepperoni'];
    const pizzaTimer = setTimeout(
    (ing1, ing2) => console.log(`Here is your pizza with ${ing1} and ${ing2}`),
    3000,
    ...ingredients
    );
    console.log('Waiting...');
  • 取消定时器:

    1
    if (ingredients.includes('pepperoni')) clearTimeout(pizzaTimer);

循环执行

  • 每秒显示当前时间:

    1
    2
    3
    4
    setInterval(() => {
    const now = new Date();
    console.log(now);
    }, 1000);

请注意,若定时器后还有代码,则后面的代码不会等待计时完成后再执行,而是立即执行。详细的原理要等到下一篇讲解。