JavaScript 基础(六):数据类型(一):基本数据类型

331 阅读1小时+

一、js 数据类型

  • 7 种基本数据类型(也称为原始类型):Undefined(未定义)、Null(未知)、Boolean(布尔值)、Number(数值)、String(字符串)、Symbol(符号:独一无二的值)、BigInt(任意长度的整数)

    • Null:用于未知(空) 的值 —— 只有一个 null 值的独立类型。
    • Undefined:用于未定义的值 —— 只有一个 undefined 值的独立类型。
    • Boolean:用于布尔值truefalse
    • Number:用于整数或浮点数,在 ±(2^53-1) 范围内的整数。
    • BigInt:用于任意长度的整数。是最近被添加到 js 语言中的。
    • String:用于字符串:一个字符串可以包含 0 个或多个字符。
  • 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 当作对象的原始类型

  1. 原始类型仍然是原始的。与预期相同,提供单个值
  2. JavaScript 允许访问字符串,数字,布尔值和 symbol 的方法和属性。
  3. 为了使它们起作用,创建了提供额外功能的特殊 “对象包装器”,使用后即被销毁

“对象包装器” 对于每种原始类型都是不同的,它们被称为 StringNumberBooleanSymbol。因此,它们提供了不同的方法。

例如,字符串方法 str.toUpperCase() 返回一个大写化处理的字符串。

let str = "Hello";

console.log( str.toUpperCase() ); // HELLO

以下是 str.toUpperCase() 中实际发生的情况:

  1. 字符串 str 是一个原始值。因此,在访问其属性时,会创建一个包含字符串字面值的特殊对象,并且具有有用的方法,例如 toUpperCase()
  2. 该方法运行并返回一个新的字符串
  3. 特殊对象被销毁,只留下原始值 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 没有任何方法

特殊的原始类型 nullundefined 是例外。它们没有对应的“对象包装器”,也没有提供任何方法。从某种意义上说,它们是“最原始的”。

尝试访问这种值的属性会导致错误:

console.log(null.test); // error

二、Undefined 和 Null

1、Undefined 类型

  • Undefined 类型只有一个值,就是特殊值 undefined

    • 当使用 varlet 声明了变量但没有初始化时,就相当于给变量赋予了 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

即使 nullundefined 有关系,它们的用途也是完全不一样的。如前所述,永远不必显式地将变量值设置为 undefined。但 null 不是这样的。任何时候,只要变量要保存对象,而当时又没有那个对象可保存,就要用 null 来填充该变量。这样就可以保持 null 是空对象指针的语义,并进一步将其与 undefined 区分开来。

三、Boolean 类型

1、类型基础

Boolean 类型有两个字面值:truefalse。这两个布尔值不同于数值,因此 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、空字符串、nullundefinedNaN)将变为 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 的范围可以从 236。默认情况下是 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.10.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 InfinityNAN

1.3.1 Infinity 无穷值

由于内存的限制,ECMAScript 并不支持表示这个世界上的所有数值。数值结果超出了 JavaScript 可以表示的范围,会被自动转换为一个特殊的 Infinity(无穷) 值,该值将不能再进一步用于任何计算。

  • -Infinity(负无穷大)、Infinity(正无穷大)

    • 我们可以通过 除以 0 来得到它。

      console.log(1 / 0); // Infinity
      
    • 使用 Number.NEGATIVE_INFINITYNumber.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,它类似于 === 一样对值进行比较,但它对于两种边缘情况更可靠:

  • 它适用于 NaNObject.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 转换为 1false 转换为 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€",并希望从中提取出一个数值。

这就是 parseIntparseFloat 的作用。

它们可以从字符串中“读取”数字(从第一个非空格字符开始),直到无法读取为止。如果发生 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 位,有以下两种方式可以实现这个需求:

  • 乘除法将数字乘以 10n 次幂(保留 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:

运算原生 BigIntJSBI
从 Number 创建a = BigInt(789)a = JSBI.BigInt(789)
加法c = a + bc = JSBI.add(a, b)
减法c = a - bc = 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);

