现代js教程摘录(数据类型)

225 阅读28分钟

本博客内容均摘录自现代js教程,连接如下

https://zh.javascript.info

本文知识摘录一些自己认为需要知道的重点知识,并不是完整的js学习文章,由于篇幅很长,所以分多次发布

数据类型

原始类型的方法

请阅读原文

数字类型

想象一下,我们需要写 10 亿。显然的方法是:

let billion = 1000000000;

但在现实生活中,我们通常避免写一长串零,因为它很容易打错。另外,我们很懒。我们通常会将 10 亿写成 "1bn",或将 73 亿写成 "7.3bn"。对于大多数大的数字来说都是如此。

在 JavaScript 中,我们通过在数字后附加字母 “e”,并指定零的数量来缩短数字:

let billion = 1e9;  // 10 亿,字面意思:数字 1 后面跟 9 个 0

alert( 7.3e9 );  // 73 亿(7,300,000,000)

现在让我们写一些非常小的数字。例如,1 微秒(百万分之一秒):

let ms = 0.000001;

就像以前一样,可以使用 "e" 来完成。如果我们想避免显式地写零,我们可以这样写:

let ms = 1e-6; // 1 的左边有 6 个 0

如果我们数一下 0.000001 中的 0 的个数,是 6 个。所以自然是 1e-6

换句话说,e 后面的负数表示除以 1 后面跟着给定数量的 0 的数字:

// -3 除以 1 后面跟着 3 个 0 的数字
1e-3 = 1 / 1000 (=0.001)

// -6 除以 1 后面跟着 6 个 0 的数字
1.23e-6 = 1.23 / 1000000 (=0.00000123)

十六进制 数字在 JavaScript 中被广泛用于表示颜色,编码字符以及其他许多东西。所以自然地,有一种较短的写方法:0x,然后是数字。

例如:

alert( 0xff ); // 255
alert( 0xFF ); // 255(一样,大小写没影响)

二进制和八进制数字系统很少使用,但也支持使用 0b0o 前缀:

let a = 0b11111111; // 二进制形式的 255
let b = 0o377; // 八进制形式的 255

alert( a == b ); // true,两边是相同的数字,都是 255
toString(base)

方法 num.toString(base) 返回在给定 base 进制数字系统中 num 的字符串表示形式。

举个例子:

let num = 255;

alert( num.toString(16) );  // ff
alert( num.toString(2) );   // 11111111

base 的范围可以从 236。默认情况下是 10

常见的用例如下:

  • base=16 用于十六进制颜色,字符编码等,数字可以是 0..9A..F

  • base=2 主要用于调试按位操作,数字可以是 01

  • base=36 是最大进制,数字可以是 0..9A..Z。所有拉丁字母都被用于了表示数字。对于 36 进制来说,一个有趣且有用的例子是,当我们需要将一个较长的数字标识符转换成较短的时候,例如做一个短的 URL。可以简单地使用基数为 36 的数字系统表示:

    alert( 123456..toString(36) ); // 2n9c
    

使用两个点来调用一个方法

请注意 123456..toString(36) 中的两个点不是打错了。如果我们想直接在一个数字上调用一个方法,比如上面例子中的 toString,那么我们需要在它后面放置两个点 ..

如果我们放置一个点:123456.toString(36),那么就会出现一个 error,因为 JavaScript 语法隐含了第一个点之后的部分为小数部分。如果我们再放一个点,那么 JavaScript 就知道小数部分为空,现在使用该方法。

也可以写成 (123456).toString(36)

舍入

舍入(rounding)是使用数字时最常用的操作之一。

这里有几个对数字进行舍入的内建函数:

  • Math.floor

    向下舍入:3.1 变成 3-1.1 变成 -2

  • Math.ceil

    向上舍入:3.1 变成 4-1.1 变成 -1

  • Math.round

    向最近的整数舍入:3.1 变成 33.6 变成 4-1.1 变成 -1

  • Math.trunc(IE 浏览器不支持这个方法)

    移除小数点后的所有内容而没有舍入:3.1 变成 3-1.1 变成 -1

这个是总结它们之间差异的表格:

Math.floorMath.ceilMath.roundMath.trunc
3.13433
3.63443
-1.1-2-1-1-1
-1.6-2-1-2-1

这些函数涵盖了处理数字小数部分的所有可能方法。但是,如果我们想将数字舍入到小数点后 n 位,该怎么办?

例如,我们有 1.2345,并且想把它舍入到小数点后两位,仅得到 1.23

有两种方式可以实现这个需求:

  1. 乘除法

    例如,要将数字舍入到小数点后两位,我们可以将数字乘以 100(或更大的 10 的整数次幂),调用舍入函数,然后再将其除回。

    let num = 1.23456;
    
    alert( Math.floor(num * 100) / 100 ); // 1.23456 -> 123.456 -> 123 -> 1.23
    
  2. 函数 toFixed(n) 将数字舍入到小数点后 n 位,并以字符串形式返回结果。

    let num = 12.34;
    alert( num.toFixed(1) ); // "12.3"
    

    这会向上或向下舍入到最接近的值,类似于 Math.round

    let num = 12.36;
    alert( num.toFixed(1) ); // "12.4"
    

    请注意 toFixed 的结果是一个字符串。如果小数部分比所需要的短,则在结尾添加零:

    let num = 12.34;
    alert( num.toFixed(5) ); // "12.34000",在结尾添加了 0,以达到小数点后五位
    

    我们可以使用一元加号或 Number() 调用,将其转换为数字:+ num.toFixed(5)

不精确的计算

在内部,数字是以 64 位格式 IEEE-754 表示的,所以正好有 64 位可以存储一个数字:其中 52 位被用于存储这些数字,其中 11 位用于存储小数点的位置(对于整数,它们为零),而 1 位用于符号。

如果一个数字太大,则会溢出 64 位存储,并可能会导致无穷大:

alert( 1e500 ); // Infinity

这可能不那么明显,但经常会发生的是,精度的损失。

考虑下这个(falsy!)测试:

alert( 0.1 + 0.2 == 0.3 ); // false

没错,如果我们检查 0.10.2 的总和是否为 0.3,我们会得到 false

奇了怪了!如果不是 0.3,那能是啥?

