一、js 数据类型
-
7 种基本数据类型(也称为原始类型):Undefined(未定义)、Null(未知)、Boolean(布尔值)、Number(数值)、String(字符串)、Symbol(符号:独一无二的值)、BigInt(任意长度的整数)。
- Null:用于未知(空) 的值 —— 只有一个
null
值的独立类型。 - Undefined:用于未定义的值 —— 只有一个
undefined
值的独立类型。 - Boolean:用于布尔值:
true
和false
。 - Number:用于整数或浮点数,在
±(2^53-1)
范围内的整数。 - BigInt:用于任意长度的整数。是最近被添加到 js 语言中的。
- String:用于字符串:一个字符串可以包含 0 个或多个字符。
- Null:用于未知(空) 的值 —— 只有一个
-
1 种复杂数据类型:Object(对象),用于更复杂的数据结构。
我们可以通过 typeof
运算符查看存储在变量中的数据类型。
1、 类型判断 typeof
typeof
操作符会以字符串形式返回参数的数据类型。
它支持两种语法形式:
- 作为运算符:
typeof x
。 - 函数形式:
typeof(x)
。
对一个值使用 typeof
操作符会返回下列字符串之一:
"undefined"
表示值未定义;"boolean"
表示值为布尔值;"string"
表示值为字符串;"number"
表示值为数值;"object"
表示值为对象(而不是函数)或null
;"function"
表示值为函数;"symbol"
表示值为符号。
console.log(typeof 'some string'); // "string"
console.log(typeof 95); // "number"
console.log(typeof Symbol('id'); // "symbol"
console.log(typeof [1, 2]); // "object"
console.log(typeof null); // "object"
console.log(typeof alert); // "function"
注意:
typeof null
返回的是"object"
。这是因为特殊值null
被认为是一个对空对象的引用。这是 JavaScript 早期的错误,为了兼容性而保留下来。但null
绝对不是一个 object。null
有自己的类型,它是一个特殊值。
严格来讲,函数在 ECMAScript 中被认为是对象,并不代表一种数据类型。可是,函数也有自己特殊的属性。为此,就有必要通过
typeof
操作符来区分函数和其他对象。
2、 原始类型的方法
JavaScript 允许我们像使用对象一样使用原始类型(字符串,数字等)。
我们来看看原始类型和对象之间的关键区别。
对象能够存储多个值作为属性。我们可以把一个函数作为对象的属性存储到对象中。
let john = {
name: "John",
sayHi: function() {
alert("Hi buddy!");
}
};
john.sayHi(); // Hi buddy!
所以我们在这里创建了一个包含 sayHi
方法的对象 john
。
许多内建对象已经存在,例如那些处理日期、错误、HTML 元素等的内建对象。它们具有不同的属性和方法。
但是,这些特性(feature)都是有成本的!
对象比原始类型“更重”。它们需要额外的资源来支持运作。
2.1 当作对象的原始类型
- 原始类型仍然是原始的。与预期相同,提供单个值
- JavaScript 允许访问字符串,数字,布尔值和 symbol 的方法和属性。
- 为了使它们起作用,创建了提供额外功能的特殊 “对象包装器”,使用后即被销毁。
“对象包装器” 对于每种原始类型都是不同的,它们被称为 String
、Number
、Boolean
和 Symbol
。因此,它们提供了不同的方法。
例如,字符串方法 str.toUpperCase()
返回一个大写化处理的字符串。
let str = "Hello";
console.log( str.toUpperCase() ); // HELLO
以下是 str.toUpperCase()
中实际发生的情况:
- 字符串
str
是一个原始值。因此,在访问其属性时,会创建一个包含字符串字面值的特殊对象,并且具有有用的方法,例如toUpperCase()
。 - 该方法运行并返回一个新的字符串。
- 特殊对象被销毁,只留下原始值
str
。
所以原始类型可以提供方法,但它们依然是轻量级的。
JavaScript 引擎高度优化了这个过程。它甚至可能跳过创建额外的对象。但是它仍然必须遵守规范,并且表现得好像它创建了一样。
2.1.1 构造器 String/Number/Boolean 仅供内部使用
像 Java 这样的一些语言允许我们使用 new Number(1)
或 new Boolean(false)
等语法,明确地为原始类型创建“对象包装器”。
在 JavaScript 中,由于历史原因,这也是可以的,但极其 不推荐。因为这样会出问题。
例如:
console.log( typeof 0 ); // "number"
console.log( typeof new Number(0) ); // "object"!
对象在 if
中始终为真,因此此处的 console.log
将显示:
let zero = new Number(0);
if (zero) { // zero 为 true,因为它是一个对象
console.log( "zero is truthy?!?" );
}
另一方面,调用不带 new
(关键字)的 String/Number/Boolean
函数是完全理智和有用的。它们将一个值转换为相应的类型:转成字符串、数字或布尔值(原始类型)。
let num = Number("123"); // 将字符串转成数字
2.1.2 null/undefined 没有任何方法
特殊的原始类型 null
和 undefined
是例外。它们没有对应的“对象包装器”,也没有提供任何方法。从某种意义上说,它们是“最原始的”。
尝试访问这种值的属性会导致错误:
console.log(null.test); // error
二、Undefined 和 Null
1、Undefined 类型
-
Undefined 类型只有一个值,就是特殊值
undefined
。- 当使用
var
或let
声明了变量但没有初始化时,就相当于给变量赋予了undefined
值。
- 当使用
注意:包含 undefined
值的变量跟未声明变量是有区别的:
/* 包含 undefined 值的变量 */
let message; // 这个变量被声明了,只是值为 undefined
console.log(message); // "undefined"
/* 未声明变量 */
console.log(age); // ReferenceError: age is not defined
-
对未声明的变量,只能执行一个有用的操作,就是对它调用
typeof
。-
对未声明的变量调用
delete
也不会报错,但在严格模式
下会抛出错误。delete age; // true /* 严格模式 */ function test() { "use strict"; delete age; } test() // SyntaxError: Delete of an unqualified identifier in strict mode.
-
无论是声明但未赋值还是未声明,typeof
返回的都是字符串 "undefined"
。
// 声明但未赋值
let message;
console.log(typeof message); // "undefined"
// 未声明
console.log(typeof age); // "undefined"
-
建议在声明变量的同时进行初始化。这样,当
typeof
返回"undefined"
时,你就会知道那是因为给定的变量尚未声明,而不是声明了但未初始化。// 确保没有声明过这个变量 let age if (typeof age == 'undefined') { // 给定的变量尚未声明 }
2、Null 类型
-
Null 类型同样只有一个值,即特殊值
null
。-
逻辑上讲,
null
值表示一个空对象指针,这也是给typeof
传一个null
会返回"object"
的原因。let car = null; console.log(typeof car); // "object"
-
在定义将来要保存对象值的变量时,建议使用 null
来初始化。这样,只要检查这个变量的值是不是 null
就可以知道这个变量是否在后来被重新赋予了一个对象的引用。
let car = null;
if (car != null) {
// car 是一个对象的引用
}
undefined
值是由 null
值派生而来的,因此 ECMA-262 将它们定义为表面上相等。
console.log(null == undefined); // true
console.log(null === undefined); // false
即使 null
和 undefined
有关系,它们的用途也是完全不一样的。如前所述,永远不必显式地将变量值设置为 undefined
。但 null
不是这样的。任何时候,只要变量要保存对象,而当时又没有那个对象可保存,就要用 null
来填充该变量。这样就可以保持 null
是空对象指针的语义,并进一步将其与 undefined
区分开来。
三、Boolean 类型
1、类型基础
Boolean 类型有两个字面值:true
和 false
。这两个布尔值不同于数值,因此 true
不等于(全等) 1,false
不等于 0。
console.log(1 == true) // true
console.log(0 == false) // true
console.log(1 === true) // false
console.log(0 === false) // false
2、类型转换
要将一个其他类型的值转换为布尔值,可以调用特定的 Boolean()
转型函数:
- 直观上为“空”的值(如 0、空字符串、
null
、undefined
和NaN
)将变为false
。 - 其他值变成
true
四、Number 类型
1、类型基础
Number 类型(JavaScript 中的常规数字)以 64 位的格式 IEEE-754 存储,也被称为“双精度浮点数”。这是我们大多数时候所使用的数字。常规数字不能超过 (2^53) 或小于 -(2^53)。
1.1 二进制、八进制、十六进制
整数也可以用八进制(以 8 为基数)或十六进制(以 16 为基数)字面量表示。
-
二进制,只能有 0 和 1。前缀
0b
。let a = 0b11111111; // 二进制形式的 255 // 从低位到高位(即从右往左)计算 // ( 1 * 2^7 + 1 * 2^6 + 1 * 2^5 + 1 * 2^4 + 1 * 2^3 + 1 * 2^2 + 1 * 2^1 + 1 * 2^0 ) // = ( 128 + 64 + 32 + 16 + 8 + 4 + 2 + 1 ) // = 255
-
八进制,第一个数字必须是零(0),然后是相应的八进制数字(数值
0~7
)。- 如果八进制字面量中包含的数字超出了应有的范围,就会忽略前缀的零,后面的数字序列会被当成十进制数。
- ECMAScript 2015 或 ES6 中的八进制值通过前缀
0o
来表示;严格模式下,前缀0
会被视为语法错误,如果要表示八进制值,应该使用前缀0o
。
// 八进制字面量中包含的数字超出了应有的范围,忽略前缀的零,当成十进制数 let octalNum2 = 079; // 无效的八进制值,当成 79 处理 let b = 0o377; // 八进制形式的 255 // (3 * 8^2 + 7 * 8^1 + 7 * 8^0 ) // = (3 * 64 + 7 * 8 + 7) // = 255 /* 严格模式下,前缀 0 会被视为语法错误,应该使用前缀 0o */ function test(){ "use strict"; // let num1 = 06; // console.log(num1); // SyntaxError: Octal literals are not allowed in strict mode let num2 = 0o6; console.log(num2); // 6 } test();
-
十六进制字面量,必须有数值前缀
0x
(区分大小写),然后是十六进制数字(0~9
以 及A~F
),十六进制数字中的字母大小写均可。let hexNum1 = 0xA; // 十六进制 10 let hexNum2 = 0x1f; // 十六进制 31 let hexNum3 = 0xff; // 十六进制 255
只有这三种进制支持这种字面量写法。对于其他进制,我们应该使用函数 parseInt
。
1.1.1 num.toString(base)
返回给定 base
进制的 num
的字符串。
let num = 255;
console.log( num.toString(16) ); // ff (15 * 16^1 + 15 * 16^0) = (15 * 16 + 15) = 255
console.log( num.toString(2) ); // 11111111
base
的范围可以从 2
到 36
。默认情况下是 10
。
-
base=36
是最大进制,数字可以是 0..9 或 A..Z。所有拉丁字母都被用于了表示数字。对于 36 进制来说,一个有趣且有用的例子是,当我们需要将一个较长的数字标识符转换成较短的时候,例如做一个短的 URL。可以简单地使用基数为 36 的数字系统表示:console.log( 123456..toString(36) ); // 2n9c
使用两个点来调用一个方法
请注意
123456..toString(36)
中的两个点不是打错了。如果我们想直接在一个数字上调用一个方法,比如上面例子中的toString
,那么我们需要在它后面放置两个点..
。如果我们放置一个点:
123456.toString(36)
,那么就会出现一个 error,因为 JavaScript 语法隐含了第一个点之后的部分为小数部分。如果我们再放一个点,那么 JavaScript 就知道小数部分为空,现在使用该方法。也可以写成
(123456).toString(36)
。
1.2 浮点数
-
浮点数,数值中必须包含小数点,而且小数点后面必须至少有一个数字。
let floatNum1 = 1.1; let floatNum2 = 0.1; let floatNum3 = .1; // 有效,但不推荐
-
因为存储浮点值使用的内存空间是存储整数值的两倍,所以 ECMAScript 总是想方设法把值转换为整数。
let floatNum1 = 1.; // 小数点后面没有数字,当成整数 1 处理 let floatNum2 = 10.0; // 小数点后面是零,当成整数 10 处理
1.2.1 科学计数法
-
对于非常大或非常小的数值,浮点值可以用科学记数法来表示。
- 科学记数法用于表示一个应该乘以 10 的给定次幂的数值。ECMAScript 中科学记数法的格式要求是一个数值(整数或浮点数)后跟一个大写或小写的字母 e,再加上一个要乘的 10 的多少次幂。
let floatNum1 = 3.125e7; // 3.125 乘以 10 的 7 次幂,等于 31250000 let floatNum1 = 3E-17; // 3 乘以 10 的 -17 次幂,等于 0.00000000000000003
-
默认情况下,ECMAScript 会将 小数点后包含 6 个零(及以上) 的浮点值自动转换为 科学记数法。
1.2.2 精度损失
在内部,数字是以 64 位格式 IEEE-754 表示的,所以正好有 64 位可以存储一个数字:其中 52 位被用于存储这些数字,其中 11 位用于存储小数点的位置(对于整数,它们为零),而 1 位用于符号。
如果一个数字太大,则会溢出 64 位存储,并可能会导致无穷大:
console.log( 1e500 ); // Infinity
这可能不那么明显,但经常会发生的是,精度的损失:
console.log(0.1 + 0.2); // 0.30000000000000004
console.log(0.15 + 0.02); // 0.16999999999999998
console.log(0.15 + 0.25); // 0.4
尝试运行下面这段代码,出现了同样的问题:精度损失:
// Hello!我是一个会自我增加的数字!
console.log( 9999999999999999 ); // 显示 10000000000000000
有 64 位来表示该数字,其中 52 位可用于存储数字,但这还不够。所以最不重要的数字就消失了。
注意 之所以存在这种舍入错误,是因为使用了 IEEE 754 数值,这种错误并非 ECMAScript 所独有。其他使用相同格式的语言也有这个问题。
1.2.2.1 导致精度损失的原因
在十进制数字系统中,可以保证以 10
的整数次幂作为除数能够正常工作,但是以 3
作为除数则不能。也是同样的原因,在二进制数字系统中,可以保证以 2
的整数次幂作为除数时能够正常工作,但 1/10
就变成了一个无限循环的二进制小数。
使用二进制数字系统无法 精确 存储 0.1
或 0.2
,就像没有办法将三分之一存储为十进制小数一样。
IEEE-754 数字格式通过将数字舍入到最接近的可能数字来解决此问题。这些舍入规则通常不允许我们看到“极小的精度损失”,但是它确实存在。
console.log( 0.1.toFixed(20) ); // 0.10000000000000000555
当我们对两个数字进行求和时,它们的“精度损失”会叠加起来。
这就是为什么 0.1 + 0.2
不等于 0.3
。
1.2.2.2 解决方法
-
最可靠的方法是借助方法
toFixed(n)
对结果进行舍入:let sum = 0.1 + 0.2; console.log( +sum.toFixed(2) ); // 0.3
-
我们可以将数字临时乘以 100(或更大的数字),将其转换为整数,进行数学运算,然后再除回:
console.log( (0.1 * 10 + 0.2 * 10) / 10 ); // 0.3 console.log( (0.28 * 100 + 0.14 * 100) / 100); // 0.4200000000000001
注意,乘/除法可以减少误差,但不能完全消除误差。
1.3 Infinity
与 NAN
1.3.1 Infinity
无穷值
由于内存的限制,ECMAScript 并不支持表示这个世界上的所有数值。数值结果超出了 JavaScript 可以表示的范围,会被自动转换为一个特殊的 Infinity
(无穷) 值,该值将不能再进一步用于任何计算。
-
-Infinity
(负无穷大)、Infinity
(正无穷大)。-
我们可以通过 除以 0 来得到它。
console.log(1 / 0); // Infinity
-
使用
Number.NEGATIVE_INFINITY
和Number.POSITIVE_INFINITY
也可以获取正、负Infinity
。console.log(Number.NEGATIVE_INFINITY, Number.POSITIVE_INFINITY); // -Infinity Infinity
-
-
要确定一个值是不是有限大,可以使用
isFinite(value)
:将其参数转换为数字,判断是否是常规数字,而不是NaN/Infinity/-Infinity
。返回true | false
。// 通过除以 0 得到 Infinity console.log(5 / 0); // Infinity console.log(5 / -0); // -Infinity let result = Number.MAX_VALUE + Number.MAX_VALUE; console.log( isFinite(result) ); // false console.log( isFinite("15") ); // true console.log( isFinite("str") ); // false,因为是一个特殊的值:NaN console.log( isFinite('') ); // true,空字符串视为 `0` console.log( isFinite(Infinity) ); // false,因为是一个特殊的值:Infinity
请注意,在所有数字函数中,包括
isFinite
,空字符串或仅有空格的字符串均被视为0
。
1.3.2 NaN
有一个特殊的数值叫 NaN
,意思是“不是数值”(Not a Number),代表一个计算错误。用于表示本来要返回数值的操作失败了(而不是抛出错误)。
console.log('not a number' / 0); // NaN
/* 在 ECMAScript 中,0、+0 或 -0 相除会返回 NaN */
console.log(0 / 0); // NaN
console.log(-0 / +0); // NaN
NaN
有以下几个独特的属性:
-
任何涉及
NaN
的操作始终返回NaN
(如NaN / 10
);console.log(NaN / 10); // NaN
-
NaN
不等于包括NaN
在内的任何值;console.log(NaN == NaN); // false
-
使用
isNaN()
函数,判断参数是否“不是数值”。isNaN(value)
将其参数转换为数字,然后测试它是否为NaN
。console.log(isNaN(NaN)); // true console.log(isNaN(10)); // false,10 是数值 console.log(isNaN("10")); // false,可以转换为数值 10 console.log(isNaN("blue")); // true,不可以转换为数值 console.log(isNaN(true)); // false,可以转换为数值 1
1.3.2.1 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)。
2、类型转换
有 3 个函数可以将非数值转换为数值:Number()
、parseInt()
和 parseFloat()
。
在算术函数和表达式中,会自动进行
Number()
类型转换。
Number()
是转型函数,可用于任何数据类型。parseInt()
和 parseFloat()
主要用于将 字符串 转换为数值。
方法 | 参数类型 | 返回值 | 读取规则 | 无数字时 |
---|---|---|---|---|
Number() | 任何数据类型 | 十进制数字 | 如果值不完全是数字,就会失败(忽略首尾的空白) | 空字符串返回 0 ,其余返回 NaN |
parseInt() | 第一个参数为字符串。具有可选的第二个参数,指定返回数字的进制 | 整数 | 从字符串中“读取”数字(从第一个非空格字符开始),直到无法读取为止。如果发生 error,则返回收集到的数字。 | 返回 NaN (包括空字符串) |
parseFloat() | 字符串 | 浮点数 | 同上 | 同上 |
2.1 Number()
Number()
函数,转换成数值类型:
- 布尔值,
true
转换为1
,false
转换为0
。 null
,返回0
。undefined
,返回NaN
。- 字符串,应用以下规则。
- 原样读取字符串,忽略首尾的空白。如果字符串包含数值字符,包括数值字符前面带加、减号的情况,则转换为一个十进制数值(忽略前面的零)。
- 如果字符串包含有效的十六进制格式如"0xf",则会转换为与该十六进制值对应的十进制整数值。
- 如果是空字符串(不包含字符),则返回 0。
- 其余的则返回
NaN
。
- 对象,调用
valueOf()
方法,并按照上述规则转换返回的值。如果转换结果是NaN
,则调用toString()
方法,再按照转换字符串的规则转换。
console.log(Number(true)); // 1
console.log(Number(10)); // 10
console.log(Number(null)); // 0
console.log(Number(undefined)); // NaN
// 字符串
console.log(Number("-11")); // -11
console.log(Number("000011")); // 11
console.log(Number("0xf")); // 15
console.log(Number("")); // 0
console.log(Number("Hello world!")); // NaN
一元加操作符与
Number()
函数遵循相同的转换规则
console.log( +"100" ); // 100
2.2 parseInt()
和 parseFloat()
使用加号 +
或 Number()
的数字转换是严格的。如果一个值不完全是一个数字,就会失败:
console.log( +"100px" ); // NaN
唯一的例外是字符串开头或结尾的空格,因为它们会被忽略。
但在现实生活中,我们经常会有带有单位的值,例如 CSS 中的 "100px" 或 "12pt"。并且,在很多国家,货币符号是紧随金额之后的,所以我们有 "19€",并希望从中提取出一个数值。
这就是 parseInt
和 parseFloat
的作用。
它们可以从字符串中“读取”数字(从第一个非空格字符开始),直到无法读取为止。如果发生 error,则返回收集到的数字。函数 parseInt
返回一个整数,而 parseFloat
返回一个浮点数:
console.log( parseInt(' 100') ); // 100
console.log( parseInt('100px') ); // 100
console.log( parseFloat('12.5em') ); // 12.5
console.log( parseInt('12.3') ); // 12,只有整数部分被返回了
console.log( parseFloat('12.3.4') ); // 12.3,在第二个点出停止了读取
console.log( parseFloat('12.0') ); // 12,如果字符串表示整数,返回整数
当没有数字可读时,parseInt/parseFloat
会返回 NaN
。空字符串也会返回 NaN
(这一点跟 Number()
不一样,它返回 0
)。
console.log( parseInt('a123') ); // NaN 第一个符号不是数字,停止了读取
console.log(parseInt('')); // NaN
2.2.1 parseInt(str, radix) 的第二个参数
parseInt()
函数具有可选的第二个参数。它指定了数字系统的基数,因此 parseInt
还可以解析十六进制数字、二进制数字等的字符串:
console.log( parseInt('0xff', 16) ); // 255
console.log( parseInt('ff', 16) ); // 255,没有 0x 仍然有效
console.log(parseInt("ff")); // NaN,未指定进制而第一个字符为非数值
console.log( parseInt('2n9c', 36) ); // 123456
3、常用方法
3.1 舍入
舍入(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。
这些函数涵盖了处理数字小数部分的所有可能方法。但是,如果我们想将数字舍入到小数点后 n 位,有以下两种方式可以实现这个需求:
- 乘除法:将数字乘以
10
的n
次幂(保留n
位小数),调用舍入函数,然后再将其除回。
let num = 1.23456;
console.log( Math.floor(num * 100) / 100 ); // 1.23456 -> 123.456 -> 123 -> 1.23
- 函数
toFixed(n)
:调用toFixed(n)
将数字舍入到小数点后 n 位,将返回的字符串转换为数字。
// toFixed(n),类似于 Math.round
let num = 12.34;
console.log( num.toFixed(1) ); // "12.3"
let num = 12.36;
console.log( num.toFixed(1) ); // "12.4"
// 如果小数部分比所需要的短,则在结尾添加零
let num = 12.34;
console.log( num.toFixed(5) ); // "12.34000",在结尾添加了 0,以达到小数点后五位
console.log( +num.toFixed(5) ); // 12.34 使用一元加号将其转为数字
我们可以使用一元加号或 Number()
调用,将其转换为数字:+ num.toFixed(5)
。
3.2 其他数学函数
JavaScript 有一个内建的 Math
对象,它包含了一个小型的数学函数和常量库。
几个例子:
Math.random()
:返回一个从 0 到 1 的随机数(不包括 1)
console.log( Math.random() ); // 0.1234567894322
console.log( Math.random() ); // 0.5435252343232
console.log( Math.random() ); // ... (任何随机数)
Math.max(a, b, c...)
/Math.min(a, b, c...)
:从任意数量的参数中返回最大/最小值。
console.log( Math.max(3, 5, -10, 0, 1) ); // 5
console.log( Math.min(1, 2) ); // 1
Math.pow(n, power)
:返回n
的给定(power)次幂
console.log( Math.pow(2, 10) ); // 2 的 10 次幂 = 1024
Math.abs(num)
:取绝对值
console.log(Math.abs(-15)); // 15
Math 对象中还有更多函数和常量,包括三角函数,你可以在 Math 对象文档 中找到这些内容。
五、BigInt 类型
在 JavaScript 中,Numbe 类型无法表示大于 (2^53 - 1)
(即 9007199254740991),或小于 -(2^53 - 1)
的整数。这是其内部表示形式导致的技术限制。
在大多数情况下,这个范围就足够了,但有时我们需要很大的数字,例如用于加密或微秒精度的时间戳。
BigInt 类型是最近被添加到 JavaScript 语言中的,用于表示任意长度的整数。
创建 BigInt 的两种方式:
- 可以通过将
n
附加到整数字段的末尾来创建BigInt
值。
// 尾部的 "n" 表示这是一个 BigInt 类型
const bigInt = 1234567890123456789012345678901234567890n;
- 调用
BigInt()
函数,该函数从字符串、数字等中生成 bigint。
const sameBigint = BigInt("1234567890123456789012345678901234567890");
const bigintFromNumber = BigInt(10); // 与 10n 相同
BigInt
大多数情况下可以像常规数字类型一样使用。
1、像常规数字类型一样使用
1.1 数学运算符
BigInt
大多数情况下可以像常规数字类型一样使用,例如:
console.log(1n + 2n); // 3
console.log(5n / 2n); // 2 向零进行舍入
console.log(5 / 2); // 2.5
请注意:除法 5/2
的结果向零进行舍入,舍入后得到的结果没有了小数部分。对 bigint 的所有操作,返回的结果也是 bigint。
注意:不可以把 bigint 和常规数字类型混合使用:
console.log(1n + 2); // Error: Cannot mix BigInt and other types
如果有需要,我们应该显式地转换它们:使用 BigInt()
或者 Number()
,像这样:
let bigint = 1n;
let number = 2;
// 将 number 转换为 bigint
console.log(bigint + BigInt(number)); // 3
// 将 bigint 转换为 number
console.log(Number(bigint) + number); // 3
转换操作始终是静默的,绝不会报错,但是如果 bigint 太大而数字类型无法容纳,则会截断多余的位,因此我们应该谨慎进行此类转换。
1.1.1 BigInt 不支持一元加法
一元加法运算符 +value
,是大家熟知的将 value
转换成数字类型的方法。
为了避免混淆,在 bigint 中不支持一元加法:
let bigint = 1n;
console.log( +bigint ); // error
所以我们应该用 Number()
来将一个 bigint 转换成一个数字类型。
1.2 比较运算符
比较运算符,例如 <
和 >
,使用它们来对 bigint 和 number 类型的数字进行比较没有问题:
console.log( 2n > 1n ); // true
console.log( 2n > 1 ); // true
但是请注意,由于 number 和 bigint 属于不同类型,它们可能在进行 ==
比较时相等,但在进行 ===
(严格相等)比较时不相等:
console.log( 1 == 1n ); // true
console.log( 1 === 1n ); // false
1.3 布尔运算
当在 if
或其他布尔运算中时,bigint 的行为类似于 number。
例如,在 if
中,bigint 0n
为假,其他值为 true
:
if (0n) {
// 永远不会执行
}
布尔运算符,例如 ||
,&&
和其他运算符,处理 bigint 的方式也类似于 number:
console.log( 1n || 2 ); // 1(1n 被认为是真)
console.log( 0n || 2 ); // 2(0n 被认为是假)
2、Polyfill
Polyfilling bigint 比较棘手。原因是许多 JavaScript 运算符,比如 +
和 -
等,在对待 bigint 的行为上与常规 number 相比有所不同。
例如,bigint 的除法总是返回 bigint(如果需要,会进行舍入)。
想要模拟这种行为,polyfill 需要分析代码,并用其函数替换所有此类运算符。但是这样做很麻烦,并且会耗费很多性能。
所以,目前并没有一个众所周知的好用的 polyfill。
不过,JSBI 库的开发者提出了另一种解决方案。
该库使用自己的方法实现了大的数字。我们可以使用它们替代原生的 bigint:
运算 | 原生 BigInt | JSBI |
---|---|---|
从 Number 创建 | a = BigInt(789) | a = JSBI.BigInt(789) |
加法 | c = a + b | c = JSBI.add(a, b) |
减法 | c = a - b | c = JSBI.subtract(a, b) |
… | … | … |
……然后,对于那些支持 bigint 的浏览器,可以使用 polyfill(Babel 插件)将 JSBI 调用转换为原生的 bigint。
换句话说,这个方法建议我们在写代码时使用 JSBI 替代原生的 bigint。但是 JSBI 在内部像使用 bigint 一样使用 number,并最大程度按照规范进行模拟,所以代码已经是准备好转换成 bigint 的了(bigint-ready)。
对于不支持 bigint 的引擎,我们可以“按原样”使用此类 JSBI 代码,对于那些支持 bigint 的引擎 — polyfill 会将调用转换为原生的 bigint。
兼容性问题
目前 Firefox/Chrome/Edge/Safari 已经支持
BigInt
了,但 IE 还没有。
你可以查看 MDN BigInt 兼容性表 以了解哪些版本的浏览器已经支持 BigInt 了。
六、String
1、类型基础
String(字符串)数据类型表示零或多个字符(16 位 Unicode 字符序列)。
- 字符串可以使用 双引号(")、单引号(')或反引号(`) 标示
- 开头和结尾的引号必须是同一种。
- 双引号和单引号都是“简单”引用,反引号是 功能扩展 引号(模板字符串)。
- 字符串是不可变的。要修改某个变量中的字符串值,必须先销毁原始的字符串,然后将包含新值的另一个字符串保存到该变量。
1.1 创建字符串
// 方法一:字符串字面量
let str = "una";
console.log(typeof str); // 'string'
console.log(str[1]); // 'n'
// 方法二:字符串对象
let txt = new String("string");
console.log(typeof txt); // object
console.log(txt);
字符串对象 => 类数组
类数组:类似数组,和数组长得非常的像,但是并不是数组,因为并不具备有数组的所有方法。
1.2 特殊字符
字符 | 描述 |
---|---|
\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 示例:
console.log( "\u00A9" ); // ©
console.log( "\u{20331}" ); // 佫,罕见的中国象形文字(长 unicode)
console.log( "\u{1F60D}" ); // 😍,笑脸符号(另一个长 unicode)
所有的特殊字符都以反斜杠字符 \
开始。它也被称为“转义字符”。
这些特殊字符可以出现在字符串中的任意位置,且可以作为单个字符被解释:
let text = "This is the letter sigma: \u03a3.";
console.log(text.length); // 28
注意:如果字符串中包含双字节字符,那么
length
属性返回的值可能不是准确的字符数。
1.3 模板字面量
模板字面量保留换行字符,可以跨行定义字符串,在定义模板时特别有用。
let myMultiLineString = 'first line\nsecond line'; // \n 是换行符
console.log(myMultiLineString);
// first line
// second line"
let pageHTML = `
<div>
<a href="#">
<span>Jake</span>
</a>
</div>
`;
console.log(pageHTML.length); // 132
模板字符串特性:
- 字符串插值
${}
,在定义时立即求值并转换为字符串。
let value = 5;
let exponent = 'second';
// 以前,字符串插值是这样实现的:
let interpolatedString = value + ' to the ' + exponent + ' power is ' + (value * value);
// 现在,可以用模板字面量这样实现:
let interpolatedTemplateLiteral = `${ value } to the ${ exponent } power is ${ value * value }`;
console.log(interpolatedString); // 5 to the second power is 25
console.log(interpolatedTemplateLiteral); // 5 to the second power is 25
-
标签函数,通过标签函数可以自定义插值行为。
- 标签函数会接收 被插值记号分隔后的模板 和 对每个表达式求值的结果。
- 标签函数本身是一个常规函数,通过前缀到模板字面量来应用自定义行为。
- 对于有 n 个插值的模板字面量,传给标签函数的表达式参数的个数始终是 n,而传给标签函数的第一个参数所包含的字符串个数则始终是 n+1。
let a = 6, b = 9; function simpleTag(strings, ...expressions) { console.log(strings); // 被插值记号分隔后的模板 for(const expression of expressions) { console.log(expression); // 对每个表达式求值的结果 } return strings[0] + expressions.map((e, i) => `${e}${strings[i + 1]}`) .join(''); } let taggedResult = simpleTag`${ a } + ${ b } = ${ a + b }`; // ["", " + ", " = ", ""] // 被插值记号分隔后的模板 // 6 // 9 // 15 console.log(taggedResult); // "6 + 9 = 15"
- 用默认的
String.raw
标签函数,以获取原始的模板字面量内容(如换行符或 Unicode 字符)。
/* `String.raw` 标签函数 */ // Unicode 示例 console.log(`\u00A9`); // © console.log(String.raw`\u00A9`); // \u00A9 // 换行符示例 console.log(`first line\nsecond line`); // first line // second line console.log(String.raw`first line\nsecond line`); // "first line\nsecond line" // 对实际的换行符来说是不行的,它们不会被转换成转义序列的形式 console.log(`first line second line`); // first line // second line console.log(String.raw`first line second line`); // first line // second line
2、类型转换
-
toString()
方法,返回当前值的字符串等价物。- 可用于数值、布尔值、对象和字符串值。
null
和undefined
值没有toString()
方法。
- 在对数值调用这个方法时,
toString()
可以接收一个底数参数(进制)。
let found = true; console.log(found.toString()); // "true" let obj = { a: 10 }; console.log(obj.toString()); // [object Object] console.log(null.toSrting()); // TypeError: Cannot read property 'toSrting' of null console.log(undefined.toSrting()); // TypeError: Cannot read property 'toSrting' of undefined let num = 10; console.log(num.toString()); // "10" console.log(num.toString(2)); // "1010" console.log(num.toString(8)); // "12" console.log(num.toString(10)); // "10" console.log(num.toString(16)); // "a"
- 可用于数值、布尔值、对象和字符串值。
-
String()
方法,返回表示相应类型值的字符串。- 如果值有
toString()
方法,则调用该方法(不传参数)并返回结果。 null
和undefined
没有toString()
方法,所以String()
方法就直接返回这两个值的字面量文本。
let value; let obj = { a: 10 }; console.log(String(10)); // "10" console.log(String(true)); // "true" console.log(obj.toString()); // [object Object] console.log(String(null)); // "null" console.log(String(value)); // "undefined"
- 如果值有
3、常用方法
3.1 查找字符
3.1.1 charAt()
str.charAt(index)
返回指定位置的字符
index
:字符串的索引值(下标),默认为 0;如果 超出字符串长度 或 小于 0,则返回一个空字符串。- 返回值:指定位置的字符。找不到返回空字符串。
let str = "una";
console.log(str.charAt(2)); // 'a'
console.log(str.charAt(-5)); // ''
3.2 查找子字符串
3.2.1 indexOf()/lastIndexOf() 返回指定子串 首次/最后一次 出现的位置
str.indexOf(value [, index])
返回指定字符串在字符串中 首次 出现的位置(从左往右)
str.lastIndexOf(value [, index])
返回指定字符串在字符串中 最后一次 出现的位置(从右往左)
value
:查找的字符串;index
:起始位置。- 返回值:返回字符串首次/最后一次出现的位置。没找到返回 -1。
关于
index
:
- 当从左往右查找时,
index
默认值为0
;- 当从右往左查找时,
index
默认值为str.length
;index
如果大于str.length
,则等同于str.length
。- 若
index
小于0
,则等同于0
;
/* indexOf */
let str = "una";
console.log(str.indexOf('n',1)); // 1
console.log(str.indexOf('u', -1)); // 0 小于0,等同于0操作,初始位置为0
console.log(str.indexOf('u', 50)); // -1
console.log(str.indexOf('v')); // -1
/* lastIndexOf */
let str= "unauna";
console.log(str.lastIndexOf("u")); // 3
// 负数从0查到0,有则为0 无则-1
console.log(str.lastIndexOf("u", -50)); // 0
console.log(str.lastIndexOf("n", -50)); // -1
console.log(str.lastIndexOf("a", 50)); // 5
console.log(str.lastIndexOf("vv")); // -1
3.2.1.1 ~
整数取反
这里使用的一个老技巧是 bitwise NOT ~
运算符。它将数字转换为 32-bit 整数(如果存在小数部分,则删除小数部分),然后对其二进制表示形式中的所有位均取反。
实际上,这意味着一件很简单的事儿:对于 32-bit 整数,~n
等于 -(n+1)
。
console.log( ~2 ); // -3,和 -(2+1) 相同
console.log( ~1 ); // -2,和 -(1+1) 相同
console.log( ~0 ); // -1,和 -(0+1) 相同
console.log( ~-1 ); // 0,和 -(-1+1) 相同
正如我们看到这样,只有当 n == -1
时,~n
才为零(适用于任何 32-bit 带符号的整数 n)。
因此,仅当 indexOf
的结果不是 -1
时,检查 if ( ~str.indexOf("...") )
才为真。
人们用它来简写 indexOf 检查:
let str = "Widget";
if (~str.indexOf("Widget")) {
console.log( 'Found it!' ); // 正常运行
}
通常不建议以非显而易见的方式使用语言特性,但这种特殊技巧在旧代码中仍被广泛使用,所以我们应该理解它。
这种检查只有在字符串没有那么长的情况下才是正确的。
现在我们只会在旧的代码中看到这个技巧,因为现代 JavaScript 提供了 .includes
方法
3.2.2 includes() 字符串是否包含指定子串
str.includes(value, index)
查找字符串中是否包含指定的子字符串
value
:查找的字符串;index
:起始位置,默认为0
。- 如果小于
0
,则等同于0
进行操作;
- 如果小于
- 返回值:
true || false
let str = "Hello world";
console.log(str.includes("world")); // true
3.2.3 startsWith()/endsWith() 字符串是否以指定子串开头/结尾
str.startsWith(value, index)
查看字符串 是否 以指定的子字符串 开头
str.endsWith(value, index)
查看字符串是否以指定的子字符串 结尾。
value
:查找的字符串;index
:起始位置。与indexOf()/lastIndexOf()
相同。- 返回值:
true || false
/* startsWith */
let str = "Hello world";
console.log(str.startsWith("Hello")); // true
console.log(str.startsWith("world")); // false
/* endsWith */
let str = "Hello world";
console.log(str.endsWith("Hello")); // false
console.log(str.endsWith("world")); // true
3.3 字符串截取 slice()/substring()/substr()
方法 | 选择方式 | 负值参数 | 第二个参数 与 str.length 的关系 |
---|---|---|---|
slice(start, end) 截取指定位置区域的子字符串 | 从 start 到 end (不含 end ) | 允许(负值从右往左查找,索引值从 -1 开始) | 大于 str.length ,等同于 str.length ;负数小于或等于 -str.length ,等同于0 |
substring(start, end) 从指定位置提取字符串中指定数目的字符 | start 与 end 之间(包括 start ,但不包括 end ) | 负值代表 0 ;若 start 大于 end ,二者颠倒进行查找 | 大于 str.length ,等同于 str.length ; |
substr(start, length) 提取字符串中两个指定的索引之间的字符(未来可能被移除) | 从 start 开始获取长为 length 的字符串 | 允许 start 为负数(负值从右往左查找,索引值从 -1 开始); | 大于 str.length ,等同于 str.length ; |
三种方法的参数默认值都为:
start
默认为0
,end
默认为str.length
。
3.3.1 slice()
let str = "unauna";
console.log(str.slice(1,5)); // 'naun'
console.log(str.slice(-4)); // 'auna' start为负数,从右往左,第4位为a
console.log(str.slice(-50)); // 'unauna' start为负数,且小于或等于-str.length,等同于0
console.log(str.slice(1, -2)); // 'nau' end为负数,从右往左,第2位为n,截取的位置不包括end
console.log(str.slice(1, -50)); // '' end小于-str.length 等同于0,start > end 返回空字符串
3.3.2 substring()
let str = "unauna";
console.log(str.substring(1, str.length - 1)); // 'naun'
console.log(str.substring(1, 50)); // 'nauna' end大于str.length,等同于 str.length
console.log(str.substring(5, 2)); // 'aun' start大于end,end就会作为 start,start就会作为end
console.log(str.substring(1, -5)); // 'u' end小于0则等同于0 (1, 0),start 大于end,end就会作为start,start就会作为end (0, 1)
console.log(str.substring(1, -1)); // 'u' end小于0等同于0
console.log(str.substring(0, 0)); // ''
3.3.3 substr()
let str = "unauna";
console.log(str.substr(-5, 2)); // 'na' start为负数,从右往左,第5位是n
console.log(str.substr(-5, 50)); // 'nauna' length大于str.length,等同于str.length
console.log(str.substr(-5, -5)); // '' length小于0,返回空字符串
3.4 字符串分割与合并
3.4.1 split() 将字符串分割成数组
str.split(separator [,limit])
将字符串分割成字符串数组
separator
:决定分割的字符串/正则。- 该字符的位置会作为分隔点,并且自己是不在当前数组内的。
- 如果分隔的字符处在字符串的 首尾,则会有一个 空字符串。
- 如果是
""
,可以把每一个字符都分隔开来。
limit
:分割的个数。- 不设置则整个字符串都分割。
- 如果大于
str.length
,以当前字符串的最大分隔为标准。
- 返回值:分割后的字符串数组。不影响原字符串。
let str ="ha-ha-ha"
console.log(str.split("-")); // ["una", "una", "una"] 决定分割的字符,该字符的位置会作为分隔点,并且自己是并不在当前数组内的。
console.log(str.split("h")); // ["", "na-", "na-", "na"] 如果分隔的字符处在字符串的 首尾,则会有一个 空字符串。
let str2 = 'uu'
console.log(str2.split("u")) // ["", "", ""] // 前后两个""是首尾造成的,中间""是没有内容进行分割
console.log(str2.split("u").length - 1); // 2 真正的出现次数
3.4.2 concat() 合并
str.concat(str1, str...)
连接两个或更多字符串,并返回新的字符串。
- 一个或多个字符串合并
- 返回值:返回合并后的字符串。不改变原字符串
- 性能不如
+=
let str1 = "ni";
let str2 = "hao";
let str3 = "ya";
console.log(str1.concat(str2)); // nihao
console.log(str1.concat(str2,str3)); // nihaoya
3.4.3 repeat() 复制
str.repeat(count)
复制字符串指定次数,并将它们连接在一起返回。
count
:复制次数- 返回值:复制连接后的字符串。不影响原字符串。
let str = "una";
console.log(str.repeat(2)) // 'unauna'
3.5 比较字符串
3.5.1 charCodeAt()/codePointAt() 字符串 => 编码
JavaScript 内部,字符以 UTF-16 的格式储存,每个字符固定为2个字节。对于那些需要4个字节储存的字符(Unicode 码点大于0xFFFF的字符),JavaScript 会认为它们是两个字符。
charCodeAt()
方法只能分别返回前两个字节和后两个字节的值- 如果下标不存在,则返回一个
NaN
- 如果下标不存在,则返回一个
codePointAt()
方法,能够正确处理 4 个字节储存的字符,返回一个字符的码点- 如果下标不存在,则返回一个
undefined
- 如果下标不存在,则返回一个
var s = "𠮷";
s.length // 2
s.charAt(0) // ''
s.charAt(1) // ''
s.charCodeAt(0) // 55362
s.charCodeAt(1) // 57271
上面代码中,汉字“𠮷”(注意,这个字不是“吉祥”的“吉”)的码点是0x20BB7,UTF-16 编码为0xD842 0xDFB7(十进制为55362 57271),需要4个字节储存。对于这种4个字节的字符,JavaScript 不能正确处理,字符串长度会误判为2,而且charAt()
方法无法读取整个字符,charCodeAt()
方法只能分别返回前两个字节和后两个字节的值。
let s = '𠮷a';
s.codePointAt(0) // 134071
s.codePointAt(1) // 57271
s.codePointAt(2) // 97
上面代码中,JavaScript 将“𠮷a”视为三个字符,codePointAt 方法在第一个字符上,正确地识别了“𠮷”,返回了它的十进制码点 134071(即十六进制的20BB7)。在第二个字符(即“𠮷”的后两个字节)和第三个字符“a”上,codePointAt()方法的结果与charCodeAt()方法相同。
codePointAt()
方法是测试一个字符由两个字节还是由四个字节组成的最简单方法。
function is32Bit(c) {
return c.codePointAt(0) > 0xFFFF;
}
is32Bit("𠮷") // true
is32Bit("a") // false
codePointAt()
方法返回的是码点的十进制值,如果想要十六进制的值,可以使用toString()
方法转换一下。
let s = '𠮷a';
s.codePointAt(0).toString(16) // "20bb7"
s.codePointAt(2).toString(16) // "61"
你可能注意到了,codePointAt()
方法的参数,仍然是不正确的。比如,上面代码中,字符a在字符串s的正确位置序号应该是 1,但是必须向codePointAt()
方法传入 2。解决这个问题的一个办法是使用for…of
循环,因为它会正确识别 32 位的 UTF-16 字符。
let s = '𠮷a';
for (let ch of s) {
console.log(ch.codePointAt(0).toString(16));
}
// 20bb7
// 61
另一种方法也可以,使用 扩展运算符(…) 进行展开运算。
let arr = [...'𠮷a']; // arr.length === 2
arr.forEach(
ch => console.log(ch.codePointAt(0).toString(16))
);
// 20bb7
// 61
3.5.2 fromCharCode()/fromCodePoint() 编码 => 字符串
基本与 charCodeAt()/codePointAt()
相同,区别是 fromCharCode()/fromCodePoint
()是将 Unicode 编码转为字符。
String.fromCharCode(num1, num2...)
String.fromCodePoint(num1, num2...)
let n = String.fromCharCode(72,69,76,76,79);
console.log(n); // 'HELLO'
console.log( String.fromCodePoint(90) ); // Z
// 在十六进制系统中 90 为 5a
console.log( '\u005a' ); // Z
3.5.3 正确的比较 str.localeCompare(str2)
执行字符串比较的“正确”算法比看起来更复杂,因为不同语言的字母都不相同。因此浏览器需要知道要比较的语言。
幸运的是,所有现代浏览器(IE10- 需要额外的库 Intl.JS) 都支持国际化标准 ECMA-402。
它提供了一种特殊的方法来比较不同语言的字符串,遵循它们的规则。
调用 str.localeCompare(str2)
会根据语言规则返回一个整数,这个整数能指示字符串 str 在排序顺序中排在字符串 str2 前面、后面、还是相同:
- 如果 str 排在 str2 前面,则返回负数。
- 如果 str 排在 str2 后面,则返回正数。
- 如果它们在相同位置,则返回 0。
console.log( 'Österreich'.localeCompare('Zealand') ); // -1
这个方法实际上在 文档 中指定了两个额外的参数,这两个参数允许它指定语言(默认语言从环境中获取,字符顺序视语言不同而不同)并设置诸如区分大小写,或应该将 "a" 和 "á" 作相同处理等附加的规则。
3.6 去掉首尾空白符 trim()
str.trim()
去掉首尾空白符
- 空白符包括:空格、制表符 tab、换行符等其他空白符等。
- 不会改变原字符串。
- 不适用于 null, undefined, Number 类型。
let str = " una ";
let str1 = " hello una ";
console.log(str.trim()); // 'una'
console.log(str1.trim()); // 'hello una' 中间的空格去不掉的哦
3.7 正则相关
3.7.1 match() 查找一个或多个正则表达式的匹配,全局标志返回数组
string.match(regexp)
查找一个或多个正则表达式的匹配,返回匹配结果数组。
regexp
:规定要匹配的模式的 RegExp 对象。- 返回值:存放匹配结果的数组。如果没找到匹配结果,返回
null
。 - 很大程度上有赖于
regexp
是否具有标志g
。如果regexp
没有标志g
,那么match()
方法就只能在stringObject
中执行一次匹配。
let str = "The rain in SPAIN stays mainly in the plain";
let n = str.match(/ain/);
let n1 = str.match(/ain/g);
console.log(n)
console.log(n1)
3.7.2 replace() 查找与正则表达式相匹配的值,进行替换
str.replace(value, newvalue)
在字符串中查找匹配的子串,替换文本或替换与正则表达式匹配的子串。
value
:子字符串或 RegExp 对象newvalue
:一个字符串值。规定替换文本或生成替换文本的函数。- 返回值:匹配后的字符串。不影响原字符串。
let str = "Hello una! Hello una!";
let n = str.replace("Hello","Hi");
let n1 = str.replace(/Hello/g,"Hi");
console.log(n) // 'Hi una! Hello una!'
console.log(n1) // 'Hi una! Hi una!'
3.7.3 search() 查找与正则表达式相匹配的值,返回其起始位置
str.search(value)
查找与正则表达式相匹配的值
value
:查找的字符串或者正则表达式。- 返回值:匹配的 String 对象起始位置。
let str = "Hello una! Hello una!";
let n = str.search("una");
console.log(n) // 6'
3.8 转换大小写
toLowerCase()
和 toUpperCase()
方法可以改变大小写,返回新字符串:
console.log( 'Interface'.toUpperCase() ); // INTERFACE
console.log( 'Interface'.toLowerCase() ); // interface
str.toLocaleLowerCase()
根据本地主机的语言环境把字符串转换为小写。str.toLocaleUpperCase()
根据本地主机的语言环境把字符串转换为大写。
3.9 其他方法
-
str.valueOf()
返回某个字符串对象的原始值。let str = "a"; console.log(str.valueOf()); // 'a'
-
str.toString()
返回一个字符串。let str = 1; console.log(str.toString()); // '1'
……更多内容细节请参见 手册。
七、Symbol 类型
1、类型基础
“Symbol” 值表示唯一的标识符。可以使用 Symbol()
来创建这种类型的值:
// id 是 symbol 的一个实例化对象
let id = Symbol();
创建时,我们可以给 Symbol
一个描述(也称为 Symbol 名),这在代码调试时非常有用:
// id 是描述为 "id" 的 Symbol
let id = Symbol("id");
Symbol 保证是唯一的。即使我们创建了许多具有相同描述的 Symbol,它们的值也是不同。 描述只是一个标签,不影响任何东西。
let id1 = Symbol("id");
let id2 = Symbol("id");
console.log(id1 == id2); // false
1.1 Symbol 不会被自动转换为字符串
JavaScript 中的大多数值都支持字符串的隐式转换。例如,我们可以 console.log
任何值,都可以生效。Symbol 比较特殊,它不会被自动转换。
例如,这个 console.log
将会提示出错:
let id = Symbol("id");
console.log(id); // 类型错误:无法将 Symbol 值转换为字符串。
这是一种防止混乱的“语言保护”,因为字符串和 Symbol 有本质上的不同,不应该意外地将它们转换成另一个。
如果我们真的想显示一个 Symbol,我们需要在它上面调用 .toString()
,如下所示:
let id = Symbol("id");
console.log(id.toString()); // Symbol(id),现在它有效了
或者获取 symbol.description
属性,只显示描述(description):
let id = Symbol("id");
console.log(id.description); // id
2、全局 symbol
正如我们所看到的,通常所有的 Symbol 都是不同的,即使它们有相同的名字。但有时我们想要名字相同的 Symbol 具有相同的实体。例如,应用程序的不同部分想要访问的 Symbol "id" 指的是完全相同的属性。
为了实现这一点,这里有一个 全局 Symbol 注册表。我们可以在其中创建 Symbol 并在稍后访问它们,它可以确保每次访问相同名字的 Symbol 时,返回的都是相同的 Symbol。
要从注册表中读取(不存在则创建)Symbol,请使用 Symbol.for(key)
。
该调用会检查全局注册表,如果有一个描述为 key
的 Symbol,则返回该 Symbol,否则将创建一个新 Symbol(Symbol(key)
),并通过给定的 key 将其存储在注册表中。
// 从全局注册表中读取
let id = Symbol.for("id"); // 如果该 Symbol 不存在,则创建它
// 再次读取(可能是在代码中的另一个位置)
let idAgain = Symbol.for("id");
// 相同的 Symbol
console.log( id === idAgain ); // true
注册表内的 Symbol 被称为 全局 Symbol。如果我们想要一个应用程序范围内的 Symbol,可以在代码中随处访问 —— 这就是它们的用途。
2.1 Symbol.keyFor
对于全局 Symbol,不仅有 Symbol.for(key)
按名字返回一个 Symbol,还有一个反向调用:Symbol.keyFor(sym)
,它的作用完全反过来:通过全局 Symbol 返回一个名字。
例如:
// 通过 name 获取 Symbol
let sym = Symbol.for("name");
let sym2 = Symbol.for("id");
// 通过 Symbol 获取 name
console.log( Symbol.keyFor(sym) ); // name
console.log( Symbol.keyFor(sym2) ); // id
Symbol.keyFor
内部使用全局 Symbol 注册表来查找 Symbol 的键。所以它不适用于非全局 Symbol。如果 Symbol 不是全局的,它将无法找到它并返回 undefined
。
let globalSymbol = Symbol.for("name");
let localSymbol = Symbol("name");
console.log( Symbol.keyFor(globalSymbol) ); // name,全局 Symbol
console.log( Symbol.keyFor(localSymbol) ); // undefined,非全局
console.log( localSymbol.description ); // name
3、系统 Symbol
JavaScript 内部有很多“系统” Symbol,我们可以使用它们来微调对象的各个方面。
它们都被列在了 众所周知的 Symbol 表的规范中:
Symbol.hasInstance
Symbol.isConcatSpreadable
Symbol.iterator
Symbol.toPrimitive
- ……等等。
例如,Symbol.toPrimitive
允许我们将对象描述为原始值转换。我们很快就会看到它的使用。
当我们研究相应的语言特征时,我们对其他的 Symbol 也会慢慢熟悉起来。
4、总结
Symbol 是唯一标识符的基本类型
Symbol 是使用带有可选描述(name)的 Symbol()
调用创建的。
Symbol 总是不同的值,即使它们有相同的名字。如果我们希望同名的 Symbol 相等,那么我们应该使用全局注册表:Symbol.for(key)
返回( 如果需要的话则创建 )一个以 key
作为名字的全局 Symbol。使用 Symbol.for 多次调用 key 相同的 Symbol 时,返回的就是同一个 Symbol。
Symbol 有两个主要的使用场景:
- “隐藏” 对象属性。 如果我们想要向“属于”另一个脚本或者库的对象添加一个属性,我们可以创建一个 Symbol 并使用它作为属性的键。Symbol 属性不会出现在
for..in
中,因此它不会意外地被与其他属性一起处理。并且,它不会被直接访问,因为另一个脚本没有我们的 symbol。因此,该属性将受到保护,防止被意外使用或重写。
因此我们可以使用 Symbol 属性“秘密地”将一些东西隐藏到我们需要的对象中,但其他地方看不到它。
- JavaScript 使用了许多系统 Symbol,这些 Symbol 可以作为
Symbol.*
访问。我们可以使用它们来改变一些内置行为。例如,在本教程的后面部分,我们将使用Symbol.iterator
来进行 迭代 操作,使用Symbol.toPrimitive
来设置 对象原始值的转换 等等。
从技术上说,Symbol 不是 100% 隐藏的。有一个内置方法 Object.getOwnPropertySymbols(obj)
允许我们获取所有的 Symbol。还有一个名为 Reflect.ownKeys(obj)
的方法可以返回一个对象的 所有 键,包括 Symbol。所以它们并不是真正的隐藏。但是大多数库、内置方法和语法结构都没有使用这些方法。
七、Array
对象允许存储键值集合,但很多时候我们发现还需要 有序集合,里面的元素都是按顺序排列的。例如,我们可能需要存储一些列表,比如用户、商品以及 HTML 元素等。
这里使用对象就不是很方便了,因为对象不能提供能够管理元素顺序的方法。我们不能在已有的元素“之间”插入一个新的属性。这种场景下对象就不太适用了。
这时一个特殊的数据结构数组(Array)就派上用场了,它能存储有序的集合。
1、类型基础
1.1 声明
- 声明:
// 方括号(常见用法)
let fruits = [item1, item2, ...];
// new Array (极其少见)
let arr = new Array(item1, item2, ...);
new Array(number)
会创建一个 指定了长度,却没有任何项 的数组,所有元素都是 undefined
。
数组元素从 0
开始编号。我们可以通过方括号中的数字获取/修改/新增元素。
数组是一种特殊的对象。使用方括号来访问属性 arr[0]
实际上是来自于对象的语法。它其实与 obj[key]
相同,其中 arr
是对象,而数字用作键(key)。
-
数组可以存储任何类型的元素。
// 混合值 let arr = [ 'Apple', { name: 'John' }, true, function() { alert('hello'); } ]; // 获取索引为 1 的对象然后显示它的 name console.log( arr[1].name ); // John // 获取索引为 3 的函数并执行 arr[3](); // hello
- 多维数组
数组里的项也可以是数组。我们可以将其用于多维数组,例如存储矩阵:
let matrix = [ [1, 2, 3], [4, 5, 6], [7, 8, 9] ]; console.log( matrix[1][1] ); // 最中间的那个数
-
length
属性:最大的数字索引值加一
length
属性是数组的长度。准确来说,它实际上不是数组里元素的个数,而是最大的数字索引值加一。
let fruits = [];
fruits[123] = "Apple";
console.log( fruits.length ); // 124
length
属性是可写的。如果我们手动增加它,则不会发生任何有趣的事儿。但是如果我们减少它,数组就会被截断。该过程是不可逆的。
let arr = [1, 2, 3, 4, 5];
arr.length = 2; // 截断到只剩 2 个元素
console.log( arr ); // [1, 2]
arr.length = 5; // 又把 length 加回来
console.log( arr[3] ); // undefined:被截断的那些数值并没有回来
所以,清空数组最简单的方法就是:arr.length = 0;
。
arr.toString()
,返回以逗号隔开的元素列表。
let arr = [1, 2, 3];
console.log( arr ); // 1,2,3
console.log( String(arr) === '1,2,3' ); // true
此外,我们试试运行一下这个:
console.log( [] + 1 ); // "1"
console.log( [1] + 1 ); // "11"
console.log( [1,2] + 1 ); // "1,21"
数组没有 Symbol.toPrimitive
,也没有 valueOf
,它们只能执行 toString
进行转换,所以这里 []
就变成了一个空字符串,[1]
变成了 "1"
,[1,2]
变成了 "1,2"
。
当 "+"
运算符把一些项加到字符串后面时,加号后面的项也会被转换成字符串,所以下一步就会是这样:
console.log( "" + 1 ); // "1"
console.log( "1" + 1 ); // "11"
console.log( "1,2" + 1 ); // "1,21"
1.2 数组增删
-
队列(queue)是最常见的使用数组的方法之一。在计算机科学中,这表示支持两个操作的一个有序元素的集合:
push
在末端添加一个元素.shift
取出队列首端的一个元素,整个队列往前移,这样原先排第二的元素现在排在了第一。
队列的应用在实践中经常会碰到。例如需要在屏幕上显示消息队列。
-
数组还有另一个用例,就是数据结构 栈。
它支持两种操作:
push
在末端添加一个元素.pop
从末端取出一个元素.
所以新元素的添加和取出都是从“末端”开始的。
对于栈来说,最后放进去的内容是最先接收的,也叫做 LIFO(Last-In-First-Out),即后进先出法则。而与队列相对应的叫做 FIFO(First-In-First-Out),即先进先出。
JavaScript 中的数组既可以用作队列,也可以用作栈。它们允许你从首端/末端来添加/删除元素。
这在计算机科学中,允许这样的操作的数据结构被称为 双端队列(deque)
- 作用于数组末端的方法:
pop/push
- 作用于数组首端的方法:
shift/unshift
push
和unshift
方法都可以一次添加多个元素:
1.2.1 unshift()/push() 将一个或多个元素添加到数组的开头/结尾,并返回该数组的长度
arr.unshift(num1, num2...)
将一个或多个元素添加到数组的开头,并返回该数组的长度
arr.push(num1, num2...)
将一个或多个元素添加到数组的结尾,并返回该数组的长度
/* unshift */
let arr = [1, 2, 3];
let len = arr.unshift(4,5,6,7);
console.log(arr); // [4,5,6,7,1,2,3]
console.log(len); // 7
/* push */
let arr = [1,2,3];
let len = arr.push(4,5,6,7);
console.log(len); // 7
console.log(arr); // [1,2,3,4,5,6,7]
1.2.2 shift()/pop() 删除数组的第一个/最后一个元素,并返回被删除的值
arr.shift(num)
删除数组的第一个元素,并返回被删除的值
arr.pop(num)
删除数组的最后一个元素,并返回被删除的值
/* shift */
let arr = ['a',1, 2, 3];
let len = arr.shift();
console.log(len); // 'a'
console.log(arr); // [1, 2, 3]
let arr2 = [];
console.log(arr2.shift()); // undefined
/* pop */
let arr = [1, 2, 3];
let a = arr.pop();
console.log(a) // 3
let arr2 = [];
console.log(arr2.pop()); // undefined
1.2.3 性能
push/pop
方法运行的比较快,而 shift/unshift
比较慢。
为什么作用于数组的末端会比首端快呢?让我们看看在执行期间都发生了什么:
fruits.shift(); // 从首端取出一个元素
只获取并移除数字 0
对应的元素是不够的。其它元素也需要被重新编号。
shift
操作必须做三件事:
- 移除索引为
0
的元素。 - 把所有的元素向左移动,把索引
1
改成0
,2 改成1
以此类推,对其重新编号。 - 更新
length
属性。
数组里的元素越多,移动它们就要花越多的时间,也就意味着越多的内存操作。
unshift
也是一样:为了在数组的首端添加元素,我们首先需要将现有的元素向右移动,增加它们的索引值。
那 push/pop
是什么样的呢?它们不需要移动任何东西,因为其它元素都保留了各自的索引。如果从末端移除一个元素,pop
方法只需要清理索引值并缩短 length
就可以了。
1.3 使用变量对象的方法遍历数组
-
最古老的方式就是
for
循环:let arr = ["Apple", "Orange", "Pear"]; for (let i = 0; i < arr.length; i++) { console.log( arr[i] ); }
-
但对于数组来说还有另一种循环方式,
for..of
:let fruits = ["Apple", "Orange", "Plum"]; // 遍历数组元素 for (let fruit of fruits) { console.log( fruit ); }
for..of
不能获取当前元素的索引,只是获取元素值,但大多数情况是够用的。而且这样写更短。 -
技术上来讲,因为数组也是对象,所以使用
for..in
也是可以的:let arr = ["Apple", "Orange", "Pear"]; for (let key in arr) { console.log( arr[key] ); // Apple, Orange, Pear }
但这其实是一个很不好的想法。会有一些潜在问题存在:
-
for..in
循环会遍历 所有属性,不仅仅是这些数字属性。在浏览器和其它环境中有一种称为“类数组”的对象,它们 看似是数组。也就是说,它们有
length
和索引属性,但是也可能有其它的非数字的属性和方法,这通常是我们不需要的。for..in
循环会把它们都列出来。所以如果我们需要处理类数组对象,这些“额外”的属性就会存在问题。 -
for..in
循环适用于普通对象,并且做了对应的优化。但是不适用于数组,因此速度要慢 10-100 倍。当然即使是这样也依然非常快。只有在遇到瓶颈时可能会有问题。但是我们仍然应该了解这其中的不同。
通常来说,我们不应该用
for..in
来处理数组。 -
1.4 不要使用 ==
比较数组
JavaScript 中的数组与其它一些编程语言的不同,不应该使用 ==
运算符比较 JavaScript 中的数组。
该运算符不会对数组进行特殊处理,它会像处理 任意对象 那样处理数组。
让我们回顾一下规则:
- 仅当两个对象引用的是同一个对象时,它们才相等
==
。 - 如果
==
左右两个参数之中有一个参数是对象,另一个参数是原始类型,那么该对象将会被转换为原始类型。 null
和undefined
相等==
,且各自不等于任何其他的值。
严格比较 ===
更简单,因为它不会进行类型转换。
所以,如果我们使用 ==
来比较数组,除非我们比较的是两个引用同一数组的变量,否则它们永远不相等。
console.log( [] == [] ); // false
console.log( [0] == [0] ); // false
从技术上讲,这些数组是不同的对象。所以它们不相等。==
运算符不会进行逐项比较。
与原始类型的比较也可能会产生看似很奇怪的结果:
console.log( 0 == [] ); // true
console.log('0' == [] ); // false
在这里的两个例子中,我们将原始类型和数组对象进行比较。数组 []
将被转换为原始类型以进行比较,被转换成了一个空字符串 ''
。
// 在 [] 被转换为 '' 后
console.log( 0 == '' ); // true,因为 '' 被转换成了数字 0
console.log('0' == '' ); // false,没有进一步的类型转换,是不同的字符串
那么,我们应该如何对数组进行比较呢?
很简单,不要使用 ==
运算符。而是,可以在循环中或者使用迭代方法逐项地比较它们。
2、常用方法
2.1 添加/移除/截取、合并
2.1.1 splice() 添加/移除数组元素
arr.splice(start[,num,item1,item2...])
删除指定位置的元素,并在该位置上添加新元素,返回删除的元素组成的数组。(改变原数组)
start
:删除的起始位置。- 如果
start
大于arr.length
,不删除。 - 如果是一个负数(负数小于或等于
-arr.length
,等同于0
),从右到左查找对应的起始位置。
- 如果
deleteCount
:删除的个数,默认删除到数组的末尾。- 如果小于
0
,或者是NaN
,则等同于0
,不删除何元素。
- 如果小于
item1,item2...
:从删除的位置,添加的一个或多个元素。- 返回值是所有删除的元素组成的数组。如果没有删除任何元素,将会得到一个空数组。
// 删除
let arr = [1,2,3,4,5,6];
let del = arr.splice(1,2);
console.log(del); // [2,3]
console.log(arr); // [1,4,5,6]
// 替换
let arr = [1,2,3,4,5,6];
arr.splice(1,2,'b','c');
console.log(arr); // [1,'b','c',4,5,6]
let arr = [1,2,3,4,5,6];
arr.splice(50);
console.log(arr); // [1,2,3,4,5,6] start 大于 length,不删除
let arr = [1,2,3,4,5,6];
arr.splice(-2);
console.log(arr); // [1,2,3,4] start<0,从右到左
let arr = [1,2,3,4,5,6];
arr.splice(1,-10);
console.log(arr); // [1,2,3,4,5,6]; num<0或num为NaN,不删除
// 添加,如果不删除元素,但是又存在第三个或者3+的参数,就会有添加的功能
let arr = [1,2,3,4,5,6];
arr.splice(1,0,'a','b','c');
console.log(arr); // [1,'a','b','c',2,3,4,5,6];
2.1.2 slice() 截取指定区域的数组元素
arr.slice(begin [,end])
截取指定区域的数组元素,并返回一个新数组。不改变原有数组。
- begin:截取的起始位置,默认为 0。
- 如果是一个负数(负数小于或等于
-arr.length
,等同于0
),从右到左查找对应的起始位置。
- 如果是一个负数(负数小于或等于
- end:截取的结束位置,默认到数组结尾。
- 大于
arr.length
,等同于arr.length
; - 如果是一个负数(负数小于或等于
-arr.length
,等同于0
),从右到左查找对应的起始位置。
- 大于
- 包含
begin
,不包含end
与上文字符串的截取
slice()
一致。
我们也可以不带参数地调用它:arr.slice()
会创建一个 arr
的副本(浅拷贝)。其通常用于获取副本,以进行不影响原始数组的进一步转换。
let arr = ["a","b","c","d","e"];
console.log(arr.slice(2)); // ['c','d','e']
console.log(arr); // ["a","b","c","d","e"]
console.log(arr.slice(50)); // []
console.log(arr.slice(-2)); // ['d','e']
console.log(arr.slice(-50)); // ["a","b","c","d","e"]
console.log(arr.slice(2,-1)); // ['c','d']
console.log(arr.slice(0, -50)); // []
console.log(arr.slice(0, 50)); // ["a","b","c","d","e"]
// 复杂数据类型会发生传址问题
var arr3 = ['a',"b","c"];
var arr4 = arr3; // 地址一致
arr4[0] = "vv";
console.log(arr4); // ['vv',"b","c"]
console.log(arr3); // ['vv',"b","c"]
// slice 可用于拷贝数组(只适用于简单数据类型,因为是浅拷贝)
var arr2 = arr.slice();
arr2[0] = "123";
console.log(arr2); // ["123","b","c","d","e"]
console.log(arr); // ["a","b","c","d","e"]
// 浅拷贝
var arr6 = [{a:10,b:20},"a"];
var arr7 = arr6.slice();
arr6[1] = "123";
arr6[0].a = "abc";
console.log(arr6); // [{a:'abc',b:20},"123"]
console.log(arr7); // [{a:'abc',b:20},"123"]
深拷贝、浅拷贝、赋值
- 深拷贝:数据拷贝过来后,和原数组之间完全没有任何关系了;
- 浅拷贝:数据拷贝过来后,里面的简单数据类型是没有关联了,但是里面的复杂数据类型,依旧是有关联的;
- 赋值:数据不管是基本类型,还是复杂类型,都和原来的是相关联的。
2.1.3 concat(arr1,arr2...) 合并数组
arr.concat(arr1,arr2...)
返回一个新数组,其中包含来自于其他数组和其他项的值。(不改变原数组)
let arr1 = ["ni"];
let arr2 = ["hao"];
let arr3 = ["ya"];
console.log(arr1.concat(arr2,arr3)); // ['ni','hao','ya']
console.log(arr1); // ['ni']
let arr4 = [1, 2];
console.log(arr4.concat([3,4])); // [1,2,3,4,]
console.log(arr4.concat([3,4], [5,6])); // [1,2,3,4,5,6]
console.log(arr4.concat([3,4], 5, 6)); // [1,2,3,4,5,6]
通常,它只复制数组中的元素。其他对象,即使它们看起来像数组一样,但仍然会被作为一个整体添加:
let arr = [1, 2];
let arrayLike = {
0: "something",
length: 1
};
console.log( 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
};
console.log( arr.concat(arrayLike) ); // 1,2,something,else
2.2 在数组中搜索指定元素 indexOf/lastIndexOf 和 includes
arr.indexOf
、arr.lastIndexOf
和 arr.includes
方法与字符串操作具有相同的语法,并且作用基本上也与字符串的方法相同,只不过这里是对数组元素而不是字符进行操作:
arr.indexOf(item, index)
从索引index
开始搜索item
,如果找到则返回索引,否则返回-1
。arr.lastIndexOf(item, index)
—— 和上面相同,只是从右向左搜索。arr.includes(item, index)
—— 从索引index
开始搜索item
,如果找到则返回true
,否则返回false
。
/* indexOf */
var arr = ["a","b","c","a"];
console.log(arr.indexOf("vv")); // -1
console.log(arr.indexOf("a", -2)); // 3
console.log(arr.indexOf("a", 50)); // -1
console.log(arr.indexOf("a", -50)); // 0
/* lastIndexOf */
let arr = ["a", "b", "c", "a"];
console.log(arr.lastIndexOf("a", arr.length - 2)); //0
console.log(arr.lastIndexOf("a", 50)); // 3
console.log(arr.lastIndexOf("vv")); // -1
/* includes */
let arr = [1, 0, false];
console.log( arr.includes(1) ); // true
请注意,这些方法使用的是严格相等 ===
比较。所以如果我们搜索 false
,会精确到的确是 false
而不是数字 0
。
如果我们想检查是否包含某个元素,并且不想知道确切的索引,那么 arr.includes
是首选。
此外,includes
的一个非常小的差别是它能正确处理 NaN
,而不像 indexOf/lastIndexOf
:
const arr = [NaN];
console.log( arr.indexOf(NaN) ); // -1(应该为 0,但是严格相等 === equality 对 NaN 无效)
console.log( arr.includes(NaN) );// true(这个结果是对的)
2.3 遍历 与 查找(符合特定条件)
-
arr.forEach()
对数组中的每一个元素,执行一次提供的函数,返回undefined
。不能中断循环。 -
arr.find()
对数组中的每一个元素,执行一次提供的函数,查找 第一个 具有特定条件的元素,找到返回该元素(结束查找),否则返回undefined
:- 如果
function()
返回true
,则搜索停止,并返回item
。如果没有搜索到,则返回undefined
。
- 如果
-
arr.findIndex()
与arr.find()
基本上是一样的,但它返回找到元素的索引,而不是元素本身。并且在未找到任何内容时返回-1
。 -
arr.filter()
对数组中的每一个元素,执行一次提供的函数,查找符合条件的所有元素,并作为一个新数组返回。 -
arr.map()
对数组中的每一个元素,执行一次提供的函数,返回执行结果组成的新数组。 -
arr.some()
测试数组中是否 至少有一个元素 通过了指定函数的测试,结果返回布尔值。 -
arr.every()
测试数组中是否 所有元素 都 通过了指定函数的测试,结果返回布尔值。
语法基本一致,以 为例:
arr.forEach(function(item, index, arr){
// ...
}, thisArg)
参数:
function()
:执行的函数item
:循环过程中的每一项元素index
:当前循环的元素对应的下标值arr
:当前数组
thisArg
:当前function
中的this
指向
几乎所有调用函数的数组方法(除了
sort
),都接受一个可选的附加参数thisArg
,thisArg
参数的值在function
中变为this
。
2.3.1 forEach()
// 详细写法
let arr = ['a','c','vv','l'];
let a = arr.forEach(function(ele,index,arr){
// console.log(this);
console.log(ele,index,arr);
},document);
console.log(a); // undefined
2.3.2 find() 与 findIndex()
let users = [
{id: 1, name: "John"},
{id: 2, name: "Pete"},
{id: 3, name: "Mary"}
];
// 简略写法
// find
let user = users.find(item => item.id == 1);
console.log(user.name); // John
// findIndex
let index = users.findIndex(item => item.id == 1);
console.log(index); // 0
2.3.3 filter()
let arr = [10, 20, 4, 50, 60, 75, 3].filter(item => item > 50);
console.log(arr); // [50, 60, 75]
2.3.4 map()
let arr = [10, 20, 4, 50, 60, 75, 3].map(item => item * 2);
console.log(newArr); // [20, 40, 8, 100, 120, 150, 6]
2.3.5 some()
let result = [10, 20, 4, 50, 60, 75, 3].some(item => item > 50);
console.log(result); // true
2.3.6 every()
let result = [10, 20, 4, 50, 60, 75, 3].every(item => item > 50);
console.log(result); // false
2.4 累加器 reduce()/reduceRight()
arr.reduce()
对数组中的每一个元素执行一次函数,将其结果累加起来,返回累加后的值。
arr.forEach(function(result, item, index, arr){
// ...
}, initValue)
callback()
:执行的函数result
:结果item
:当前循环的元素index
:当前循环的元素对应的下标值arr
:当前数组
initValue
:对于result
进行初始化。默认为数组第一个元素。;
应用函数时,上一个函数调用的结果将作为第一个参数传递给下一个函数。
因此,第一个参数本质上是累加器,用于存储所有先前执行的组合结果。最后,它成为 reduce()
的结果。
let result = [10, 20, 4, 50, 60, 75, 3].reduce((result, item) => {
return result + item; // 不 return,result 默认返回 undefined
}, 0);
console.log(result); // 222
我们也可以省略初始值:
// 删除 reduce 的初始值(没有 0)
let result = [10, 20, 4, 50, 60, 75, 3].reduce((result, item) => result + item);
console.log(result); // 222
结果是一样的。这是因为如果没有初始值,那么 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()
方法的功能一样,只是遍历为从右到左。
2.5 转换数组
2.5.1 sort() 排序
arr.sort
方法对数组进行 原位(in-place) 排序,更改元素的顺序。(译注:原位是指在此数组内,而非生成一个新数组。)
它还返回排序后的数组,但是返回值通常会被忽略,因为修改了 arr
本身。
arr.sort( function(a,b){} )
- 排序函数。默认根据字符串的 Unicode 编码 进行排序。要使用我们自己的排序顺序,需要提供一个排序函数。比较函数只需要返回一个正数表示“大于”,一个负数表示“小于”。
- 若
a - b
的结果- 大于0:b 排到 a 的前面
- 小于0:a 排到 b 的前面
- 等于0:a 和 b 的位置不变
let arr = ['b',"z",'l','a'];
arr.sort();
console.log(arr); // ['a','b','l','z']
// 默认根据字符串的 Unicode 编码 进行排序。即所有元素都被转换为字符串进行排序
let num = [6,50,12,40];
num.sort();
console.log(num); // [12, 40, 50, 6]
// 比较函数只需要返回一个正数表示“大于”,一个负数表示“小于”。
let num = [6,50,12,40];
num.sort(function(a,b){
// return b - a; // num 为 [50,40,12,6]
return a - b; // num 为 [6,12,40,50]
})
对于许多字母,最好使用 str.localeCompare
方法正确地对字母进行排序,例如 Ö
:
let countries = ['Österreich', 'Andorra', 'Vietnam'];
console.log( countries.sort( (a, b) => a > b ? 1 : -1) ); // Andorra, Vietnam, Österreich(错的)
console.log( countries.sort( (a, b) => a.localeCompare(b) ) ); // Andorra,Österreich,Vietna
2.5.1.1 使用 Math.random()
进行 随机排序
let arr = ["a","b","c","d"];
arr.sort((a, b) => Math.random() - 0.5;)
console.log(arr); // ["b", "d", "c", "a"] 随机排序,每一次刷新都是不同的数组
2.5.2 reverse() 倒置
arr.reverse()
倒置数组,并返回倒置后的数组。改变原有数组。
let arr = ['a','b','c'];
console.log(arr.reverse()); // ['c','b','a']
console.log(arr); // ['c','b','a']
2.5.3 split()/join() 字符串与数组转换
-
str.split(delim [, length])
通过给定的分隔符delim
将字符串分割成一个数组,返回分割后的数组。delim
:分隔符length
:指定返回的数组长度
let str = 'Bilbo, Gandalf, Nazgul' let arr = str.split(', '); console.log(arr) // ["Bilbo", "Gandalf", "Nazgul"] // 指定返回的数组长度 let arr1 = str.split(', ', 2); console.log(arr1) // ["Bilbo", "Gandalf"]
-
arr.join(glue)
将数组元素用glue
粘合成字符串,返回粘合后的字符串。glue
:以什么符号对数组元素进行拼接。
let arr = ['Bilbo', 'Gandalf', 'Nazgul']; let str = arr.join(','); console.log(str); // Bilbo, Gandalf, Nazgul
2.6 Array.isArray 判断数组
数组是基于对象的,不构成单独的语言类型。所以 typeof
不能帮助从数组中区分出普通对象:
console.log(typeof {}); // object
console.log(typeof []); // same
……但是数组经常被使用,因此有一种特殊的方法用于判断:Array.isArray(value)
。如果 value
是一个数组,则返回 true
;否则返回 false
。
console.log(Array.isArray({})); // false
console.log(Array.isArray([])); // true
2.7 fill() 数组填充
arr.fill(value[, start[, end]])
用一个固定值填充一个数组中从起始索引到终止索引内的全部元素。不包括终止索引
- value:用来填充数组元素的值。
- start:起始索引,默认值为0。
- end:终止索引,默认值为 arr.length
let arr = ["a","b","c","d","e"];
arr.fill("f",1,3);
console.log(arr); // ["a","f","f","d","e"]
2.8 扁平化多维数组
2.8.1 flat()
arr.flat([depth])
扁平化多维数组,返回新数组
depth
:指定要提取嵌套数组的结构深度,默认值为 1。- 返回一个包含将数组与子数组中所有元素的新数组
let arr = [["小明","18"],["小红","19"]];
console.log(arr.flat()); // ["小明","18","小红","19"]
// 无限层:有多少层提取多少层
let arr1 = [
[1,2],
[3,4],
[
[6,7],
[
[8],
[9,10]
]
]
];
console.log(arr1.flat(Infinity)); // [1,2,3,4,6,7,8,9,10]
2.8.2 flatMap()
arr.flatMap(function callback(currentValue[, index[, array]]) {
// 返回新数组的元素
}[, thisArg])
方法首先使用映射函数映射每个元素,然后将结果压缩成一个新数组。它与 map
和 深度值1的 flat
几乎相同,但 flatMap
通常在合并成一种方法的效率稍微高一些。
callback
:可以生成一个新数组中的元素的函数,可以传入三个参数:currentValue
:当前正在数组中处理的元素index
:数组中正在处理的当前元素的索引。array
:被调用的数组
thisArg
:执行 callback 函数时 使用的this
值- 返回一个包含将数组与子数组中所有元素的新数组
let arr = [["小明","18"],["小红","19"]];
let arr2 = arr.flatMap(item => {
item = item.filter((item, index) => retrun index == 0);
return item;
});
console.log(arr2); // ["小明","小红"]
2.9 转成数组
2.9.1 Array.of() 将参数转成一个数组
Array.of(element0[, element1[, ...[, elementN]]])
将参数转成一个数组,返回转换后的新数组
elementN
:要放入数组中的数据- 返回转换后的新数组
let arr = Array.of(1,2,3,4,5,5,6);
console.log(arr); // [1,2,3,4,5,5,6]
2.9.2 Array.form() 把类数组转换为数组
Array.from(arrayLike[, mapFn[, thisArg]])
将类数组转换成数组,返回转换后的新数组
arrayLike
:类数组mapFn
:类似 map 方法,循环类数组时的回调函数,返回值组成新数组thisArg
:mapFn 函数执行时的this
指向- 返回转换后的新数组
// 类数组,有下标、length
let datas = {
0: "a",
1: "b",
2: "c",
length: 3
};
// datas = Array.from(datas);
// 改变 this 指向,不要用箭头函数,箭头函数 this 指向 箭头函数声明时所在的作用域 的 this
datas = Array.from(datas,function(item,index){
console.log(item,index,this);
return item.toUpperCase();
},document);
console.log(datas); // ["A", "B", "C"]
2.10 copyWithin() 覆盖现有元素,数组长度不变
arr.copyWithin(target, start, end)
—— 将从位置 start
到 end
的所有元素复制到 自身 的 target
位置(覆盖现有元素,数组长度不变)。
- target:复制到指定目标索引位置
- start:元素复制的起始位置。
- end:停止复制的索引位置,默认为
arr.length
。如果为负值,表示倒数。 - 返回值:数组
let arr = [1, 2, 3, 4, 5, 6];
let result = arr.copyWithin(2, 0, 2);
console.log(result); // [1, 2, 1, 2, 5, 6]
3、数组方法备忘单
-
添加/删除元素:
push(...items)
—— 向尾端添加一个/多个元素,pop()
—— 从尾端提取一个元素,shift()
—— 从首端提取一个元素,unshift(...items)
—— 向首端添加一个/多个元素,splice(start, deleteCount, ...items)
—— 从start
开始删除deleteCount
个元素,并插入items
。slice(start, end)
—— 将从索引start
到索引end
(*不包括end
)的元素截取出来,组成一个新数组并返回。concat(...items)
—— 返回一个新数组:复制当前数组的所有元素,并向其中添加items
。如果items
中的任意一项是一个数组,那么就取其元素。
-
搜索元素:
indexOf/lastIndexOf(item, start)
—— 从索引start
开始搜索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
计算数组上的单个值,并在调用之间传递中间结果。累加器。
-
检查数组:
arr.some(func)
:如果任何结果为true
,则返回true
,否则返回false
。arr.every(func)
:如果所有结果为true
,则返回true
,否则返回false
。- 其行为类似于
||
和&&
运算符
-
判断是否为数组:
Array.isArray(arr)
:检查 arr 是否是一个数组。
注意,
sort
,reverse
和splice
方法修改的是数组本身。
这些是最常用的方法,它们覆盖 99% 的用例。但是还有其他几个:
-
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 实例,而不需要考虑参数的数量或类型。
有关完整列表,请参阅 手册。