字符串对象 => 类数组

类数组:类似数组,和数组长得非常的像,但是并不是数组,因为并不具备有数组的所有方法。

image.png

1.2 特殊字符

字符描述
\n换行
\r回车:不单独使用。Windows 文本文件使用两个字符 \r\n 的组合来表示换行
\', \"引号
\\反斜线
\t制表符
\b, \f, \v退格,换页,垂直标签 —— 为了兼容性,现在已经不使用了
\xXX具有给定十六进制 Unicode XX 的 Unicode 字符,例如:'\x7A''z' 相同。
\uXXXXUTF-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() 方法,返回当前值的字符串等价物

    • 可用于数值、布尔值、对象和字符串值
      • nullundefined 值没有 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()方法,则调用该方法(不传参数)并返回结果
    • nullundefined 没有 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.lengthindex 如果大于 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) 截取指定位置区域的子字符串startend不含 end允许(负值从右往左查找,索引值从 -1 开始)大于 str.length,等同于 str.length负数小于或等于 -str.length ,等同于0
substring(start, end)指定位置提取字符串中指定数目的字符startend 之间(包括 start,但不包括 end负值代表 0;若 start 大于 end,二者颠倒进行查找大于 str.length,等同于 str.length
substr(start, length) 提取字符串中两个指定的索引之间的字符(未来可能被移除)start 开始获取长为 length 的字符串允许 start 为负数负值从右往左查找,索引值从 -1 开始);大于 str.length,等同于 str.length

三种方法的参数默认值都为:start 默认为 0end 默认为 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)

image.png

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 有两个主要的使用场景:

  1. “隐藏” 对象属性。 如果我们想要向“属于”另一个脚本或者库的对象添加一个属性,我们可以创建一个 Symbol 并使用它作为属性的键。Symbol 属性不会出现在 for..in 中,因此它不会意外地被与其他属性一起处理。并且,它不会被直接访问,因为另一个脚本没有我们的 symbol。因此,该属性将受到保护,防止被意外使用或重写

因此我们可以使用 Symbol 属性“秘密地”将一些东西隐藏到我们需要的对象中,但其他地方看不到它。

  1. 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
  • pushunshift 方法都可以一次添加多个元素

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 操作必须做三件事:

  1. 移除索引为 0 的元素。
  2. 把所有的元素向左移动,把索引 1 改成 0,2 改成 1 以此类推,对其重新编号。
  3. 更新 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 中的数组。

该运算符不会对数组进行特殊处理,它会像处理 任意对象 那样处理数组

让我们回顾一下规则:

  • 仅当两个对象引用的是同一个对象时,它们才相等 ==
  • 如果 == 左右两个参数之中有一个参数是对象,另一个参数是原始类型,那么该对象将会被转换为原始类型
  • nullundefined 相等 ==,且各自不等于任何其他的值

严格比较 === 更简单,因为它不会进行类型转换。

所以,如果我们使用 == 来比较数组,除非我们比较的是两个引用同一数组的变量,否则它们永远不相等。

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.indexOfarr.lastIndexOfarr.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),都接受一个可选的附加参数 thisArgthisArg 参数的值在 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) —— 将从位置 startend 的所有元素复制到 自身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第一个值/所有值
    • findIndexfind 类似,一个返回索引,一个返回值
  • 遍历元素:

    • 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 是否是一个数组。

注意,sortreversesplice 方法修改的是数组本身

这些是最常用的方法,它们覆盖 99% 的用例。但是还有其他几个:

  • arr.fill(value, start, end) —— 从索引 startend用重复的 value 填充数组

  • arr.copyWithin(target, start, end) —— 将从位置 startend 的所有元素复制到 自身target 位置(覆盖现有元素)。

  • arr.flat(depth)/arr.flatMap(fn) 从多维数组创建一个新的扁平数组

  • Array.of(element0[, element1[, …[, elementN]]]) 基于可变数量的参数创建一个新的 Array 实例,而不需要考虑参数的数量或类型

有关完整列表,请参阅 手册