本博客内容均摘录自现代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(一样,大小写没影响)
二进制和八进制数字系统很少使用,但也支持使用 0b 和 0o 前缀:
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 的范围可以从 2 到 36。默认情况下是 10。
常见的用例如下:
-
base=16 用于十六进制颜色,字符编码等,数字可以是
0..9或A..F。 -
base=2 主要用于调试按位操作,数字可以是
0或1。 -
base=36 是最大进制,数字可以是
0..9或A..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变成3,3.6变成4,-1.1变成-1。 -
Math.trunc(IE 浏览器不支持这个方法)移除小数点后的所有内容而没有舍入:
3.1变成3,-1.1变成-1。
这个是总结它们之间差异的表格:
Math.floor | Math.ceil | Math.round | Math.trunc | |
|---|---|---|---|---|
3.1 | 3 | 4 | 3 | 3 |
3.6 | 3 | 4 | 4 | 3 |
-1.1 | -2 | -1 | -1 | -1 |
-1.6 | -2 | -1 | -2 | -1 |
这些函数涵盖了处理数字小数部分的所有可能方法。但是,如果我们想将数字舍入到小数点后 n 位,该怎么办?
例如,我们有 1.2345,并且想把它舍入到小数点后两位,仅得到 1.23。
有两种方式可以实现这个需求:
-
乘除法
例如,要将数字舍入到小数点后两位,我们可以将数字乘以
100(或更大的 10 的整数次幂),调用舍入函数,然后再将其除回。let num = 1.23456; alert( Math.floor(num * 100) / 100 ); // 1.23456 -> 123.456 -> 123 -> 1.23 -
函数 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.1 和 0.2 的总和是否为 0.3,我们会得到 false。
奇了怪了!如果不是 0.3,那能是啥?
alert( 0.1 + 0.2 ); // 0.30000000000000004
哎哟!这个错误比不正确的比较的后果更严重。想象一下,你创建了一个电子购物网站,如果访问者将价格为 ¥ 0.10 和 ¥ 0.20 的商品放入了他的购物车。订单总额将是 ¥ 0.30000000000000004。这会让任何人感到惊讶。
但为什么会这样呢?
一个数字以其二进制的形式存储在内存中,一个 1 和 0 的序列。但是在十进制数字系统中看起来很简单的 0.1,0.2 这样的小数,实际上在二进制形式中是无限循环小数。
换句话说,什么是 0.1?0.1 就是 1 除以 10,1/10,即十分之一。在十进制数字系统中,这样的数字表示起来很容易。将其与三分之一进行比较:1/3。三分之一变成了无限循环小数 0.33333(3)。
在十进制数字系统中,可以保证以 10 的整数次幂作为除数能够正常工作,但是以 3 作为除数则不能。也是同样的原因,在二进制数字系统中,可以保证以 2 的整数次幂作为除数时能够正常工作,但 1/10 就变成了一个无限循环的二进制小数。
使用二进制数字系统无法 精确 存储 0.1 或 0.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,它类似于 === 一样对值进行比较,但它对于两种边缘情况更可靠:
- 它适用于
NaN:Object.is(NaN,NaN)=== true,这是件好事。 - 值
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€",并希望从中提取出一个数值。
这就是 parseInt 和 parseFloat 的作用。
它们可以从字符串中“读取”数字,直到无法读取为止。如果发生 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.startsWith 和 str.endsWith 的功能与其名称所表示的意思相同:
alert( "Widget".startsWith("Wid") ); // true,"Widget" 以 "Wid" 开始
alert( "Widget".endsWith("get") ); // true,"Widget" 以 "get" 结束
JavaScript 中有三种获取字符串的方法:substring、substr 和 slice。
-
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])返回字符串在
start和end之间 的部分。这与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) | 从 start 到 end(不含 end) | 允许 |
substring(start, end) | start 与 end 之间(包括 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
push 和 unshift 方法都可以一次添加多个元素:
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 操作必须做三件事:
- 移除索引为
0的元素。 - 把所有的元素向左移动,把索引
1改成0,2改成1以此类推,对其重新编号。 - 更新
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
}
但这其实是一个很不好的想法。会有一些潜在问题存在:
-
for..in循环会遍历 所有属性,不仅仅是这些数字属性。在浏览器和其它环境中有一种称为“类数组”的对象,它们 看似是数组。也就是说,它们有
length和索引属性,但是也可能有其它的非数字的属性和方法,这通常是我们不需要的。for..in循环会把它们都列出来。所以如果我们需要处理类数组对象,这些“额外”的属性就会存在问题。 -
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 设置为 0,splice 方法就能够插入元素而不用删除任何元素:
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])
它会返回一个新数组,将所有从索引 start 到 end(不包括 end)的数组项复制到一个新的数组。start和 end 都可以是负数,在这种情况下,从末尾计算索引。
它和字符串的 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,然后是 arg1,arg2 的元素的新数组。
如果参数 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.indexOf、arr.lastIndexOf 和 arr.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。
例如,我们有一个存储用户的数组,每个用户都有 id 和 name 字段。让我们找到 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
拆分为字母
调用带有空参数 s 的 split(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,在其中查找数值大小在 a 和 b 之间的元素,并返回它们的数组。
该函数不应该修改原数组。它应该返回新的数组。
例如:
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,并删除其中介于 a 和 b 区间以外的所有值。检查: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 对象。
该任务由两部分组成。
-
首先,实现
calculate(str)方法,该方法接受像"1 + 2"这样格式为“数字 运算符 数字”(以空格分隔)的字符串,并返回结果。该方法需要能够理解加号+和减号-。用法示例:
let calc = new Calculator; alert( calc.calculate("3 + 7") ); // 10 -
然后添加方法
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 对象数组,每个对象都有 name,surname 和 id。
编写代码以该数组为基础,创建另一个具有 id 和 fullName 的对象数组,其中 fullName 由 name 和 surname 生成。
例如:
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,那么数组会按照调用该函数的返回值排序。即 a 和 b 是两个将要被比较的元素:
- 如果
compareFunction(a, b)小于0,那么a会被排列到b之前; - 如果
compareFunction(a, b)等于0,那么a和b的相对位置不变。备注: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.length 是 10000,我们会有 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;
}, {})
}
[使用沙箱的测试功能打开解决方案。]