JavaScript 数据类型
原始方法
JavaScript 允许我们像使用对象一样使用原始类型(字符串,数字等)。
原始类型和对象之间的关键区别:
一个原始值:
- 是原始类型中的一种值。
- 在 JavaScript 中有 7 种原始类型:
string
,number
,bigint
,boolean
,symbol
,null
和undefined
。
一个对象:
- 能够存储多个值作为属性。
- 可以使用大括号
{}
创建对象,例如:{name: "John", age: 30}
。JavaScript 中还有其他种类的对象,例如函数就是对象。
我们可以把一个函数作为对象的属性存储到对象中:
let john = {
name: "John",
sayHi: function() {
alert("Hi buddy!");
}
};
john.sayHi(); // Hi buddy!
当作对象的原始类型
例如,字符串方法 str.toUpperCase() 返回一个大写化处理的字符串。
let str = "Hello";
alert( str.toUpperCase() ); // HELLO
以下是 str.toUpperCase()
中实际发生的情况:
1.字符串 str
是一个原始值。因此,在访问其属性时,会创建一个包含字符串字面值的特殊对象,并且具有有用的方法,例如 toUpperCase()
。
2.该方法运行并返回一个新的字符串(由 alert
显示)。
3.特殊对象被销毁,只留下原始值 str
。
数字有其自己的方法,例如,toFixed(n) 将数字舍入到给定的精度:
let n = 1.23456;
alert( n.toFixed(2) ); // 1.23
构造器 String/Number/Boolean
仅供内部使用
alert( typeof 0 ); // "number"
alert( typeof new Number(0) ); // "object"!
let zero = new Number(0);
if (zero) { // zero 为 true,因为它是一个对象
alert( "zero is truthy?!?" );
}
调用不带 new
(关键字)的 String/Number/Boolean
函数是完全理智和有用的。
let num = Number("123"); // 将字符串转成数字
null/undefined
没有任何方法
总结
- 除
null
和undefined
以外的原始类型都提供了许多有用的方法。我们后面的章节中学习这些内容。 - 从形式上讲,这些方法通过临时对象工作,但 JavaScript 引擎可以很好地调整,以在内部对其进行优化,因此调用它们并不需要太高的成本。
数字类型
在现代 JavaScript 中,数字(number)有两种类型:
- JavaScript 中的常规数字以 64 位的格式 IEEE-754 存储,也被称为“双精度浮点数”。这是我们大多数时候所使用的数字。
- BigInt 数字,用于表示任意长度的整数。有时会需要它们,因为常规数字不能超过
253
或小于-253
。由于仅在少数特殊领域才会用到 BigInt,因此我们在特殊的章节 BigInt 中对其进行了介绍。
let billion = 1e9; // 10 亿,字面意思:数字 1 后面跟 9 个 0
alert( 7.3e9 ); // 73 亿(7,300,000,000)
"e"
把数字乘以 1
后面跟着给定数量的 0 的数字。
十六进制,二进制和八进制数字
十六进制 数字在 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
。
舍入
舍入(rounding)是使用数字时最常用的操作之一。
原值 | 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 |
let num = 1.23456;
alert( Math.floor(num * 100) / 100 );
// 1.23456 -> 123.456 -> 123 -> 1.23
函数 toFixed(n) 将数字舍入到小数点后 n
位,并以字符串形式返回结果。
let num = 12.36;
alert( num.toFixed(1) ); // "12.4"
let num = 12.34;
alert( num.toFixed(5) ); // "12.34000",在结尾添加了 0,以达到小数点后五位
不精确的计算
在内部,数字是以 64 位格式 IEEE-754 表示的,所以正好有 64 位可以存储一个数字:其中 52 位被用于存储这些数字,其中 11 位用于存储小数点的位置(对于整数,它们为零),而 1 位用于符号。
如果一个数字太大,则会溢出 64 位存储,并可能会导致无穷大:
alert( 1e500 ); // Infinity
经常会发生的是,精度的损失。
alert( 0.1 + 0.2 == 0.3 ); // false
alert( 0.1 + 0.2 ); // 0.30000000000000004
原因:
在十进制数字系统中看起来很简单的 0.1,0.2 这样的小数,实际上在二进制形式中是无限循环小数。
使用二进制数字系统无法 精确 存储 0.1 或 0.2,就像没有办法将三分之一存储为十进制小数一样。
IEEE-754 数字格式通过将数字舍入到最接近的可能数字来解决此问题。这些舍入规则通常不允许我们看到“极小的精度损失”,但是它确实存在。
alert( 0.1.toFixed(20) ); // 0.10000000000000000555
当我们对两个数字进行求和时,它们的“精度损失”会叠加起来。
这就是为什么 0.1 + 0.2
不等于 0.3
。
不仅仅是 JavaScript许多其他编程语言也存在同样的问题。
最可靠的方法是借助方法 toFixed(n) 对结果进行舍入:
let sum = 0.1 + 0.2;
alert( sum.toFixed(2) ); // 0.30
// 可以使用一元加号将其强制转换为一个数字
alert( +sum.toFixed(2) ); // 0.3
// Hello!我是一个会自我增加的数字!
alert( 9999999999999999 ); // 显示 10000000000000000
// 有 64 位来表示该数字,其中 52 位可用于存储数字,但这还不够。所以最不重要的数字就消失了。
isFinite 和 isNaN
Infinity
(和-Infinity
)是一个特殊的数值,比任何数值都大(小)。NaN
代表一个 error。
alert( isNaN(NaN) ); // true
alert( isNaN("str") ); // true
alert( NaN === NaN ); // false
alert( isFinite("15") ); // true
alert( isFinite("str") ); // false,因为是一个特殊的值:NaN
alert( isFinite(Infinity) ); // false,因为是一个特殊的值:Infinity
parseInt 和 parseFloat
alert( parseInt('100px') ); // 100
alert( parseFloat('12.5em') ); // 12.5
alert( parseInt('12.3') ); // 12,只有整数部分被返回了
alert( parseFloat('12.3.4') ); // 12.3,在第二个点出停止了读取
alert( parseInt('a123') ); // NaN,第一个符号停止了读取
alert( parseInt('0xff', 16) ); // 255
alert( parseInt('ff', 16) ); // 255,没有 0x 仍然有效
alert( parseInt('2n9c', 36) ); // 123456
其他数学函数
JavaScript 有一个内建的 Math 对象,它包含了一个小型的数学函数和常量库。
alert( Math.random() ); // 0.1234567894322
alert( Math.random() ); // 0.5435252343232
alert( Math.random() ); // ... (任何随机数)
alert( Math.max(3, 5, -10, 0, 1) ); // 5
alert( Math.min(1, 2) ); // 1
alert( Math.pow(2, 10) ); // 2 的 10 次幂 = 1024
Math
对象中还有更多函数和常量,包括三角函数,你可以在 Math 函数文档 中找到这些内容。
总结
要写有很多零的数字:
- 将
"e"
和 0 的数量附加到数字后。就像:123e6
与123
后面接 6 个 0 相同。 "e"
后面的负数将使数字除以 1 后面接着给定数量的零的数字。例如123e-6
表示0.000123
(123
的百万分之一)。
对于不同的数字系统:
- 可以直接在十六进制(
0x
),八进制(0o
)和二进制(0b
)系统中写入数字。 parseInt(str,base)
将字符串str
解析为在给定的base
数字系统中的整数,2 ≤ base ≤ 36
。num.toString(base)
将数字转换为在给定的base
数字系统中的字符串。
要将 12pt
和 100px
之类的值转换为数字:
- 使用
parseInt/parseFloat
进行“软”转换,它从字符串中读取数字,然后返回在发生 error 前可以读取到的值。
小数:
- 使用
Math.floor
,Math.ceil
,Math.trunc
,Math.round
或num.toFixed(precision)
进行舍入。 - 请确保记住使用小数时会损失精度。
更多数学函数:
- 需要时请查看 Math 对象。这个库很小,但是可以满足基本的需求。
字符串
引号(Quotes)
字符串可以包含在单引号、双引号或反引号中:
let single = 'single-quoted';
let double = "double-quoted";
let backticks = `backticks`;
单引号和双引号基本相同。但是,反引号允许我们通过 ${…}
将任何表达式嵌入到字符串中:
function sum(a, b) {
return a + b;
}
alert(`1 + 2 = ${sum(1, 2)}.`); // 1 + 2 = 3.
let guestList = `Guests:
* John
* Pete
* Mary
`;
alert(guestList); // 客人清单,多行
特殊字符
字符 | 描述 |
---|---|
\n |
换行 |
\r |
回车:不单独使用。Windows 文本文件使用两个字符 \r\n 的组合来表示换行。 |
\’ , \” |
引号 |
\ |
反斜线 |
\t |
制表符 |
\b , \f , \v |
退格,换页,垂直标签 —— 为了兼容性,现在已经不使用了。 |
\xXX |
具有给定十六进制 Unicode XX 的 Unicode 字符,例如:‘\x7A’ 和 ‘z’ 相同。 |
\uXXXX |
以 UTF-16 编码的十六进制代码 XXXX 的 unicode 字符,例如 \u00A9 —— 是版权符号 © 的 unicode。它必须正好是 4 个十六进制数字。 |
\u{X…XXXXXX} (1 到 6 个十六进制字符) |
具有给定 UTF-32 编码的 unicode 符号。一些罕见的字符用两个 unicode 符号编码,占用 4 个字节。这样我们就可以插入长代码了。 |
// unicode 示例:
alert( "\u00A9" ); // ©
alert( "\u{20331}" ); // 佫,罕见的中国象形文字(长 unicode)
alert( "\u{1F60D}" ); // 😍,笑脸符号(另一个长 unicode)
字符串长度
length
属性表示字符串长度:
alert( `My\n`.length ); // 3
str.length
是一个数字属性,而不是函数。后面不需要添加括号。
访问字符
let str = `Hello`;
// 第一个字符
alert( str[0] ); // H
alert( str.charAt(0) ); // H
// 最后一个字符
alert( str[str.length - 1] ); // o
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" 等)
}
// JavaScript 中,字符串不可更改。
let str = 'Hi';
str[0] = 'h'; // error
alert( str[0] ); // 无法运行
let str = 'Hi';
str = 'h' + str[1]; // 替换字符串
alert( str ); // hi
改变大小写
toLowerCase() 和 toUpperCase() 方法可以改变大小写:
alert( 'Interface'.toUpperCase() ); // INTERFACE
alert( 'Interface'.toLowerCase() ); // interface
alert( 'Interface'[0].toLowerCase() ); // 'i'
查找子字符串
let str = 'Widget with id';
alert( str.indexOf('Widget') ); // 0,因为 'Widget' 一开始就被找到
alert( str.indexOf('widget') ); // -1,没有找到,检索是大小写敏感的
alert( str.indexOf("id") ); // 1,"id" 在位置 1 处(……idget 和 id)
let str = 'Widget with id';
// 我们从 下标 2 开始检索:
alert( str.indexOf('id', 2) ) // 12
let str = 'As sly as a fox, as strong as an ox';
let target = 'as'; // 这是我们要查找的目标
let pos = 0;
while (true) {
let foundPos = str.indexOf(target, pos);
if (foundPos == -1) break;
alert( `Found at ${foundPos}` );
pos = foundPos + 1; // 继续从下一个位置查找
}
let str = "As sly as a fox, as strong as an ox";
let target = "as";
let pos = -1;
while ((pos = str.indexOf(target, pos + 1)) != -1) {
alert( pos );
}
let str = "Widget with id";
if (str.indexOf("Widget") != -1) {
alert("We found it"); // 现在工作了!
}
// 对于 32-bit 整数,~n 等于 -(n+1)。
let str = "Widget";
if (~str.indexOf("Widget")) {
alert( '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
alert( "Midget".includes("id") ); // true
alert( "Midget".includes("id", 3) ); // false, 从位置 3 开始没有 "id"
alert( "Widget".startsWith("Wid") ); // true,"Widget" 以 "Wid" 开始
alert( "Widget".endsWith("get") ); // true,"Widget" 以 "get" 结束
获取子字符串
JavaScript 中有三种获取字符串的方法:substring
、substr
和 slice
。
let str = "stringify";
alert( str.slice(0, 5) ); // 'strin',从 0 到 5 的子字符串(不包括 5)
alert( str.slice(0, 1) ); // 's',从 0 到 1,但不包括 1,所以只有在 0 处的字符
let str = "stringify";
alert( str.slice(2) ); // 从第二个位置直到结束
let str = "stringify";
// 从右边的第四个位置开始,在右边的第一个位置结束
alert( str.slice(-4, -1) ); // 'gif'
let str = "stringify";
// 这些对于 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) ); // ""(空字符串)
// 返回字符串从 start 开始的给定 length 的部分。
let str = "stringify";
alert( str.substr(2, 4) ); // 'ring',从位置 2 开始,获取 4 个字符
方法 | 选择方式…… | 负值参数 |
---|---|---|
slice(start, end) |
从 start 到 end (不含 end ) |
允许 |
substring(start, end) |
start 与 end 之间(包括 start ,但不包括 end ) |
负值代表 0 |
substr(start, length) |
从 start 开始获取长为 length 的字符串 |
允许 start 为负数 |
比较字符串
字符串按字母顺序逐字比较。
// 小写字母总是大于大写字母:
alert( 'a' > 'Z' ); // true
//带变音符号的字母存在“乱序”的情况:
alert( 'Österreich' > 'Zealand' ); // true
// 不同的字母有不同的代码
alert( "z".codePointAt(0) ); // 122
alert( "Z".codePointAt(0) ); // 90
alert( String.fromCodePoint(90) ); // Z
// 在十六进制系统中 90 为 5a
alert( '\u005a' ); // Z
let str = '';
for (let i = 65; i <= 220; i++) {
str += String.fromCodePoint(i);
}
alert( str );
// ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~
// ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖ×ØÙÚÛÜ
str.localeCompare(str2) 会根据语言规则返回一个整数,这个整数能表明 str
是否在 str2
前,后或者等于它:
- 如果
str
小于str2
则返回负数。 - 如果
str
大于str2
则返回正数。 - 如果它们相等则返回
0
。
alert( 'Österreich'.localeCompare('Zealand') ); // -1
alert( '𝒳'.length ); // 2,大写数学符号 X
alert( '😂'.length ); // 2,笑哭表情
alert( '𩷶'.length ); // 2,罕见的中国象形文字
alert( '𝒳'[0] ); // 奇怪的符号……
alert( '𝒳'[1] ); // ……代理对的一块
总结
- 有 3 种类型的引号。反引号允许字符串跨越多行并可以使用
${…}
在字符串中嵌入表达式。 - JavaScript 中的字符串使用的是 UTF-16 编码。
- 我们可以使用像
\n
这样的特殊字符或通过使用\u...
来操作它们的 unicode 进行字符插入。 - 获取字符时,使用
[]
。 - 获取子字符串,使用
slice
或substring
。 - 字符串的大/小写转换,使用:
toLowerCase/toUpperCase
。 - 查找子字符串时,使用
indexOf
或includes/startsWith/endsWith
进行简单检查。 - 根据语言比较字符串时使用
localeCompare
,否则将按字符代码进行比较。
还有其他几种有用的字符串方法:
str.trim()
—— 删除字符串前后的空格 (“trims”)。str.repeat(n)
—— 重复字符串n
次。- ……更多内容细节请参见 手册。
数组
Array
能存储有序数据的集合。
// 创建一个空数组有两种语法:
let arr = new Array();
let arr = [];
let fruits = ["Apple", "Orange", "Plum"];
alert( fruits[0] ); // Apple
alert( fruits[1] ); // Orange
alert( fruits[2] ); // Plum
fruits[2] = 'Pear'; // 现在变成了 ["Apple", "Orange", "Pear"]
fruits[3] = 'Lemon'; // 现在变成 ["Apple", "Orange", "Pear", "Lemon"]
let fruits = ["Apple", "Orange", "Plum"];
alert( fruits.length ); // 3
let fruits = ["Apple", "Orange", "Plum"];
alert( fruits ); // Apple,Orange,Plum
// 混合值
let arr = [ 'Apple', { name: 'John' }, true, function() { alert('hello'); } ];
// 获取索引为 1 的对象然后显示它的 name
alert( arr[1].name ); // John
// 获取索引为 3 的函数并执行
arr[3](); // hello
let fruits = [
"Apple",
"Orange",
"Plum",
];
pop/push, shift/unshift 方法
队列(queue)是最常见的使用数组的方法之一。在计算机科学中,这表示支持两个操作的一个有序元素的集合:
-
push
在末端添加一个元素. -
shift
取出队列首端的一个元素,整个队列往前移,这样原先排第二的元素现在排在了第一。
这两种操作数组都支持。
数组还有另一个用例,就是数据结构 栈。
它支持两种操作:
push
在末端添加一个元素.pop
从末端取出一个元素.
所以新元素的添加和取出都是从“末端”开始的。
栈通常被被形容成一叠卡片:要么在最上面添加卡片,要么从最上面拿走卡片:
let fruits = ["Apple", "Orange", "Pear"];
alert( fruits.pop() ); // 移除 "Pear" 然后 alert 显示出来
alert( fruits ); // Apple, Orange
fruits.push("Pear");
alert( fruits ); // Apple, Orange, Pear
alert( fruits.shift() ); // 移除 Apple 然后 alert 显示出来
alert( 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 );
push/pop
方法运行的比较快,而 shift/unshift
比较慢。
在 JavaScript 中只有 7 种基本类型。数组是一个对象,因此其行为也像一个对象。
let fruits = ["Banana"]
let arr = fruits; // 通过引用复制 (两个变量引用的是相同的数组)
alert( arr === fruits ); // true
arr.push("Pear"); // 通过引用修改数组
alert( fruits ); // Banana, Pear — 现在有 2 项了
let fruits = []; // 创建一个数组
fruits[99999] = 5; // 分配索引远大于数组长度的属性
fruits.age = 25; // 创建一个具有任意名称的属性
循环数组
let arr = ["Apple", "Orange", "Pear"];
for (let i = 0; i < arr.length; i++) {
alert( arr[i] );
}
// 遍历数组元素
for (let fruit of fruits) {
alert( fruit );
}
// for..in 循环会遍历 所有属性,不仅仅是这些数字属性。
let arr = ["Apple", "Orange", "Pear"];
for (let key in arr) {
alert( arr[key] ); // Apple, Orange, Pear
}
我们修改数组的时候,length
属性会自动更新。准确来说,它实际上不是数组里元素的个数,而是最大的数字索引值加一。
let fruits = [];
fruits[123] = "Apple";
alert( fruits.length ); // 124
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;
。
new Array()
这是创建数组的另一种语法:
let arr = new Array("Apple", "Pear", "etc");
它很少被使用,因为方括号 []
更短更简洁。而且这种语法还存在一些诡异的特性。
如果使用单个参数(即数字)调用 new Array
,那么它会创建一个 指定了长度,却没有任何项 的数组。
让我们看看如何搬起石头砸自己的脚:
let arr = new Array(2); // 会创建一个 [2] 的数组吗?
alert( arr[0] ); // undefined!没有元素。
alert( arr.length ); // length 2
在上面的代码中,new Array(number)
创建的数组的所有元素都是 undefined
。
为了避免这种乌龙事件,我们通常都是使用方括号的,除非我们清楚地知道自己正在做什么。
多维数组
数组里的项也可以是数组。我们可以将其用于多维数组,例如存储矩阵:
let matrix = [ [1, 2, 3], [4, 5, 6], [7, 8, 9]];
alert( matrix[1][1] ); // 最中间的那个数
toString
数组有自己的 toString
方法的实现,会返回以逗号隔开的元素列表。
例如:
let arr = [1, 2, 3];
alert( arr ); // 1,2,3
alert( String(arr) === '1,2,3' ); // true
此外,我们试试运行一下这个:
alert( [] + 1 ); // "1"
alert( [1] + 1 ); // "11"
alert( [1,2] + 1 ); // "1,21"
数组没有 Symbol.toPrimitive
,也没有 valueOf
,它们只能执行 toString
进行转换,所以这里 []
就变成了一个空字符串,[1]
变成了 "1"
,[1,2]
变成了 "1,2"
。
当 "+"
运算符把一些项加到字符串后面时,加号后面的项也会被转换成字符串,所以下一步就会是这样:
alert( "" + 1 ); // "1"
alert( "1" + 1 ); // "11"
alert( "1,2" + 1 ); // "1,21"
总结
数组是一种特殊的对象,适用于存储和管理有序的数据项。
- 声明:
// 方括号 (常见用法)
let arr = [item1, item2...];// new Array (极其少见)
let arr = new Array(item1, item2...);
调用 new Array(number)
会创建一个给定长度的数组,但不含有任何项。
-
length
属性是数组的长度,准确地说,它是数组最后一个数字索引值加一。它由数组方法自动调整。 -
如果我们手动缩短
length
,那么数组就会被截断。
我们可以通过下列操作以双端队列的方式使用数组:
push(...items)
在末端添加items
项。pop()
从末端移除并返回该元素。shift()
从首端移除并返回该元素。unshift(...items)
从首端添加items
项。
遍历数组的元素:
for (let i=0; i<arr.length; i++)
— 运行得最快,可兼容旧版本浏览器。for (let item of arr)
— 现代语法,只能访问 items。for (let i in arr)
— 永远不要用这个。
数组方法
数组提供的方法有很多。为了方便起见,在本章中,我们将按组讲解。
添加/移除数组元素
我们已经学了从数组的首端或尾端添加和删除元素的方法:
arr.push(...items)
—— 从尾端添加元素,arr.pop()
—— 从尾端提取元素,arr.shift()
—— 从首端提取元素,arr.unshift(...items)
—— 从首端添加元素。
这里还有其他几种方法。
splice
如何从数组中删除元素?
数组是对象,所以我们可以尝试使用 delete
:
let arr = ["I", "go", "home"];
delete arr[1]; // remove "go"
alert( arr[1] ); // undefined
// now arr = ["I", , "home"];
alert( arr.length ); // 3
元素被删除了,但数组仍然有 3 个元素,我们可以看到 arr.length == 3
。
这很正常,因为 delete obj.key
是通过 key
来移除对应的值。对于对象来说是可以的。但是对于数组来说,我们通常希望剩下的元素能够移动并占据被释放的位置。我们希望得到一个更短的数组。
所以应该使用特殊的方法。
arr.splice 方法可以说是处理数组的瑞士军刀。它可以做所有事情:添加,删除和插入元素。
语法是:
arr.splice(start[, deleteCount, elem1, ..., elemN])
它从索引 start
开始修改 arr
:删除 deleteCount
个元素并在当前位置插入 elem1, ..., elemN
。最后返回已被删除元素的数组。
通过例子我们可以很容易地掌握这个方法。
让我们从删除开始:
let arr = ["I", "study", "JavaScript"];
arr.splice(1, 1); // 从索引 1 开始删除 1 个元素
alert( arr ); // ["I", "JavaScript"]
简单,对吧?从索引 1
开始删除 1
个元素。(译注:当只填写了 splice
的 start
参数时,将删除从索引 start
开始的所有数组项)
在下一个例子中,我们删除了 3 个元素,并用另外两个元素替换它们:
let arr = ["I", "study", "JavaScript", "right", "now"];
// 删除数组的前三项,并使用其他内容代替它们
arr.splice(0, 3, "Let's", "dance");
alert( arr ) // 现在 ["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
slice
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
的副本。其通常用于获取副本,以进行不影响原始数组的进一步转换。
concat
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(fn)
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)
方法实现了通用的排序算法。我们不需要关心它的内部工作原理(大多数情况下都是经过 快速排序 或 Timsort 算法优化的)。它将遍历数组,使用提供的函数比较其元素并对其重新排序,我们所需要的就是提供执行比较的函数 fn
。
顺便说一句,如果我们想知道要比较哪些元素 —— 那么什么都不会阻止 alert 它们:
[1, -2, 15, 2, 0, 8].sort(function(a, b) {
alert( a + " <> " + b );
return 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 );
这与上面更长的版本完全相同。
smart header="使用 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(对的!)
reverse
arr.reverse 方法用于颠倒 arr
中元素的顺序。
例如:
let arr = [1, 2, 3, 4, 5];
arr.reverse();
alert( arr ); // 5,4,3,2,1
它也会返回颠倒后的数组 arr
。
split 和 join
举一个现实生活场景的例子。我们正在编写一个消息应用程序,并且该人员输入以逗号分隔的接收者列表: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)](mdn:js/Array/join) 与
split相反。它会在它们之间创建一串由
glue粘合的
arr` 项。
例如:
let arr = ['Bilbo', 'Gandalf', 'Nazgul'];
let str = arr.join(';'); // 使用分号 ; 将数组粘合成字符串
alert( str ); // Bilbo;Gandalf;Nazgul
reduce/reduceRight
当我们需要遍历一个数组时 —— 我们可以使用 forEach
,for
或 for..of
。
当我们需要遍历并返回每个元素的数据时 —— 我们可以使用 map
。
arr.reduce 方法和 arr.reduceRight 方法和上面的种类差不多,但稍微复杂一点。它们用于根据数组计算单个值。
语法是:
let value = arr.reduce(function(accumulator, item, index, array) {
// ...
}, [initial]);
该函数一个接一个地应用于所有数组元素,并将其结果“搬运(carry on)”到下一个调用。
参数:
accumulator
—— 是上一个函数调用的结果,第一次等于initial
(如果提供了initial
的话)。item
—— 当前的数组元素。index
—— 当前索引。arr
—— 数组本身。
应用函数时,上一个函数调用的结果将作为第一个参数传递给下一个函数。
因此,第一个参数本质上是累加器,用于存储所有先前执行的组合结果。最后,它成为 reduce
的结果。
听起来复杂吗?
掌握这个知识点的最简单的方法就是通过示例。
在这里,我们通过一行代码得到一个数组的总和:
let arr = [1, 2, 3, 4, 5];
let result = arr.reduce((sum, current) => sum + current, 0);
alert(result); // 15
传递给 reduce
的函数仅使用了 2 个参数,通常这就足够了。
让我们看看细节,到底发生了什么。
- 在第一次运行时,
sum
的值为初始值initial
(reduce
的最后一个参数),等于 0,current
是第一个数组元素,等于1
。所以函数运行的结果是1
。 - 在第二次运行时,
sum = 1
,我们将第二个数组元素(2
)与其相加并返回。 - 在第三次运行中,
sum = 3
,我们继续把下一个元素与其相加,以此类推……
计算流程:
或者以表格的形式表示,每一行代表的是对下一个数组元素的函数调用:
sum |
current |
result |
|
---|---|---|---|
第 1 次调用 | 0 |
1 |
1 |
第 2 次调用 | 1 |
2 |
3 |
第 3 次调用 | 3 |
3 |
6 |
第 4 次调用 | 6 |
4 |
10 |
第 5 次调用 | 10 |
5 |
15 |
在这里,我们可以清楚地看到上一个调用的结果如何成为下一个调用的第一个参数。
我们也可以省略初始值:
let arr = [1, 2, 3, 4, 5];
// 删除 reduce 的初始值(没有 0)
let result = arr.reduce((sum, current) => sum + current);
alert( result ); // 15
结果是一样的。这是因为如果没有初始值,那么 reduce
会将数组的第一个元素作为初始值,并从第二个元素开始迭代。
计算表与上面相同,只是去掉第一行。
但是这种使用需要非常小心。如果数组为空,那么在没有初始值的情况下调用 reduce
会导致错误。
例如:
let arr = [];
// Error: Reduce of empty array with no initial value
// 如果初始值存在,则 reduce 将为空 arr 返回它(即这个初始值)。
arr.reduce((sum, current) => sum + current);
所以建议始终指定初始值。
arr.reduceRight 和 arr.reduce 方法的功能一样,只是遍历为从右到左。
Array.isArray
数组是基于对象的,不构成单独的语言类型。
所以 typeof
不能帮助从数组中区分出普通对象:
alert(typeof {}); // object
alert(typeof []); // same
……但是数组经常被使用,因此有一种特殊的方法用于判断:Array.isArray(value)。如果 value
是一个数组,则返回 true
;否则返回 false
。
alert(Array.isArray({})); // false
alert(Array.isArray([])); // true
大多数方法都支持 "thisArg"
几乎所有调用函数的数组方法 —— 比如 find
,filter
,map
,除了 sort
是一个特例,都接受一个可选的附加参数 thisArg
。
上面的部分中没有解释该参数,因为该参数很少使用。但是为了完整性,我们需要讲讲它。
以下是这些方法的完整语法:
arr.find(func, thisArg);
arr.filter(func, thisArg);
arr.map(func, thisArg);
// ...
// thisArg 是可选的最后一个参数
thisArg
参数的值在 func
中变为 this
。
例如,在这里我们使用 army
对象方法作为过滤器,thisArg
用于传递上下文(passes the context):
let army = {
minAge: 18,
maxAge: 27,
canJoin(user) {
return user.age >= this.minAge && user.age < this.maxAge;
}
};
let users = [
{age: 16},
{age: 20},
{age: 23},
{age: 30}
];
// 找到 army.canJoin 返回 true 的 user
let soldiers = users.filter(army.canJoin, army);
alert(soldiers.length); // 2
alert(soldiers[0].age); // 20
alert(soldiers[1].age); // 23
如果在上面的示例中我们使用了 users.filter(army.canJoin)
,那么 army.canJoin
将被作为独立函数调用,并且这时 this=undefined
,从而会导致即时错误。
可以用 users.filter(user => army.canJoin(user))
替换对 users.filter(army.canJoin, army)
的调用。前者的使用频率更高,因为对于大多数人来说,它更容易理解。
总结
数组方法备忘单:
-
添加/删除元素:
push(...items)
—— 向尾端添加元素,pop()
—— 从尾端提取一个元素,shift()
—— 从首端提取一个元素,unshift(...items)
—— 向首端添加元素,splice(pos, deleteCount, ...items)
—— 从pos
开始删除deleteCount
个元素,并插入items
。slice(start, end)
—— 创建一个新数组,将从索引start
到索引end
(但不包括end
)的元素复制进去。concat(...items)
—— 返回一个新数组:复制当前数组的所有元素,并向其中添加items
。如果items
中的任意一项是一个数组,那么就取其元素。
-
搜索元素:
indexOf/lastIndexOf(item, pos)
—— 从索引pos
开始搜索item
,搜索到则返回该项的索引,否则返回-1
。includes(value)
—— 如果数组有value
,则返回true
,否则返回false
。find/filter(func)
—— 通过func
过滤元素,返回使func
返回true
的第一个值/所有值。findIndex
和find
类似,但返回索引而不是值。
-
遍历元素:
forEach(func)
—— 对每个元素都调用func
,不返回任何内容。
-
转换数组:
map(func)
—— 根据对每个元素调用func
的结果创建一个新数组。sort(func)
—— 对数组进行原位(in-place)排序,然后返回它。reverse()
—— 原位(in-place)反转数组,然后返回它。split/join
—— 将字符串转换为数组并返回。reduce/reduceRight(func, initial)
—— 通过对每个元素调用func
计算数组上的单个值,并在调用之间传递中间结果。
-
其他:
Array.isArray(arr)
检查arr
是否是一个数组。
请注意,sort
,reverse
和 splice
方法修改的是数组本身。
这些是最常用的方法,它们覆盖 99% 的用例。但是还有其他几个:
-
arr.some(fn)/arr.every(fn) 检查数组。
与
map
类似,对数组的每个元素调用函数fn
。如果任何/所有结果为true
,则返回true
,否则返回false
。这两个方法的行为类似于
||
和&&
运算符:如果fn
返回一个真值,arr.some()
立即返回true
并停止迭代其余数组项;如果fn
返回一个假值,arr.every()
立即返回false
并停止对其余数组项的迭代。我们可以使用
every
来比较数组:
function arraysEqual(arr1, arr2) {
return arr1.length === arr2.length && arr1.every((value, index) => value === arr2[index]);
}
alert( arraysEqual([1, 2], [1, 2])); // true
-
arr.fill(value, start, end) —— 从索引
start
到end
,用重复的value
填充数组。 -
arr.copyWithin(target, start, end) —— 将从位置
start
到end
的所有元素复制到 自身 的target
位置(覆盖现有元素)。 -
arr.flat(depth)/arr.flatMap(fn) 从多维数组创建一个新的扁平数组。
-
Array.of(element0[, element1[, ...[, elementN]]]) 基于可变数量的参数创建一个新的
Array
实例,而不需要考虑参数的数量或类型。
有关完整列表,请参阅 手册。
乍看起来,似乎有很多方法,很难记住。但实际上这比看起来要容易得多。
浏览这个备忘单,以了解这些方法。然后解决本章中的习题来进行练习,以便让你有数组方法的使用经验。
然后,每当你需要对数组进行某些操作,而又不知道怎么做的时候,请回到这儿,查看这个备忘单,然后找到正确的方法。示例将帮助你正确编写它。用不了多久,你就自然而然地记住这些方法了,根本不需要你死记硬背。