1. 为什么需要数组
到目前为止,一个变量只装一个值。可是你有四个平台,难道要写四个变量 platform1、platform2……?将来有二十套穿搭,就要写二十个变量,想“把每一套都显示出来”还得写二十行。
数组就是用来装“一组同类东西”的。你可以把它想象成衣柜里的一根挂杆,上面的衣架按顺序编了号。有一点和日常习惯不同:编号从 0 开始。这个编号叫“下标”(也叫索引,index)。
const platforms = ["小红书", "TikTok", "Facebook", "Instagram"];
console.log(platforms);
console.log(platforms.length);
console.log(platforms[0]);
console.log(platforms[3]);
console.log(platforms[platforms.length - 1]);
console.log(platforms[4]);
运行结果:
[ '小红书', 'TikTok', 'Facebook', 'Instagram' ]
4
小红书
Instagram
Instagram
undefined
- 方括号
[ ] 创建数组,元素之间用逗号隔开。元素可以是任何类型,但通常一个数组里放同一类东西。
.length 是元素的个数,这里是 4。
platforms[0] 取第一个元素。方括号里写下标,第一个是 0,第四个是 3。
- 最后一个元素的下标永远是“长度减 1”,所以写成
platforms[platforms.length - 1]。这样不管数组有多长都能取对。
- 取一个不存在的下标不会报错,而是得到 undefined。4 个元素的数组,最大下标是 3。
为什么从 0 开始?可以把下标理解成“离开头走了几步”:第一个元素就在开头,走了 0 步;第二个走了 1 步。这样想,platforms[0] 是第一个就不别扭了。几乎所有编程语言都这样数,刚开始容易错,写多了自然就习惯。
提示:上面的第一行是 Node 的显示方式。在 Chrome 控制台里,数组会显示成 (4) ['小红书', 'TikTok', 'Facebook', 'Instagram'],前面的 (4) 是长度,点左边的小三角可以展开看每个下标。样子不同,内容一样。
学会数组,昨天的 weekdayName 可以大大缩短。星期几的数字 0 到 6,正好可以当下标用:
const weekdays = ["周日", "周一", "周二", "周三", "周四", "周五", "周六"];
function weekdayName(dayNumber) {
return weekdays[dayNumber];
}
console.log(weekdayName(4));
console.log(weekdayName(0));
运行结果:
周四
周日
二十多行的 switch 变成了一行。“用一个数字去数组里查对应的值”,是非常常见的写法。
2. 修改和增删元素
用下标可以直接改掉某个位置的值,还有四个方法专门在两头增删元素。“方法”就是跟在数组后面、用点号调用的函数,你昨天在日期上用过的 getMonth() 也是方法。
const worlds = ["绅士", "街头"];
worlds.push("泰风");
console.log(worlds);
worlds[0] = "绅士线";
console.log(worlds);
const last = worlds.pop();
console.log(last);
console.log(worlds);
worlds.unshift("三岁时刻");
console.log(worlds);
worlds.shift();
console.log(worlds);
运行结果:
[ '绅士', '街头', '泰风' ]
[ '绅士线', '街头', '泰风' ]
泰风
[ '绅士线', '街头' ]
[ '三岁时刻', '绅士线', '街头' ]
[ '绅士线', '街头' ]
| 方法 | 做什么 | 好记的说法 |
push(值) | 在末尾加一个 | 往挂杆最右边挂一件 |
pop() | 删掉末尾那个,并返回它 | 取下最右边那件 |
unshift(值) | 在开头加一个 | 往最左边挂一件,其他的往右挪 |
shift() | 删掉开头那个,并返回它 | 取下最左边那件 |
你可能注意到了:worlds 是用 const 声明的,为什么还能改里面的内容?因为 const 管的是“标签不能贴到别的数组上”,不管“数组里面放什么”。worlds.push(...) 没问题,worlds = ["别的"] 才会报错。
想在中间删除或插入,用 splice(从哪个下标开始, 删几个, 要插入的值...):
const schedule = ["绅士线", "街头线", "泰风线", "三岁时刻", "三岁换装"];
schedule.splice(1, 1);
console.log(schedule);
schedule.splice(1, 0, "街头线(重播)");
console.log(schedule);
运行结果:
[ '绅士线', '泰风线', '三岁时刻', '三岁换装' ]
[ '绅士线', '街头线(重播)', '泰风线', '三岁时刻', '三岁换装' ]
第一次从下标 1 开始删 1 个,删掉了“街头线”。第二次从下标 1 开始删 0 个,也就是不删,只在这个位置插入一个新值。
想知道数组里有没有某个值,用 includes;想知道它在第几个,用 indexOf:
const platforms = ["小红书", "TikTok", "Facebook", "Instagram"];
console.log(platforms.includes("TikTok"));
console.log(platforms.includes("YouTube"));
console.log(platforms.indexOf("Facebook"));
console.log(platforms.indexOf("YouTube"));
运行结果:
true
false
2
-1
indexOf 找不到时返回 -1,因为 -1 不可能是一个真实的下标。
这里有个区别要留心:push、pop、unshift、shift、splice 会直接修改原来的数组;而下面要学的 map、filter 不动原数组,而是交给你一个新数组。后面学 React 时,“不修改原数据、生成新数据”会变得非常重要,现在先记住哪些方法会改原数组。
3. 遍历:把每个元素都处理一遍
数组最常做的事,就是“对每个元素做同一件事”,这叫遍历。有三种写法。第一种是你昨天就会的 for 循环,用下标一个个取:
const platforms = ["小红书", "TikTok", "Facebook", "Instagram"];
for (let i = 0; i < platforms.length; i++) {
console.log(`${i + 1}. ${platforms[i]}`);
}
运行结果:
1. 小红书
2. TikTok
3. Facebook
4. Instagram
注意循环从 i = 0 开始,条件是 i < platforms.length(小于,不是小于等于),这样 i 正好走遍 0、1、2、3。显示给人看时加 1,从 1 开始编号。
第二种是 for...of,不需要下标时最简洁,读作“对于数组里的每一个 platform”:
const platforms = ["小红书", "TikTok", "Facebook", "Instagram"];
for (const platform of platforms) {
console.log(platform);
}
运行结果:
小红书
TikTok
Facebook
Instagram
第三种是数组自带的方法 forEach,它是今天后面几个方法的基础:
const platforms = ["小红书", "TikTok", "Facebook", "Instagram"];
platforms.forEach((platform, index) => {
console.log(`${index + 1}. ${platform}`);
});
运行结果:
1. 小红书
2. TikTok
3. Facebook
4. Instagram
这里第一次出现了一个重要的概念:把一个函数当作参数,交给另一个函数。括号里的 (platform, index) => { ... } 是一个箭头函数,你没有自己调用它,而是把它交给了 forEach。forEach 会替你调用它,每个元素调用一次,每次把“当前元素”和“它的下标”作为参数传进来。这种交给别人去调用的函数,叫回调函数。
参数名是你自己起的,习惯上用数组名的单数:platforms 里的每一个叫 platform,looks 里的每一个叫 look。不需要下标时,第二个参数可以不写。
三种写法怎么选?需要中途停下(break)或者要跳着走(比如只看偶数下标)时,用普通 for 循环;只是从头到尾走一遍,用 for...of 或 forEach 都可以。注意 forEach 里不能用 break,它一定会把每个元素都走完。本课表后面会大量使用 forEach 以及下面要学的 map、filter、find,因为它们读起来更像一句话:“对每一套穿搭,做某件事”。
4. 一件衣服有好几条信息:先认识对象
下午要做的 looks 数组,每一项是一套穿搭,每套有名字、属于哪个世界、主色是什么三条信息。一个字符串装不下三条信息,这就需要对象。明天 DAY 17 会专门讲对象,今天先学够用的部分:
const look = { name: "Baggy 裤配 Vans", world: "street", color: "#3a5a80" };
console.log(look.name);
console.log(look.world);
运行结果:
Baggy 裤配 Vans
street
花括号 { } 创建对象,里面是一对对“名字: 值”,用逗号隔开。名字叫“属性名”,取值时用点号:look.name 读作“这套穿搭的名字”。
把好几个这样的对象放进一个数组,就是“对象数组”,这是今后你最常打交道的数据形状。后面网站里的 looks、平台列表、提问箱,数据都长这样:
learn/js/day16-looks.js
const looks = [
{ name: "马甲三件套", world: "gentleman", color: "#2f3e46" },
{ name: "白衬衫配牛津皮鞋", world: "gentleman", color: "#c9b99a" },
{ name: "Baggy 裤配 Vans", world: "street", color: "#3a5a80" },
{ name: "Oversize 卫衣", world: "street", color: "#e07a5f" },
{ name: "泰拳短裤训练装", world: "thai", color: "#b5171f" },
{ name: "绑手带配运动背心", world: "thai", color: "#f2a541" },
];
console.log(looks.length);
console.log(looks[2].name);
console.log(looks[looks.length - 1].world);
运行结果:
6
Baggy 裤配 Vans
thai
looks[2].name 从左往右读:先用下标取出第三套穿搭(一个对象),再用点号取它的名字。数组最后一个元素后面多写一个逗号是允许的,这样以后加新的一行时不容易漏逗号。color 是这套衣服的主色,用的是你在 CSS 里熟悉的十六进制颜色,DAY 19 生成卡片时会用上。
接下来的三节,都用这个 looks 数组举例。每个例子都把 looks 完整写了一遍,这样你可以单独复制任何一段去运行。
5. map:把每一项变成另一个样子
为什么需要它:你有 6 套穿搭的完整信息,但页面上只想显示名字列表。你要的是“一个新数组,里面是每套穿搭的名字”。这种“一一对应地转换”就用 map:
const looks = [
{ name: "马甲三件套", world: "gentleman", color: "#2f3e46" },
{ name: "白衬衫配牛津皮鞋", world: "gentleman", color: "#c9b99a" },
{ name: "Baggy 裤配 Vans", world: "street", color: "#3a5a80" },
{ name: "Oversize 卫衣", world: "street", color: "#e07a5f" },
{ name: "泰拳短裤训练装", world: "thai", color: "#b5171f" },
{ name: "绑手带配运动背心", world: "thai", color: "#f2a541" },
];
const names = looks.map((look) => look.name);
console.log(names);
console.log(looks.length, names.length);
运行结果:
[
'马甲三件套',
'白衬衫配牛津皮鞋',
'Baggy 裤配 Vans',
'Oversize 卫衣',
'泰拳短裤训练装',
'绑手带配运动背心'
]
6 6
- map 和 forEach 一样,会对每个元素调用一次你给的回调函数。
- 不同的是,map 会把回调函数每次的返回值收集起来,组成一个新数组交给你。回调返回
look.name,新数组里装的就是名字。
- 新数组的长度一定和原数组一样:6 进,6 出。原来的 looks 一点没变。
(look) => look.name 是 DAY 15 学的简写箭头函数,省略了花括号,自动返回 look.name。
- 数组太长时,Node 会把它拆成每个元素一行来显示,这只是显示方式,数组本身没变。
可以把 map 想象成一条流水线:每件衣服依次经过同一台机器,出来的是它的“名牌”。进去几件,出来几个,顺序不变。DAY 19 你会用同样的思路,把每套穿搭变成页面上的一张卡片,DAY 28 在 React 里渲染列表时,用的也是 map。可以说,map 是前端开发里用得最多的数组方法。
回调函数也能拿到第二个参数下标,比如生成带编号的标题:
const worlds = ["gentleman", "street", "thai"];
const labels = worlds.map((world, index) => `${index + 1}号世界:${world}`);
console.log(labels);
运行结果:
[ '1号世界:gentleman', '2号世界:street', '3号世界:thai' ]
6. filter 和 find:筛选和查找
想只看“街头”的穿搭?用 filter(过滤)。它也接收一个回调函数,这个函数要返回 true 或 false,回答“这一项要不要留下”:
const looks = [
{ name: "马甲三件套", world: "gentleman", color: "#2f3e46" },
{ name: "白衬衫配牛津皮鞋", world: "gentleman", color: "#c9b99a" },
{ name: "Baggy 裤配 Vans", world: "street", color: "#3a5a80" },
{ name: "Oversize 卫衣", world: "street", color: "#e07a5f" },
{ name: "泰拳短裤训练装", world: "thai", color: "#b5171f" },
{ name: "绑手带配运动背心", world: "thai", color: "#f2a541" },
];
const streetLooks = looks.filter((look) => look.world === "street");
console.log(streetLooks.length);
console.log(streetLooks[0].name);
const snookerLooks = looks.filter((look) => look.world === "snooker");
console.log(snookerLooks);
运行结果:
2
Baggy 裤配 Vans
[]
回调 (look) => look.world === "street" 读作“对每一套 look,问一句:它的 world 是 street 吗”。=== 本身就会算出 true 或 false,所以直接返回它就行,不需要写成 if (...) { return true; } else { return false; } 那么啰嗦。filter 对每一项调用回调,返回 true 的留下,false 的去掉,留下的组成一个新数组。filter 的结果永远是数组,一个都没筛到,就是空数组 []。
如果你只想要一个,比如“找到名字里带 Vans 的那一套”,用 find:
const looks = [
{ name: "马甲三件套", world: "gentleman", color: "#2f3e46" },
{ name: "白衬衫配牛津皮鞋", world: "gentleman", color: "#c9b99a" },
{ name: "Baggy 裤配 Vans", world: "street", color: "#3a5a80" },
{ name: "Oversize 卫衣", world: "street", color: "#e07a5f" },
{ name: "泰拳短裤训练装", world: "thai", color: "#b5171f" },
{ name: "绑手带配运动背心", world: "thai", color: "#f2a541" },
];
const vans = looks.find((look) => look.name.includes("Vans"));
console.log(vans);
const firstThai = looks.find((look) => look.world === "thai");
console.log(firstThai.name);
const missing = looks.find((look) => look.world === "snooker");
console.log(missing);
运行结果:
{ name: 'Baggy 裤配 Vans', world: 'street', color: '#3a5a80' }
泰拳短裤训练装
undefined
- find 从头开始找,找到第一个让回调返回 true 的元素,直接返回这个元素本身(不是数组),然后停止。泰风有两套,find 只给你第一套。
- 一个都找不到,返回 undefined。
look.name.includes("Vans"):字符串也有 includes 方法,判断里面是否包含某段文字。
注意:find 可能返回 undefined。如果你直接写 missing.name,程序会报错 TypeError: Cannot read properties of undefined (reading 'name')。用 find 的结果之前,先用 if 判断一下它是不是找到了。
7. 怎么选:map、forEach、filter、find
四个方法长得很像,都接收一个回调函数、都会遍历数组,区别在于它们返回什么:
| 方法 | 回调返回什么 | 方法本身返回什么 | 什么时候用 |
forEach | 不在乎 | undefined | 只是对每一项“做件事”,比如打印 |
map | 新的值 | 一样长的新数组 | 把每一项变成另一个样子 |
filter | true 或 false | 筛选后的新数组,可能为空 | 要“所有符合条件的” |
find | true 或 false | 第一个符合的元素,或 undefined | 只要“一个” |
forEach 什么也不返回,这一点可以验证。下面的 toUpperCase() 是字符串的方法,作用是把英文字母变成大写,回调确实返回了大写的名字,但 forEach 不会收集它们:
const worlds = ["gentleman", "street", "thai"];
const result = worlds.forEach((world) => world.toUpperCase());
console.log(result);
运行结果:
undefined
这些方法还可以接着调用,叫“链式调用”。比如“街头穿搭的名字”,就是先 filter 再 map:
const looks = [
{ name: "马甲三件套", world: "gentleman", color: "#2f3e46" },
{ name: "白衬衫配牛津皮鞋", world: "gentleman", color: "#c9b99a" },
{ name: "Baggy 裤配 Vans", world: "street", color: "#3a5a80" },
{ name: "Oversize 卫衣", world: "street", color: "#e07a5f" },
{ name: "泰拳短裤训练装", world: "thai", color: "#b5171f" },
{ name: "绑手带配运动背心", world: "thai", color: "#f2a541" },
];
const streetNames = looks
.filter((look) => look.world === "street")
.map((look) => look.name);
console.log(streetNames);
运行结果:
[ 'Baggy 裤配 Vans', 'Oversize 卫衣' ]
从上往下读:从 looks 出发,筛出街头的,再取出名字。每一步都产生一个新数组,交给下一步。明天 DAY 17 深入学对象,你会用同样的方法处理四个平台的数据。