alert( 0.1 + 0.2 ); // 0.30000000000000004

哎哟!这个错误比不正确的比较的后果更严重。想象一下,你创建了一个电子购物网站,如果访问者将价格为 ¥ 0.10¥ 0.20 的商品放入了他的购物车。订单总额将是 ¥ 0.30000000000000004。这会让任何人感到惊讶。

但为什么会这样呢?

一个数字以其二进制的形式存储在内存中,一个 1 和 0 的序列。但是在十进制数字系统中看起来很简单的 0.10.2 这样的小数,实际上在二进制形式中是无限循环小数。

换句话说,什么是 0.10.1 就是 1 除以 101/10,即十分之一。在十进制数字系统中,这样的数字表示起来很容易。将其与三分之一进行比较:1/3。三分之一变成了无限循环小数 0.33333(3)

在十进制数字系统中,可以保证以 10 的整数次幂作为除数能够正常工作,但是以 3 作为除数则不能。也是同样的原因,在二进制数字系统中,可以保证以 2 的整数次幂作为除数时能够正常工作,但 1/10 就变成了一个无限循环的二进制小数。

使用二进制数字系统无法 精确 存储 0.10.2,就像没有办法将三分之一存储为十进制小数一样。

IEEE-754 数字格式通过将数字舍入到最接近的可能数字来解决此问题。这些舍入规则通常不允许我们看到“极小的精度损失”,但是它确实存在。

我们可以看到:

alert( 0.1.toFixed(20) ); // 0.10000000000000000555

当我们对两个数字进行求和时,它们的“精度损失”会叠加起来。

这就是为什么 0.1 + 0.2 不等于 0.3

不仅仅是 JavaScript

许多其他编程语言也存在同样的问题。

PHP,Java,C,Perl,Ruby 给出的也是完全相同的结果,因为它们基于的是相同的数字格式。

我们能解决这个问题吗?当然,最可靠的方法是借助方法 toFixed(n) 对结果进行舍入:

let sum = 0.1 + 0.2;
alert( sum.toFixed(2) ); // 0.30

请注意,toFixed 总是返回一个字符串。它确保小数点后有 2 位数字。如果我们有一个电子购物网站,并需要显示 ¥ 0.30,这实际上很方便。对于其他情况,我们可以使用一元加号将其强制转换为一个数字:

let sum = 0.1 + 0.2;
alert( +sum.toFixed(2) ); // 0.3

我们可以将数字临时乘以 100(或更大的数字),将其转换为整数,进行数学运算,然后再除回。当我们使用整数进行数学运算时,误差会有所减少,但仍然可以在除法中得到:

alert( (0.1 * 10 + 0.2 * 10) / 10 ); // 0.3
alert( (0.28 * 100 + 0.14 * 100) / 100); // 0.4200000000000001

因此,乘/除法可以减少误差,但不能完全消除误差。

有时候我们可以尝试完全避免小数。例如,我们正在创建一个电子购物网站,那么我们可以用角而不是元来存储价格。但是,如果我们要打 30% 的折扣呢?实际上,完全避免小数处理几乎是不可能的。只需要在必要时剪掉其“尾巴”来对其进行舍入即可。

有趣的事儿

尝试运行下面这段代码:

// Hello!我是一个会自我增加的数字!
alert( 9999999999999999 ); // 显示 10000000000000000

出现了同样的问题:精度损失。有 64 位来表示该数字,其中 52 位可用于存储数字,但这还不够。所以最不重要的数字就消失了。

JavaScript 不会在此类事件中触发 error。它会尽最大努力使数字符合所需的格式,但不幸的是,这种格式不够大到满足需求。

两个零

数字内部表示的另一个有趣结果是存在两个零:0-0

这是因为在存储时,使用一位来存储符号,因此对于包括零在内的任何数字,可以设置这一位或者不设置。

在大多数情况下,这种区别并不明显,因为运算符将它们视为相同的值。

值 “NaN” 是独一无二的,它不等于任何东西,包括它自身:

alert( NaN === NaN ); // false

Object.is 进行比较

有一个特殊的内建方法 Object.is,它类似于 === 一样对值进行比较,但它对于两种边缘情况更可靠:

  1. 它适用于 NaNObject.is(NaN,NaN)=== true,这是件好事。
  2. 0-0 是不同的:Object.is(0,-0)=== false,从技术上讲这是对的,因为在内部,数字的符号位可能会不同,即使其他所有位均为零。

在所有其他情况下,Object.is(a,b)a === b 相同。

这种比较方式经常被用在 JavaScript 规范中。当内部算法需要比较两个值是否完全相同时,它使用 Object.is(内部称为 SameValue)。

parseInt 和 parseFloat

使用加号 +Number() 的数字转换是严格的。如果一个值不完全是一个数字,就会失败:

alert( +"100px" ); // NaN

唯一的例外是字符串开头或结尾的空格,因为它们会被忽略。

但在现实生活中,我们经常会有带有单位的值,例如 CSS 中的 "100px""12pt"。并且,在很多国家,货币符号是紧随金额之后的,所以我们有 "19€",并希望从中提取出一个数值。

这就是 parseIntparseFloat 的作用。

它们可以从字符串中“读取”数字,直到无法读取为止。如果发生 error,则返回收集到的数字。函数 parseInt返回一个整数,而 parseFloat 返回一个浮点数:

alert( parseInt('100px') ); // 100
alert( parseFloat('12.5em') ); // 12.5

alert( parseInt('12.3') ); // 12,只有整数部分被返回了
alert( parseFloat('12.3.4') ); // 12.3,在第二个点出停止了读取

某些情况下,parseInt/parseFloat 会返回 NaN。当没有数字可读时会发生这种情况:

alert( parseInt('a123') ); // NaN,第一个符号停止了读取

parseInt(str, radix)` 的第二个参数

parseInt() 函数具有可选的第二个参数。它指定了数字系统的基数,因此 parseInt 还可以解析十六进制数字、二进制数字等的字符串:

alert( parseInt('0xff', 16) ); // 255
alert( parseInt('ff', 16) ); // 255,没有 0x 仍然有效

alert( parseInt('2n9c', 36) ); // 123456
其他数学函数

JavaScript 有一个内建的 Math 对象,它包含了一个小型的数学函数和常量库。

几个例子:

  • Math.random()

    返回一个从 0 到 1 的随机数(不包括 1)alert( Math.random() ); // 0.1234567894322 alert( Math.random() ); // 0.5435252343232 alert( Math.random() ); // ... (任何随机数)

  • Math.max(a, b, c...) / Math.min(a, b, c...)

    从任意数量的参数中返回最大/最小值。alert( Math.max(3, 5, -10, 0, 1) ); // 5 alert( Math.min(1, 2) ); // 1

  • Math.pow(n, power)

    返回 n 的给定(power)次幂alert( Math.pow(2, 10) ); // 2 的 10 次幂 = 1024

Math 对象中还有更多函数和常量,包括三角函数,你可以在 Math 函数文档 中找到这些内容。

字符串

length 属性表示字符串长度:

alert( `My\n`.length ); // 3

注意 \n 是一个单独的“特殊”字符,所以长度确实是 3

length 是一个属性

掌握其他编程语言的人,有时会错误地调用 str.length() 而不是 str.length。这是行不通的。

请注意 str.length 是一个数字属性,而不是函数。后面不需要添加括号。

要获取在 pos 位置的一个字符,可以使用方括号 [pos] 或者调用 str.charAt(pos) 方法。第一个字符从零位置开始:

let str = `Hello`;

// 第一个字符
alert( str[0] ); // H
alert( str.charAt(0) ); // H

// 最后一个字符
alert( str[str.length - 1] ); // o

方括号是获取字符的一种现代化方法,而 charAt 是历史原因才存在的。

它们之间的唯一区别是,如果没有找到字符,[] 返回 undefined,而 charAt 返回一个空字符串:

let str = `Hello`;

alert( str[1000] ); // undefined
alert( str.charAt(1000) ); // ''(空字符串)

我们也可以使用 for..of 遍历字符:

for (let char of "Hello") {
  alert(char); // H,e,l,l,o(char 变为 "H",然后是 "e",然后是 "l" 等)
}

if 测试中 indexOf 有一点不方便。我们不能像这样把它放在 if 中:

let str = "Widget with id";

if (str.indexOf("Widget")) {
    alert("We found it"); // 不工作!
}

上述示例中的 alert 不会显示,因为 str.indexOf("Widget") 返回 0(意思是它在起始位置就查找到了匹配项)。是的,但是 if 认为 0 表示 false

因此我们应该检查 -1,像这样:

let str = "Widget with id";

if (str.indexOf("Widget") != -1) {
    alert("We found it"); // 现在工作了!
}
includes,startsWith,endsWith

更现代的方法 str.includes(substr, pos) 根据 str 中是否包含 substr 来返回 true/false

如果我们需要检测匹配,但不需要它的位置,那么这是正确的选择:

alert( "Widget with id".includes("Widget") ); // true

alert( "Hello".includes("Bye") ); // false

str.includes 的第二个可选参数是开始搜索的起始位置:

alert( "Midget".includes("id") ); // true
alert( "Midget".includes("id", 3) ); // false, 从位置 3 开始没有 "id"

方法 str.startsWithstr.endsWith 的功能与其名称所表示的意思相同:

alert( "Widget".startsWith("Wid") ); // true,"Widget" 以 "Wid" 开始
alert( "Widget".endsWith("get") ); // true,"Widget" 以 "get" 结束

JavaScript 中有三种获取字符串的方法:substringsubstrslice

  • str.slice(start [, end])

    返回字符串从 start 到(但不包括)end 的部分。例如:let str = "stringify"; alert( str.slice(0, 5) ); // 'strin',从 0 到 5 的子字符串(不包括 5) alert( str.slice(0, 1) ); // 's',从 0 到 1,但不包括 1,所以只有在 0 处的字符如果没有第二个参数,slice 会一直运行到字符串末尾:let str = "st*ringify*"; alert( str.slice(2) ); // 从第二个位置直到结束``start/end 也有可能是负值。它们的意思是起始位置从字符串结尾计算:let str = "strin*gif*y"; // 从右边的第四个位置开始,在右边的第一个位置结束 alert( str.slice(-4, -1) ); // 'gif'

  • str.substring(start [, end])

    返回字符串在 startend 之间 的部分。这与 slice 几乎相同,但它允许 start 大于 end。例如:let str = "st*ring*ify"; // 这些对于 substring 是相同的 alert( str.substring(2, 6) ); // "ring" alert( str.substring(6, 2) ); // "ring" // ……但对 slice 是不同的: alert( str.slice(2, 6) ); // "ring"(一样) alert( str.slice(6, 2) ); // ""(空字符串)不支持负参数(不像 slice),它们被视为 0

  • str.substr(start [, length])

    返回字符串从 start 开始的给定 length 的部分。与以前的方法相比,这个允许我们指定 length 而不是结束位置:let str = "st*ring*ify"; alert( str.substr(2, 4) ); // 'ring',从位置 2 开始,获取 4 个字符第一个参数可能是负数,从结尾算起:let str = "strin*gi*fy"; alert( str.substr(-4, 2) ); // 'gi',从第 4 位获取 2 个字符

我们回顾一下这些方法,以免混淆:

方法选择方式……负值参数
slice(start, end)startend(不含 end允许
substring(start, end)startend 之间(包括 start,但不包括 end负值代表 0
substr(start, length)start 开始获取长为 length 的字符串允许 start 为负数

数组

pop/push, shift/unshift 方法

队列(queue)是最常见的使用数组的方法之一。在计算机科学中,这表示支持两个操作的一个有序元素的集合:

  • push 在末端添加一个元素.
  • shift 取出队列首端的一个元素,整个队列往前移,这样原先排第二的元素现在排在了第一。

这两种操作数组都支持。

队列的应用在实践中经常会碰到。例如需要在屏幕上显示消息队列。

数组还有另一个用例,就是数据结构

它支持两种操作:

  • push 在末端添加一个元素.
  • pop 从末端取出一个元素.

所以新元素的添加和取出都是从“末端”开始的。

栈通常被被形容成一叠卡片:要么在最上面添加卡片,要么从最上面拿走卡片:

对于栈来说,最后放进去的内容是最先接收的,也叫做 LIFO(Last-In-First-Out),即后进先出法则。而与队列相对应的叫做 FIFO(First-In-First-Out),即先进先出。

JavaScript 中的数组既可以用作队列,也可以用作栈。它们允许你从首端/末端来添加/删除元素。

这在计算机科学中,允许这样的操作的数据结构被称为 双端队列(deque)

作用于数组末端的方法:

  • pop

    取出并返回数组的最后一个元素:let fruits = ["Apple", "Orange", "Pear"]; alert( fruits.pop() ); // 移除 "Pear" 然后 alert 显示出来 alert( fruits ); // Apple, Orange

  • push

    在数组末端添加元素:let fruits = ["Apple", "Orange"]; fruits.push("Pear"); alert( fruits ); // Apple, Orange, Pear调用 fruits.push(...)fruits[fruits.length] = ... 是一样的。

作用于数组首端的方法:

  • shift

    取出数组的第一个元素并返回它:let fruits = ["Apple", "Orange", "Pear"]; alert( fruits.shift() ); // 移除 Apple 然后 alert 显示出来 alert( fruits ); // Orange, Pear

  • unshift

    在数组的首端添加元素:let fruits = ["Orange", "Pear"]; fruits.unshift('Apple'); alert( fruits ); // Apple, Orange, Pear

pushunshift 方法都可以一次添加多个元素:

let fruits = ["Apple"];

fruits.push("Orange", "Peach");
fruits.unshift("Pineapple", "Lemon");

// ["Pineapple", "Lemon", "Apple", "Orange", "Peach"]
alert( fruits );

数组是一种特殊的对象。使用方括号来访问属性 arr[0] 实际上是来自于对象的语法。它其实与 obj[key]相同,其中 arr 是对象,而数字用作键(key)。

数组误用的几种方式:

  • 添加一个非数字的属性,比如 arr.test = 5
  • 制造空洞,比如:添加 arr[0],然后添加 arr[1000] (它们中间什么都没有)。
  • 以倒序填充数组,比如 arr[1000]arr[999] 等等。

请将数组视为作用于 有序数据 的特殊结构。它们为此提供了特殊的方法。数组在 JavaScript 引擎内部是经过特殊调整的,使得更好地作用于连续的有序数据,所以请以正确的方式使用数组。如果你需要任意键值,那很有可能实际上你需要的是常规对象 {}

push/pop 方法运行的比较快,而 shift/unshift 比较慢。

为什么作用于数组的末端会比首端快呢?让我们看看在执行期间都发生了什么:

fruits.shift(); // 从首端取出一个元素

只获取并移除数字 0 对应的元素是不够的。其它元素也需要被重新编号。

shift 操作必须做三件事:

  1. 移除索引为 0 的元素。
  2. 把所有的元素向左移动,把索引 1 改成 02 改成 1 以此类推,对其重新编号。
  3. 更新 length 属性。
循环

但对于数组来说还有另一种循环方式,for..of

let fruits = ["Apple", "Orange", "Plum"];

// 遍历数组元素
for (let fruit of fruits) {
  alert( fruit );
}

for..of 不能获取当前元素的索引,只是获取元素值,但大多数情况是够用的。而且这样写更短。

技术上来讲,因为数组也是对象,所以使用 for..in 也是可以的:

let arr = ["Apple", "Orange", "Pear"];

for (let key in arr) {
  alert( arr[key] ); // Apple, Orange, Pear
}

但这其实是一个很不好的想法。会有一些潜在问题存在:

  1. for..in 循环会遍历 所有属性,不仅仅是这些数字属性。

    在浏览器和其它环境中有一种称为“类数组”的对象,它们 看似是数组。也就是说,它们有 length 和索引属性,但是也可能有其它的非数字的属性和方法,这通常是我们不需要的。for..in 循环会把它们都列出来。所以如果我们需要处理类数组对象,这些“额外”的属性就会存在问题。

  2. for..in 循环适用于普通对象,并且做了对应的优化。但是不适用于数组,因此速度要慢 10-100 倍。当然即使是这样也依然非常快。只有在遇到瓶颈时可能会有问题。但是我们仍然应该了解这其中的不同。

通常来说,我们不应该用 for..in 来处理数组。

关于 “length”

当我们修改数组的时候,length 属性会自动更新。准确来说,它实际上不是数组里元素的个数,而是最大的数字索引值加一。

例如,一个数组只有一个元素,但是这个元素的索引值很大,那么这个数组的 length 也会很大:

let fruits = [];
fruits[123] = "Apple";

alert( fruits.length ); // 124

要知道的是我们通常不会这样使用数组。

length 属性的另一个有意思的点是它是可写的。

如果我们手动增加它,则不会发生任何有趣的事儿。但是如果我们减少它,数组就会被截断。该过程是不可逆的,下面是例子:

let arr = [1, 2, 3, 4, 5];

arr.length = 2; // 截断到只剩 2 个元素
alert( arr ); // [1, 2]

arr.length = 5; // 又把 length 加回来
alert( arr[3] ); // undefined:被截断的那些数值并没有回来

所以,清空数组最简单的方法就是:arr.length = 0;

数组方法

splice slice

arr.splice(str) 方法可以说是处理数组的瑞士军刀。它可以做所有事情:添加,删除和插入元素。

语法是:

arr.splice(index[, deleteCount, elem1, ..., elemN])

index 开始:删除 deleteCount 个元素并在当前位置插入 elem1, ..., elemN。最后返回已删除元素的数组。

通过例子我们可以很容易地掌握这个方法。

让我们从删除开始:

let arr = ["I", "study", "JavaScript"];

arr.splice(1, 1); // 从索引 1 开始删除 1 个元素

alert( arr ); // ["I", "JavaScript"]

简单,对吧?从索引 1 开始删除 1 个元素。

在下一个例子中,我们删除了 3 个元素,并用另外两个元素替换它们:

let arr = ["I", "study", "JavaScript", "right", "now"];

// remove 3 first elements and replace them with another
arr.splice(0, 3, "Let's", "dance");

alert( arr ) // now ["Let's", "dance", "right", "now"]

在这里我们可以看到 splice 返回了已删除元素的数组:

let arr = ["I", "study", "JavaScript", "right", "now"];

// 删除前两个元素
let removed = arr.splice(0, 2);

alert( removed ); // "I", "study" <-- 被从数组中删除了的元素

我们可以将 deleteCount 设置为 0splice 方法就能够插入元素而不用删除任何元素:

let arr = ["I", "study", "JavaScript"];

// 从索引 2 开始
// 删除 0 个元素
// 然后插入 "complex" 和 "language"
arr.splice(2, 0, "complex", "language");

alert( arr ); // "I", "study", "complex", "language", "JavaScript"

允许负向索引

在这里和其他数组方法中,负向索引都是被允许的。它们从数组末尾计算位置,如下所示:

let arr = [1, 2, 5];

// 从索引 -1(尾端前一位)
// 删除 0 个元素,
// 然后插入 3 和 4
arr.splice(-1, 0, 3, 4);

alert( arr ); // 1,2,3,4,5

arr.slice 方法比 arr.splice 简单得多。

语法是:

arr.slice([start], [end])

它会返回一个新数组,将所有从索引 startend(不包括 end)的数组项复制到一个新的数组。startend 都可以是负数,在这种情况下,从末尾计算索引。

它和字符串的 str.slice 方法有点像,就是把子字符串替换成子数组。

例如:

let arr = ["t", "e", "s", "t"];

alert( arr.slice(1, 3) ); // e,s(复制从位置 1 到位置 3 的元素)

alert( arr.slice(-2) ); // s,t(复制从位置 -2 到尾端的元素)

我们也可以不带参数地调用它:arr.slice() 会创建一个 arr 的副本。其通常用于获取副本,以进行不影响原始数组的进一步转换。

concht

arr.concat 创建一个新数组,其中包含来自于其他数组和其他项的值。

语法:

arr.concat(arg1, arg2...)

它接受任意数量的参数 —— 数组或值都可以。

结果是一个包含来自于 arr,然后是 arg1arg2 的元素的新数组。

如果参数 argN 是一个数组,那么其中的所有元素都会被复制。否则,将复制参数本身。

例如:

let arr = [1, 2];

// create an array from: arr and [3,4]
alert( arr.concat([3, 4]) ); // 1,2,3,4

// create an array from: arr and [3,4] and [5,6]
alert( arr.concat([3, 4], [5, 6]) ); // 1,2,3,4,5,6

// create an array from: arr and [3,4], then add values 5 and 6
alert( arr.concat([3, 4], 5, 6) ); // 1,2,3,4,5,6

通常,它只复制数组中的元素。其他对象,即使它们看起来像数组一样,但仍然会被作为一个整体添加:

let arr = [1, 2];

let arrayLike = {
  0: "something",
  length: 1
};

alert( arr.concat(arrayLike) ); // 1,2,[object Object]

……但是,如果类似数组的对象具有 Symbol.isConcatSpreadable 属性,那么它就会被 concat 当作一个数组来处理:此对象中的元素将被添加:

let arr = [1, 2];

let arrayLike = {
  0: "something",
  1: "else",
  [Symbol.isConcatSpreadable]: true,
  length: 2
};

alert( arr.concat(arrayLike) ); // 1,2,something,else
forEach

arr.forEach 方法允许为数组的每个元素都运行一个函数。

语法:

arr.forEach(function(item, index, array) {
  // ... do something with item
});

例如,下面这个程序显示了数组的每个元素:

// 对每个元素调用 alert
["Bilbo", "Gandalf", "Nazgul"].forEach(alert);

而这段代码更详细地介绍了它们在目标数组中的位置:

["Bilbo", "Gandalf", "Nazgul"].forEach((item, index, array) => {
  alert(`${item} is at index ${index} in ${array}`);
});

该函数的结果(如果它有返回)会被抛弃和忽略。

indexOf/lastIndexOf 和 includes

arr.indexOfarr.lastIndexOfarr.includes 方法与字符串操作具有相同的语法,并且作用基本上也与字符串的方法相同,只不过这里是对数组元素而不是字符进行操作:

  • arr.indexOf(item, from) 从索引 from 开始搜索 item,如果找到则返回索引,否则返回 -1
  • arr.lastIndexOf(item, from) —— 和上面相同,只是从右向左搜索。
  • arr.includes(item, from) —— 从索引 from 开始搜索 item,如果找到则返回 true(译注:如果没找到,则返回 false)。

例如:

let arr = [1, 0, false];

alert( arr.indexOf(0) ); // 1
alert( arr.indexOf(false) ); // 2
alert( arr.indexOf(null) ); // -1

alert( arr.includes(1) ); // true

请注意,这些方法使用的是严格相等 === 比较。所以如果我们搜索 false,会精确到的确是 false 而不是数字 0

如果我们想检查是否包含某个元素,并且不想知道确切的索引,那么 arr.includes 是首选。

此外,includes 的一个非常小的差别是它能正确处理NaN,而不像 indexOf/lastIndexOf

const arr = [NaN];
alert( arr.indexOf(NaN) ); // -1(应该为 0,但是严格相等 === equality 对 NaN 无效)
alert( arr.includes(NaN) );// true(这个结果是对的)
find 和 findIndex

想象一下,我们有一个对象数组。我们如何找到具有特定条件的对象?

这时可以用 arr.find 方法。

语法如下:

let result = arr.find(function(item, index, array) {
  // 如果返回 true,则返回 item 并停止迭代
  // 对于 falsy 则返回 undefined
});

依次对数组中的每个元素调用该函数:

  • item 是元素。
  • index 是它的索引。
  • array 是数组本身。

如果它返回 true,则搜索停止,并返回 item。如果没有搜索到,则返回 undefined

例如,我们有一个存储用户的数组,每个用户都有 idname 字段。让我们找到 id == 1 的那个用户:

let users = [
  {id: 1, name: "John"},
  {id: 2, name: "Pete"},
  {id: 3, name: "Mary"}
];

let user = users.find(item => item.id == 1);

alert(user.name); // John

在现实生活中,对象数组是很常见的,所以 find 方法非常有用。

注意在这个例子中,我们传给了 find 一个单参数函数 item => item.id == 1。这很典型,并且 find方法的其他参数很少使用。

arr.findIndex 方法(与 arr.find 方法)基本上是一样的,但它返回找到元素的索引,而不是元素本身。并且在未找到任何内容时返回 -1

filter

find 方法搜索的是使函数返回 true 的第一个(单个)元素。

如果需要匹配的有很多,我们可以使用 arr.filter(fn)

语法与 find 大致相同,但是 filter 返回的是所有匹配元素组成的数组:

let results = arr.filter(function(item, index, array) {
  // 如果 true item 被 push 到 results,迭代继续
  // 如果什么都没找到,则返回空数组
});

例如:

let users = [
  {id: 1, name: "John"},
  {id: 2, name: "Pete"},
  {id: 3, name: "Mary"}
];

// 返回前两个用户的数组
let someUsers = users.filter(item => item.id < 3);

alert(someUsers.length); // 2
map

arr.map 方法是最有用和经常使用的方法之一。

它对数组的每个元素都调用函数,并返回结果数组。

语法:

let result = arr.map(function(item, index, array) {
  // 返回新值而不是当前元素
})

例如,在这里我们将每个元素转换为它的字符串长度:

let lengths = ["Bilbo", "Gandalf", "Nazgul"].map(item => item.length);
alert(lengths); // 5,7,6
sort

arr.sort 方法对数组进行 原位(in-place) 排序,更改元素的顺序。(译注:原位是指在此数组内,而非生成一个新数组。)

它还返回排序后的数组,但是返回值通常会被忽略,因为修改了 arr 本身。

语法:

let arr = [ 1, 2, 15 ];

// 该方法重新排列 arr 的内容
arr.sort();

alert( arr );  // 1, 15, 2

你有没有注意到结果有什么奇怪的地方?

顺序变成了 1, 15, 2。不对,但为什么呢?

这些元素默认情况下被按字符串进行排序。

从字面上看,所有元素都被转换为字符串,然后进行比较。对于字符串,按照词典顺序进行排序,实际上应该是 "2" > "15"

要使用我们自己的排序顺序,我们需要提供一个函数作为 arr.sort() 的参数。

该函数应该比较两个任意值并返回:

function compare(a, b) {
  if (a > b) return 1; // 如果第一个值比第二个值大
  if (a == b) return 0; // 如果两个值相等
  if (a < b) return -1; // 如果第一个值比第二个值小
}

例如,按数字进行排序:

function compareNumeric(a, b) {
  if (a > b) return 1;
  if (a == b) return 0;
  if (a < b) return -1;
}

let arr = [ 1, 2, 15 ];

arr.sort(compareNumeric);

alert(arr);  // 1, 2, 15

现在结果符合预期了。

我们思考一下这儿发生了什么。arr 可以是由任何内容组成的数组,对吗?它可能包含数字、字符串、对象或其他任何内容。我们有一组 一些元素。要对其进行排序,我们需要一个 排序函数 来确认如何比较这些元素。默认是按字符串进行排序的。

arr.sort(fn) 方法实现了通用的排序算法。我们不需要关心它的内部工作原理(大多数情况下都是经过 快速排序 算法优化的)。它将遍历数组,使用提供的函数比较其元素并对其重新排序,我们所需要的就是提供执行比较的函数 fn

顺便说一句,如果我们想知道要比较哪些元素 —— 那么什么都不会阻止 alert 它们:

[1, -2, 15, 2, 0, 8].sort(function(a, b) {
  alert( a + " <> " + b );
});

该算法可以在此过程中,将一个元素与多个其他元素进行比较,但是它会尝试进行尽可能少的比较。

比较函数可以返回任何数字

实际上,比较函数只需要返回一个正数表示“大于”,一个负数表示“小于”。

通过这个原理我们可以编写更短的函数:

let arr = [ 1, 2, 15 ];

arr.sort(function(a, b) { return a - b; });

alert(arr);  // 1, 2, 15

箭头函数最好

你还记得 箭头函数 吗?这里使用箭头函数会更加简洁:

arr.sort( (a, b) => a - b );

这与上面更长的版本完全相同。

使用 localeCompare for strings

你记得 字符串比较 算法吗?默认情况下,它通过字母的代码比较字母。

对于许多字母,最好使用 str.localeCompare 方法正确地对字母进行排序,例如 Ö

例如,让我们用德语对几个国家/地区进行排序:

let countries = ['Österreich', 'Andorra', 'Vietnam'];

alert( countries.sort( (a, b) => a > b ? 1 : -1) ); // Andorra, Vietnam, Österreich(错的)

alert( countries.sort( (a, b) => a.localeCompare(b) ) ); // Andorra,Österreich,Vietnam(对的!)
split 和 join

arr.reverse 方法用于颠倒 arr 中元素的顺序。 例如:

let arr = [1, 2, 3, 4, 5];
arr.reverse();

alert( arr ); // 5,4,3,2,1

它也会返回颠倒后的数组 arr

举一个现实生活场景的例子。我们正在编写一个消息应用程序,并且该人员输入以逗号分隔的接收者列表:John, Pete, Mary。但对我们来说,名字数组比单个字符串舒适得多。怎么做才能获得这样的数组呢?

str.split(delim) 方法可以做到。它通过给定的分隔符 delim 将字符串分割成一个数组。

在下面的例子中,我们用“逗号后跟着一个空格”作为分隔符:

let names = 'Bilbo, Gandalf, Nazgul';

let arr = names.split(', ');

for (let name of arr) {
  alert( `A message to ${name}.` ); // A message to Bilbo(和其他名字)
}

split 方法有一个可选的第二个数字参数 —— 对数组长度的限制。如果提供了,那么额外的元素会被忽略。但实际上它很少使用:

let arr = 'Bilbo, Gandalf, Nazgul, Saruman'.split(', ', 2);

alert(arr); // Bilbo, Gandalf

拆分为字母

调用带有空参数 ssplit(s),会将字符串拆分为字母数组:

let str = "test";

alert( str.split('') ); // t,e,s,t

arr.join(glue)split 相反。它会在它们之间创建一串由 glue 粘合的 arr 项。

例如:

let arr = ['Bilbo', 'Gandalf', 'Nazgul'];

let str = arr.join(';'); // 使用分号 ; 将数组粘合成字符串

alert( str ); // Bilbo;Gandalf;Nazgul
reduce/reduceRight

建议看原文,比较有用

Array.isArray

数组是基于对象的,不构成单独的语言类型。

所以 typeof 不能帮助从数组中区分出普通对象:

alert(typeof {}); // object
alert(typeof []); // same

……但是数组经常被使用,因此有一种特殊的方法用于判断:Array.isArray(value)。如果 value 是一个数组,则返回 true;否则返回 false

alert(Array.isArray({})); // false

alert(Array.isArray([])); // true
数组练习题

重要程度: 5

编写函数 camelize(str) 将诸如 “my-short-string” 之类的由短划线分隔的单词变成骆驼式的 “myShortString”。

即:删除所有短横线,并将短横线后的每一个单词的首字母变为大写。

示例:

camelize("background-color") == 'backgroundColor';
camelize("list-style-image") == 'listStyleImage';
camelize("-webkit-transition") == 'WebkitTransition';

提示:使用 split 将字符串拆分成数组,对其进行转换之后再 join 回来。

打开带有测试的沙箱。

解决方案

function camelize(str) {
  return str
    .split('-') // splits 'my-long-word' into array ['my', 'long', 'word']
    .map(
      // capitalizes first letters of all array items except the first one
      // converts ['my', 'long', 'word'] into ['my', 'Long', 'Word']
      (word, index) => index == 0 ? word : word[0].toUpperCase() + word.slice(1)
    )
    .join(''); // joins ['my', 'Long', 'Word'] into 'myLongWord'
}

使用沙箱的测试功能打开解决方案。

写一个函数 filterRange(arr, a, b),该函数获取一个数组 arr,在其中查找数值大小在 ab 之间的元素,并返回它们的数组。

该函数不应该修改原数组。它应该返回新的数组。

例如:

let arr = [5, 3, 8, 1];

let filtered = filterRange(arr, 1, 4);

alert( filtered ); // 3,1(匹配值)

alert( arr ); // 5,3,8,1(未修改)

打开带有测试的沙箱。

解决方案

function filterRange(arr, a, b) {
  // 在表达式周围添加了括号,以提高可读性
  return arr.filter(item => (a <= item && item <= b));
}

let arr = [5, 3, 8, 1];

let filtered = filterRange(arr, 1, 4);

alert( filtered ); // 3,1(匹配的值)

alert( arr ); // 5,3,8,1(未经改动的数组中的值)

使用沙箱的测试功能打开解决方案。

写一个函数 filterRangeInPlace(arr, a, b),该函数获取一个数组 arr,并删除其中介于 ab 区间以外的所有值。检查:a ≤ arr[i] ≤ b

该函数应该只修改数组。它不应该返回任何东西。

例如:

let arr = [5, 3, 8, 1];

filterRangeInPlace(arr, 1, 4); // 删除了范围在 1 到 4 之外的所有值

alert( arr ); // [3, 1]

打开带有测试的沙箱。

解决方案

function filterRangeInPlace(arr, a, b) {

  for (let i = 0; i < arr.length; i++) {
    let val = arr[i];

    // 如果超出范围,则删除
    if (val < a || val > b) {
      arr.splice(i, 1);
      i--;
    }
  }

}

let arr = [5, 3, 8, 1];

filterRangeInPlace(arr, 1, 4); // 删除 1 到 4 范围之外的值

alert( arr ); // [3, 1]

使用沙箱的测试功能打开解决方案。

let arr = [5, 2, 1, -10, 8];

// ……你的代码以降序对其进行排序

alert( arr ); // 8, 5, 2, 1, -10

解决方案

let arr = [5, 2, 1, -10, 8];

arr.sort((a, b) => b - a);

alert( arr );

创建一个构造函数 Calculator,以创建“可扩展”的 calculator 对象。

该任务由两部分组成。

  1. 首先,实现 calculate(str) 方法,该方法接受像 "1 + 2" 这样格式为“数字 运算符 数字”(以空格分隔)的字符串,并返回结果。该方法需要能够理解加号 + 和减号 -

    用法示例:

    let calc = new Calculator;
    
    alert( calc.calculate("3 + 7") ); // 10
    
  2. 然后添加方法 addMethod(name, func),该方法教 calculator 进行新操作。它需要运算符 name 和实现它的双参数函数 func(a,b)

    例如,我们添加乘法 *,除法 / 和求幂 **

    let powerCalc = new Calculator;
    powerCalc.addMethod("*", (a, b) => a * b);
    powerCalc.addMethod("/", (a, b) => a / b);
    powerCalc.addMethod("**", (a, b) => a ** b);
    
    let result = powerCalc.calculate("2 ** 3");
    alert( result ); // 8
    
  • 此任务中没有括号或复杂的表达式。
  • 数字和运算符之间只有一个空格。
  • 你可以自行选择是否添加错误处理功能。

打开带有测试的沙箱。

解决方案

  • 请注意方法的存储方式。它们只是被添加到 this.methods 属性中。
  • 所有检测和数字转换都通过 calculate 方法完成。将来可能会扩展它以支持更复杂的表达式。
function Calculator() {

  this.methods = {
    "-": (a, b) => a - b,
    "+": (a, b) => a + b
  };

  this.calculate = function(str) {

    let split = str.split(' '),
      a = +split[0],
      op = split[1],
      b = +split[2]

    if (!this.methods[op] || isNaN(a) || isNaN(b)) {
      return NaN;
    }

    return this.methods[op](a, b);
  }

  this.addMethod = function(name, func) {
    this.methods[name] = func;
  };
}

使用沙箱的测试功能打开解决方案。

你有一个 user 对象数组,每个对象都有 user.name。编写将其转换为 names 数组的代码。

例如:

let john = { name: "John", age: 25 };
let pete = { name: "Pete", age: 30 };
let mary = { name: "Mary", age: 28 };

let users = [ john, pete, mary ];

let names = /* ... your code */

alert( names ); // John, Pete, Mary

解决方案

let john = { name: "John", age: 25 };
let pete = { name: "Pete", age: 30 };
let mary = { name: "Mary", age: 28 };

let users = [ john, pete, mary ];

let names = users.map(item => item.name);

alert( names ); // John, Pete, Mary

你有一个 user 对象数组,每个对象都有 namesurnameid

编写代码以该数组为基础,创建另一个具有 idfullName 的对象数组,其中 fullNamenamesurname 生成。

例如:

let john = { name: "John", surname: "Smith", id: 1 };
let pete = { name: "Pete", surname: "Hunt", id: 2 };
let mary = { name: "Mary", surname: "Key", id: 3 };

let users = [ john, pete, mary ];

let usersMapped = /* ... your code ... */

/*
usersMapped = [
  { fullName: "John Smith", id: 1 },
  { fullName: "Pete Hunt", id: 2 },
  { fullName: "Mary Key", id: 3 }
]
*/

alert( usersMapped[0].id ) // 1
alert( usersMapped[0].fullName ) // John Smith

所以,实际上你需要将一个对象数组映射到另一个对象数组。在这儿尝试使用箭头函数 => 来编写。

解决方案

let john = { name: "John", surname: "Smith", id: 1 };
let pete = { name: "Pete", surname: "Hunt", id: 2 };
let mary = { name: "Mary", surname: "Key", id: 3 };

let users = [ john, pete, mary ];

let usersMapped = users.map(user => ({
  fullName: `${user.name} ${user.surname}`,
  id: user.id
}));

/*
usersMapped = [
  { fullName: "John Smith", id: 1 },
  { fullName: "Pete Hunt", id: 2 },
  { fullName: "Mary Key", id: 3 }
]
*/

alert( usersMapped[0].id ); // 1
alert( usersMapped[0].fullName ); // John Smith

请注意,在箭头函数中,我们需要使用额外的括号。

我们不能这样写:

let usersMapped = users.map(user => {
  fullName: `${user.name} ${user.surname}`,
  id: user.id
});

我们记得,有两种箭头函数的写法:直接返回值 value => expr 和带主体的 value => {...}

JavaScript 在这里会把 { 视为函数体的开始,而不是对象的开始。解决方法是将它们包装在普通括号 () 中:

let usersMapped = users.map(user => ({
  fullName: `${user.name} ${user.surname}`,
  id: user.id
}));

这样就可以了。

编写函数 sortByAge(users) 获得对象数组的 age 属性,并根据 age 对这些对象数组进行排序。

例如:

let john = { name: "John", age: 25 };
let pete = { name: "Pete", age: 30 };
let mary = { name: "Mary", age: 28 };

let arr = [ pete, john, mary ];

sortByAge(arr);

// now: [john, mary, pete]
alert(arr[0].name); // John
alert(arr[1].name); // Mary
alert(arr[2].name); // Pete

解决方案

function sortByAge(arr) {
  arr.sort((a, b) => a.age > b.age ? 1 : -1);
}

let john = { name: "John", age: 25 };
let pete = { name: "Pete", age: 30 };
let mary = { name: "Mary", age: 28 };

let arr = [ pete, john, mary ];

sortByAge(arr);

// 排序后的数组为:[john, mary, pete]
alert(arr[0].name); // John
alert(arr[1].name); // Mary
alert(arr[2].name); // Pete

译注:解决方案的代码还可以更短一些

function sortByAge(arr) {
  arr.sort((a, b) => a.age - b.age);
}

因为 sort() 方法的语法为 arr.sort([compareFunction]),如果没有指明 compareFunction,那么元素会被按照转换为的字符串的诸个字符的 Unicode 编码进行排序,如果指明了 compareFunction,那么数组会按照调用该函数的返回值排序。即 ab 是两个将要被比较的元素:

  • 如果 compareFunction(a, b) 小于 0,那么 a 会被排列到 b 之前;
  • 如果 compareFunction(a, b) 等于 0,那么 ab 的相对位置不变。备注:ECMAScript 标准并不保证这一行为,而且也不是所有浏览器都会遵守(例如 Mozilla 在 2003 年之前的版本);
  • 如果 compareFunction(a, b) 大于 0,那么 b 会被排列到 a 之前。

因此,升序排列的函数可以简写为:(a, b) => a.age - b.age

让我们先遍历数字:

  • 对于每个元素,我们将检查结果数组是否已经有该元素。
  • 如果有,则忽略,否则将其添加到结果中。
function unique(arr) {
  let result = [];

  for (let str of arr) {
    if (!result.includes(str)) {
      result.push(str);
    }
  }

  return result;
}

let strings = ["Hare", "Krishna", "Hare", "Krishna",
  "Krishna", "Krishna", "Hare", "Hare", ":-O"
];

alert( unique(strings) ); // Hare, Krishna, :-O

代码有效,但其中存在潜在的性能问题。

方法 result.includes(str) 在内部遍历数组 result,并将每个元素与 str 进行比较以找到匹配项。

所以如果 result 中有 100 个元素,并且没有任何一项与 str 匹配,那么它将遍历整个 result并进行 100 次比较。如果 result 很大,比如 10000,那么就会有 10000 次的比较。

这本身并不是问题,因为 JavaScript 引擎速度非常快,所以遍历一个有 10000 个元素的数组只需要几微秒。

但是我们在 for循环中对 arr 的每个元素都进行了一次检测。

因此,如果 arr.length10000,我们会有 10000 * 10000 = 1 亿次的比较。那真的太多了。

所以该解决方案仅适用于小型数组。

进一步,在后面的 Map and Set(映射和集合) 一章中,我们将看到如何对该方法进行优化。

使用沙箱的测试功能打开解决方案。

假设我们收到了一个用户数组,形式为:{id:..., name:..., age... }

创建一个函数 groupById(arr) 从该数组创建对象,以 id 为键(key),数组项为值。

例如:

let users = [
  {id: 'john', name: "John Smith", age: 20},
  {id: 'ann', name: "Ann Smith", age: 24},
  {id: 'pete', name: "Pete Peterson", age: 31},
];

let usersById = groupById(users);

/*
// 调用函数后,我们应该得到:

usersById = {
  john: {id: 'john', name: "John Smith", age: 20},
  ann: {id: 'ann', name: "Ann Smith", age: 24},
  pete: {id: 'pete', name: "Pete Peterson", age: 31},
}
*/

处理服务端数据时,这个函数很有用。

在这个任务里我们假设 id 是唯一的。没有两个具有相同 id 的数组项。

请在解决方案中使用数组的 .reduce 方法。

打开带有测试的沙箱。

解决方案

function groupById(array) {
  return array.reduce((obj, value) => {
    obj[value.id] = value;
    return obj;
  }, {})
}

[使用沙箱的测试功能打开解决方案。]