大话 JavaScript(Speaking JavaScript):第十八章到第二十章

330 阅读39分钟

第十八章:数组

原文:18. Arrays

译者:飞龙

协议:CC BY-NC-SA 4.0

数组是从索引(从零开始的自然数)到任意值的映射。值(映射的范围)称为数组的元素。创建数组的最方便的方法是通过数组字面量。这样的字面量列举了数组元素;元素的位置隐含地指定了它的索引。

在本章中,我将首先介绍基本的数组机制,如索引访问和length属性,然后再介绍数组方法。

概述

本节提供了数组的快速概述。详细内容将在后面解释。

作为第一个例子,我们通过数组字面量创建一个数组 arr(参见[创建数组](ch18.html#creating_arrays "Creating Arrays"))并访问元素(参见[数组索引](ch18.html#array_indices "Array Indices"):

> var arr = [ 'a', 'b', 'c' ]; // array literal
> arr[0]  // get element 0
'a'
> arr[0] = 'x';  // set element 0
> arr
[ 'x', 'b', 'c' ]

我们可以使用数组属性 length(参见length)来删除和追加元素:

> var arr = [ 'a', 'b', 'c' ];
> arr.length
3
> arr.length = 2;  // remove an element
> arr
[ 'a', 'b' ]
> arr[arr.length] = 'd';  // append an element
> arr
[ 'a', 'b', 'd' ]

数组方法 push() 提供了另一种追加元素的方式:

> var arr = [ 'a', 'b' ];
> arr.push('d')
3
> arr
[ 'a', 'b', 'd' ]

数组是映射,不是元组

ECMAScript 标准将数组规定为从索引到值的映射(字典)。换句话说,数组可能不是连续的,并且可能有空洞。例如:

> var arr = [];
> arr[0] = 'a';
'a'
> arr[2] = 'b';
'b'
> arr
[ 'a', , 'b' ]

前面的数组有一个空洞:索引 1 处没有元素。数组中的空洞 更详细地解释了空洞。

请注意,大多数 JavaScript 引擎会在内部优化没有空洞的数组,并将它们连续存储。

数组也可以有属性

数组仍然是对象,可以有对象属性。这些属性不被视为实际数组的一部分;也就是说,它们不被视为数组元素:

> var arr = [ 'a', 'b' ];
> arr.foo = 123;
> arr
[ 'a', 'b' ]
> arr.foo
123

创建数组

你可以通过数组字面量创建一个数组:

var myArray = [ 'a', 'b', 'c' ];

数组中的尾随逗号会被忽略:

> [ 'a', 'b' ].length
2
> [ 'a', 'b', ].length
2
> [ 'a', 'b', ,].length  // hole + trailing comma
3

数组构造函数

有两种使用构造函数 Array 的方式:可以创建一个给定长度的空数组,或者数组的元素是给定的值。对于这个构造函数,new 是可选的:以普通函数的方式调用它(不带 new)与以构造函数的方式调用它是一样的。

创建一个给定长度的空数组

给定长度的空数组中只有空洞!因此,很少有意义使用这个版本的构造函数:

> var arr = new Array(2);
> arr.length
2
> arr  // two holes plus trailing comma (ignored!)
[ , ,]

一些引擎在以这种方式调用 Array() 时可能会预先分配连续的内存,这可能会稍微提高性能。但是,请确保增加的冗余性值得!

初始化带有元素的数组(避免!)

这种调用 Array 的方式类似于数组字面量:

// The same as ['a', 'b', 'c']:
var arr1 = new Array('a', 'b', 'c');

问题在于你不能创建只有一个数字的数组,因为那会被解释为创建一个 length 为该数字的数组:

> new Array(2)  // alas, not [ 2 ]
[ , ,]

> new Array(5.7)  // alas, not [ 5.7 ]
RangeError: Invalid array length

> new Array('abc')  // ok
[ 'abc' ]

多维数组

如果你需要为元素创建多个维度,你必须嵌套数组。当你创建这样的嵌套数组时,最内层的数组可以根据需要增长。但是,如果你想直接访问元素,你至少需要创建外部数组。在下面的例子中,我为井字游戏创建了一个三乘三的矩阵。该矩阵完全填满了数据(而不是让行根据需要增长):

// Create the Tic-tac-toe board
var rows = [];
for (var rowCount=0; rowCount < 3; rowCount++) {
    rows[rowCount] = [];
    for (var colCount=0; colCount < 3; colCount++) {
        rows[rowCount][colCount] = '.';
    }
}

// Set an X in the upper right corner
rows[0][2] = 'X';  // [row][column]

// Print the board
rows.forEach(function (row) {
    console.log(row.join(' '));
});

以下是输出:

. . X
. . .
. . .

我希望这个例子能够演示一般情况。显然,如果矩阵很小并且具有固定的维度,你可以通过数组字面量来设置它:

var rows = [ ['.','.','.'], ['.','.','.'], ['.','.','.'] ];

数组索引

当你使用数组索引时,你必须牢记以下限制:

  • 索引是范围在 0 ≤ i < 2³²−1 的数字 i

  • 最大长度为 2³²−1。

超出范围的索引被视为普通的属性键(字符串!)。它们不会显示为数组元素,也不会影响属性 length。例如:

> var arr = [];

> arr[-1] = 'a';
> arr
[]
> arr['-1']
'a'

> arr[4294967296] = 'b';
> arr
[]
> arr['4294967296']
'b'

in 操作符和索引

in 操作符用于检测对象是否具有给定键的属性。但它也可以用于确定数组中是否存在给定的元素索引。例如:

> var arr = [ 'a', , 'b' ];
> 0 in arr
true
> 1 in arr
false
> 10 in arr
false

删除数组元素

除了删除属性之外,delete 操作符还可以删除数组元素。删除元素会创建空洞(length 属性不会更新):

> var arr = [ 'a', 'b' ];
> arr.length
2
> delete arr[1]  // does not update length
true
> arr
[ 'a',  ]
> arr.length
2

你也可以通过减少数组的长度来删除尾随的数组元素(参见length了解详情)。要删除元素而不创建空洞(即,后续元素的索引被减少),你可以使用 Array.prototype.splice()(参见添加和删除元素(破坏性))。在这个例子中,我们删除索引为 1 的两个元素:

> var arr = ['a', 'b', 'c', 'd'];
> arr.splice(1, 2) // returns what has been removed
[ 'b', 'c' ]
> arr
[ 'a', 'd' ]

数组索引详解

提示

这是一个高级部分。通常情况下,您不需要知道这里解释的细节。

数组索引并非看起来那样。 到目前为止,我一直假装数组索引是数字。这也是 JavaScript 引擎在内部实现数组的方式。然而,ECMAScript 规范对索引的看法不同。引用第 15.4 节的话来说:

  • 如果且仅当ToString``(ToUint32(P))等于PToUint32(P)不等于 2³²−1 时,属性键P(一个字符串)才是数组索引。这意味着什么将在下面解释。

  • 属性键为数组索引的数组属性称为元素

换句话说,在规范中,括号中的所有值都被转换为字符串,并解释为属性键,甚至是数字。以下互动演示了这一点:

> var arr = ['a', 'b'];
> arr['0']
'a'
> arr[0]
'a'

要成为数组索引,属性键P(一个字符串!)必须等于以下计算结果:

  1. P转换为数字。

  2. 将数字转换为 32 位无符号整数。

  3. 将整数转换为字符串。

这意味着数组索引必须是 32 位范围内的字符串化整数i,其中 0 ≤ i < 2³²−1。规范明确排除了上限(如前面引用的)。它保留给了最大长度。要了解这个定义是如何工作的,让我们使用通过位运算符实现 32 位整数中的ToUint32()函数。

首先,不包含数字的字符串总是转换为 0,这在字符串化后不等于字符串:

> ToUint32('xyz')
0
> ToUint32('?@#!')
0

其次,超出范围的字符串化整数也会转换为完全不同的整数,与字符串化后不相等:

> ToUint32('-1')
4294967295
> Math.pow(2, 32)
4294967296
> ToUint32('4294967296')
0

第三,字符串化的非整数数字会转换为整数,这些整数又是不同的:

> ToUint32('1.371')
1

请注意,规范还强制规定数组索引不得具有指数:

> ToUint32('1e3')
1000

它们不包含前导零:

> var arr = ['a', 'b'];
> arr['0']  // array index
'a'
> arr['00'] // normal property
undefined

长度

length属性的基本功能是跟踪数组中的最高索引:

> [ 'a', 'b' ].length
2
> [ 'a', , 'b' ].length
3

因此,length不计算元素的数量,因此您必须编写自己的函数来执行此操作。例如:

function countElements(arr) {
    var elemCount = 0;
    arr.forEach(function () {
        elemCount++;
    });
    return elemCount;
}

为了计算元素(非空洞),我们已经利用了forEach跳过空洞的事实。以下是互动:

> countElements([ 'a', 'b' ])
2
> countElements([ 'a', , 'b' ])
2

手动增加数组的长度

手动增加数组的长度对数组几乎没有影响;它只会创建空洞:

> var arr = [ 'a', 'b' ];
> arr.length = 3;
> arr  // one hole at the end
[ 'a', 'b', ,]

最后的结果末尾有两个逗号,因为尾随逗号是可选的,因此总是被忽略。

我们刚刚做的并没有添加任何元素:

> countElements(arr)
2

然而,length属性确实作为指针,指示在哪里插入新元素。例如:

> arr.push('c')
4
> arr
[ 'a', 'b', , 'c' ]

因此,通过Array构造函数设置数组的初始长度会创建一个完全空的数组:

> var arr = new Array(2);
> arr.length
2
> countElements(arr)
0

减少数组的长度

如果您减少数组的长度,则新长度及以上的所有元素都将被删除:

> var arr = [ 'a', 'b', 'c' ];
> 1 in arr
true
> arr[1]
'b'

> arr.length = 1;
> arr
[ 'a' ]
> 1 in arr
false
> arr[1]
undefined

清除数组

如果将数组的长度设置为 0,则它将变为空。这样可以清除数组。例如:

function clearArray(arr) {
    arr.length = 0;
}

以下是互动:

> var arr = [ 'a', 'b', 'c' ];
> clearArray(arr)
> arr
[]

但是,请注意,这种方法可能会很慢,因为每个数组元素都会被显式删除。具有讽刺意味的是,创建一个新的空数组通常更快:

arr = [];

清除共享数组

您需要知道的是,将数组的长度设置为零会影响共享数组的所有人:

> var a1 = [1, 2, 3];
> var a2 = a1;
> a1.length = 0;

> a1
[]
> a2
[]

相比之下,分配一个空数组不会:

> var a1 = [1, 2, 3];
> var a2 = a1;
> a1 = [];

> a1
[]
> a2
[ 1, 2, 3 ]

最大长度

最大数组长度为 2³²−1:

> var arr1 = new Array(Math.pow(2, 32));  // not ok
RangeError: Invalid array length

> var arr2 = new Array(Math.pow(2, 32)-1);  // ok
> arr2.push('x');
RangeError: Invalid array length

数组中的空洞

数组是从索引到值的映射。这意味着数组可以有空洞,即长度小于数组中缺失的索引。在这些索引中读取元素会返回undefined

提示

建议避免数组中的空洞。JavaScript 对它们的处理不一致(即,一些方法忽略它们,其他方法不会)。幸运的是,通常你不需要知道如何处理空洞:它们很少有用,并且会对性能产生负面影响。

创建空洞

通过给数组索引赋值可以创建空洞:

> var arr = [];
> arr[0] = 'a';
> arr[2] = 'c';
> 1 in arr  // hole at index 1
false

也可以通过在数组字面量中省略值来创建空洞:

> var arr = ['a',,'c'];
> 1 in arr  // hole at index 1
false

警告

需要两个尾随逗号来创建尾随的空洞,因为最后一个逗号总是被忽略:

> [ 'a', ].length
1
> [ 'a', ,].length
2

稀疏数组与密集数组

本节将检查空洞和undefined作为元素之间的区别。鉴于读取空洞会返回undefined,两者非常相似。

带有空洞的数组称为稀疏数组。没有空洞的数组称为密集数组。密集数组是连续的,并且在每个索引处都有一个元素——从零开始,到length-1 结束。让我们比较以下两个数组,一个是稀疏数组,一个是密集数组。这两者非常相似:

var sparse = [ , , 'c' ];
var dense  = [ undefined, undefined, 'c' ];

空洞几乎就像在相同索引处有一个undefined元素。两个数组的长度都是一样的:

> sparse.length
3
> dense.length
3

但是稀疏数组没有索引为 0 的元素:

> 0 in sparse
false
> 0 in dense
true

通过for进行迭代对两个数组来说是一样的:

> for (var i=0; i<sparse.length; i++) console.log(sparse[i]);
undefined
undefined
c
> for (var i=0; i<dense.length; i++) console.log(dense[i]);
undefined
undefined
c

通过forEach进行迭代会跳过空洞,但不会跳过未定义的元素:

> sparse.forEach(function (x) { console.log(x) });
c
> dense.forEach(function (x) { console.log(x) });
undefined
undefined
c

哪些操作会忽略空洞,哪些会考虑它们?

涉及数组的一些操作会忽略空洞,而另一些会考虑它们。本节解释了细节。

数组迭代方法

forEach()会跳过空洞:

> ['a',, 'b'].forEach(function (x,i) { console.log(i+'.'+x) })
0.a
2.b

every()也会跳过空洞(类似的:some()):

> ['a',, 'b'].every(function (x) { return typeof x === 'string' })
true

map()会跳过,但保留空洞:

> ['a',, 'b'].map(function (x,i) { return i+'.'+x })
[ '0.a', , '2.b' ]

filter()消除空洞:

> ['a',, 'b'].filter(function (x) { return true })
[ 'a', 'b' ]

其他数组方法

join()将空洞、undefinednull转换为空字符串:

> ['a',, 'b'].join('-')
'a--b'
> [ 'a', undefined, 'b' ].join('-')
'a--b'

sort()在排序时保留空洞:

> ['a',, 'b'].sort()  // length of result is 3
[ 'a', 'b', ,  ]

for-in 循环

for-in循环正确列出属性键(它们是数组索引的超集):

> for (var key in ['a',, 'b']) { console.log(key) }
0
2

Function.prototype.apply()

apply()将每个空洞转换为一个值为undefined的参数。以下交互演示了这一点:函数f()将其参数作为数组返回。当我们传递一个带有三个空洞的数组给apply()以调用f()时,后者接收到三个undefined参数:

> function f() { return [].slice.call(arguments) }
> f.apply(null, [ , , ,])
[ undefined, undefined, undefined ]

这意味着我们可以使用apply()来创建一个带有undefined的数组:

> Array.apply(null, Array(3))
[ undefined, undefined, undefined ]

警告

apply()将空洞转换为undefined在空数组中,但不能用于在任意数组中填补空洞(可能包含或不包含空洞)。例如,任意数组[2]

> Array.apply(null, [2])
[ , ,]

数组不包含任何空洞,所以apply()应该返回相同的数组。但实际上它返回一个长度为 2 的空数组(它只包含两个空洞)。这是因为Array()将单个数字解释为数组长度,而不是数组元素。

从数组中移除空洞

正如我们所见,filter()会移除空洞:

> ['a',, 'b'].filter(function (x) { return true })
[ 'a', 'b' ]

使用自定义函数将任意数组中的空洞转换为undefined

function convertHolesToUndefineds(arr) {
    var result = [];
    for (var i=0; i < arr.length; i++) {
        result[i] = arr[i];
    }
    return result;
}

使用该函数:

> convertHolesToUndefineds(['a',, 'b'])
[ 'a', undefined, 'b' ]

数组构造方法

Array.isArray(obj)

如果obj是数组则返回true。它正确处理跨realms(窗口或框架)的对象——与instanceof相反(参见Pitfall: crossing realms (frames or windows))。

数组原型方法

在接下来的章节中,数组原型方法按功能分组。对于每个子章节,我会提到这些方法是破坏性的(它们会改变被调用的数组)还是非破坏性的(它们不会修改它们的接收者;这样的方法通常会返回新的数组)。

添加和删除元素(破坏性)

本节中的所有方法都是破坏性的:

Array.prototype.shift()

删除索引为 0 的元素并返回它。随后元素的索引减 1:

> var arr = [ 'a', 'b' ];
> arr.shift()
'a'
> arr
[ 'b' ]

Array.prototype.unshift(elem1?, elem2?, ...)

将给定的元素添加到数组的开头。它返回新的长度:

> var arr = [ 'c', 'd' ];
> arr.unshift('a', 'b')
4
> arr
[ 'a', 'b', 'c', 'd' ]

Array.prototype.pop()

移除数组的最后一个元素并返回它:

> var arr = [ 'a', 'b' ];
> arr.pop()
'b'
> arr
[ 'a' ]

Array.prototype.push(elem1?, elem2?, ...)

将给定的元素添加到数组的末尾。它返回新的长度:

> var arr = [ 'a', 'b' ];
> arr.push('c', 'd')
4
> arr
[ 'a', 'b', 'c', 'd' ]

apply()(参见Function.prototype.apply(thisValue, argArray))使您能够破坏性地将数组arr2附加到另一个数组arr1

> var arr1 = [ 'a', 'b' ];
> var arr2 = [ 'c', 'd' ];

> Array.prototype.push.apply(arr1, arr2)
4
> arr1
[ 'a', 'b', 'c', 'd' ]

Array.prototype.splice(start, deleteCount?, elem1?, elem2?, ...)

start开始,删除deleteCount个元素并插入给定的元素。换句话说,您正在用elem1elem2等替换位置start处的deleteCount个元素。该方法返回已被移除的元素:

> var arr = [ 'a', 'b', 'c', 'd' ];
> arr.splice(1, 2, 'X');
[ 'b', 'c' ]
> arr
[ 'a', 'X', 'd' ]

特殊的参数值:

  • start可以为负数,这种情况下它将被加到长度以确定起始索引。因此,-1指的是最后一个元素,依此类推。

  • deleteCount是可选的。如果省略(以及所有后续参数),则删除从索引start开始的所有元素及之后的所有元素。

在此示例中,我们删除最后两个索引后的所有元素:

> var arr = [ 'a', 'b', 'c', 'd' ];
> arr.splice(-2)
[ 'c', 'd' ]
> arr
[ 'a', 'b' ]

排序和颠倒元素(破坏性)

这些方法也是破坏性的:

Array.prototype.reverse()

颠倒数组中元素的顺序并返回对原始(修改后的)数组的引用:

> var arr = [ 'a', 'b', 'c' ];
> arr.reverse()
[ 'c', 'b', 'a' ]
> arr // reversing happened in place
[ 'c', 'b', 'a' ]

Array.prototype.sort(compareFunction?)

对数组进行排序并返回它:

> var arr = ['banana', 'apple', 'pear', 'orange'];
> arr.sort()
[ 'apple', 'banana', 'orange', 'pear' ]
> arr  // sorting happened in place
[ 'apple', 'banana', 'orange', 'pear' ]

请记住,排序通过将值转换为字符串进行比较,这意味着数字不会按数字顺序排序:

> [-1, -20, 7, 50].sort()
[ -1, -20, 50, 7 ]

您可以通过提供可选参数compareFunction来解决这个问题,它控制排序的方式。它具有以下签名:

function compareFunction(a, b)

此函数比较ab并返回:

  • 如果a小于b,则返回小于零的整数(例如,-1

  • 如果a等于b,则返回零

  • 如果a大于b,则返回大于零的整数(例如,1

比较数字

对于数字,您可以简单地返回a-b,但这可能会导致数值溢出。为了防止这种情况发生,您需要更冗长的代码:

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

我不喜欢嵌套的条件运算符。但在这种情况下,代码要简洁得多,我很想推荐它:

function compareCanonically(a, b) {
    return return a < b ? -1 (a > b ? 1 : 0);
}

使用该函数:

> [-1, -20, 7, 50].sort(compareCanonically)
[ -20, -1, 7, 50 ]

比较字符串

对于字符串,您可以使用String.prototype.localeCompare(参见比较字符串):

> ['c', 'a', 'b'].sort(function (a,b) { return a.localeCompare(b) })
[ 'a', 'b', 'c' ]

比较对象

参数compareFunction对于排序对象也很有用:

var arr = [
    { name: 'Tarzan' },
    { name: 'Cheeta' },
    { name: 'Jane' } ];

function compareNames(a,b) {
    return a.name.localeCompare(b.name);
}

使用compareNames作为比较函数,arrname排序:

> arr.sort(compareNames)
[ { name: 'Cheeta' },
  { name: 'Jane' },
  { name: 'Tarzan' } ]

连接、切片、连接(非破坏性)

以下方法对数组执行各种非破坏性操作:

Array.prototype.concat(arr1?, arr2?, ...)

创建一个新数组,其中包含接收器的所有元素,后跟数组arr1的所有元素,依此类推。如果其中一个参数不是数组,则将其作为元素添加到结果中(例如,这里的第一个参数'c'):

> var arr = [ 'a', 'b' ];
> arr.concat('c', ['d', 'e'])
[ 'a', 'b', 'c', 'd', 'e' ]

调用concat()的数组不会改变:

> arr
[ 'a', 'b' ]

Array.prototype.slice(begin?, end?)

将数组元素复制到一个新数组中,从begin开始,直到end之前的元素:

> [ 'a', 'b', 'c', 'd' ].slice(1, 3)
[ 'b', 'c' ]

如果缺少end,则使用数组长度:

> [ 'a', 'b', 'c', 'd' ].slice(1)
[ 'b', 'c', 'd' ]

如果两个索引都缺失,则复制数组:

> [ 'a', 'b', 'c', 'd' ].slice()
[ 'a', 'b', 'c', 'd' ]

如果任一索引为负数,则将数组长度加上它。因此,-1指的是最后一个元素,依此类推:

> [ 'a', 'b', 'c', 'd' ].slice(1, -1)
[ 'b', 'c' ]
> [ 'a', 'b', 'c', 'd' ].slice(-2)
[ 'c', 'd' ]

Array.prototype.join(separator?)

通过对所有数组元素应用toString()并在结果之间放置separator字符串来创建一个字符串。如果省略separator,则使用,

> [3, 4, 5].join('-')
'3-4-5'
> [3, 4, 5].join()
'3,4,5'
> [3, 4, 5].join('')
'345'

join()undefinednull转换为空字符串:

> [undefined, null].join('#')
'#'

数组中的空位也会转换为空字符串:

> ['a',, 'b'].join('-')
'a--b'

搜索值(非破坏性)

以下方法在数组中搜索值:

Array.prototype.indexOf(searchValue, startIndex?)

startIndex开始搜索数组中的searchValue。它返回第一次出现的索引,如果找不到则返回-1。如果startIndex为负数,则将数组长度加上它;如果缺少startIndex,则搜索整个数组:

> [ 3, 1, 17, 1, 4 ].indexOf(1)
1
> [ 3, 1, 17, 1, 4 ].indexOf(1, 2)
3

搜索时使用严格相等(参见相等运算符:===与==),这意味着indexOf()无法找到NaN

> [NaN].indexOf(NaN)
-1

Array.prototype.lastIndexOf(searchElement, startIndex?)

startIndex开始向后搜索searchElement,返回第一次出现的索引或-1(如果找不到)。如果startIndex为负数,则将数组长度加上它;如果缺失,则搜索整个数组。搜索时使用严格相等(参见相等运算符:===与==):

> [ 3, 1, 17, 1, 4 ].lastIndexOf(1)
3
> [ 3, 1, 17, 1, 4 ].lastIndexOf(1, -3)
1

迭代(非破坏性)

迭代方法使用一个函数来迭代数组。我区分三种迭代方法,它们都是非破坏性的:检查方法主要观察数组的内容;转换方法从接收器派生一个新数组;减少方法基于接收器的元素计算结果。

检查方法

本节中描述的每个方法都是这样的:

arr.examinationMethod(callback, thisValue?)

这样的方法需要以下参数:

  • callback是它的第一个参数,一个它调用的函数。根据检查方法的不同,回调返回布尔值或无返回值。它具有以下签名:

    function callback(element, index, array)
    

elementcallback要处理的数组元素,index是元素的索引,array是调用examinationMethod的数组。

  • thisValue允许您配置callback内部的this的值。

现在是我刚刚描述的检查方法的签名:

Array.prototype.forEach(callback, thisValue?)

迭代数组的元素:

var arr = [ 'apple', 'pear', 'orange' ];
arr.forEach(function (elem) {
    console.log(elem);
});

Array.prototype.every(callback, thisValue?)

如果回调对每个元素返回true,则返回true。一旦回调返回false,迭代就会停止。请注意,不返回值会导致隐式返回undefinedevery()将其解释为falseevery()的工作方式类似于全称量词(“对于所有”)。

这个例子检查数组中的每个数字是否都是偶数:

> function isEven(x) { return x % 2 === 0 }
> [ 2, 4, 6 ].every(isEven)
true
> [ 2, 3, 4 ].every(isEven)
false

如果数组为空,则结果为true(并且不调用callback):

> [].every(function () { throw new Error() })
true

Array.prototype.some(callback, thisValue?)

如果回调对至少一个元素返回true,则返回true。一旦回调返回true,迭代就会停止。请注意,不返回值会导致隐式返回undefinedsome()将其解释为falsesome()的工作方式类似于存在量词(“存在”)。

这个例子检查数组中是否有偶数:

> function isEven(x) { return x % 2 === 0 }
> [ 1, 3, 5 ].some(isEven)
false
> [ 1, 2, 3 ].some(isEven)
true

如果数组为空,则结果为false(并且不调用callback):

> [].some(function () { throw new Error() })
false

forEach()的一个潜在陷阱是它不支持break或类似的东西来提前中止循环。如果你需要这样做,可以使用some()

function breakAtEmptyString(strArr) {
    strArr.some(function (elem) {
        if (elem.length === 0) {
            return true; // break
        }
        console.log(elem);
        // implicit: return undefined (interpreted as false)
    });
}

some()如果发生了中断,则返回true,否则返回false。这使您可以根据迭代是否成功完成(这在for循环中有点棘手)做出不同的反应。

转换方法

转换方法接受一个输入数组并产生一个输出数组,而回调控制输出的产生方式。回调的签名与检查方法相同:

function callback(element, index, array)

有两种转换方法:

Array.prototype.map(callback, thisValue?)

每个输出数组元素是将callback应用于输入元素的结果。例如:

> [ 1, 2, 3 ].map(function (x) { return 2 * x })
[ 2, 4, 6 ]

Array.prototype.filter(callback, thisValue?)

输出数组仅包含那些callback返回true的输入元素。例如:

> [ 1, 0, 3, 0 ].filter(function (x) { return x !== 0 })
[ 1, 3 ]

减少方法

对于减少,回调具有不同的签名:

function callback(previousValue, currentElement, currentIndex, array)

参数previousValue是回调先前返回的值。当首次调用回调时,有两种可能性(描述适用于Array.prototype.reduce();与reduceRight()的差异在括号中提到):

  • 提供了明确的initialValue。然后previousValueinitialValuecurrentElement是第一个数组元素(reduceRight:最后一个数组元素)。

  • 没有提供明确的initialValue。然后previousValue是第一个数组元素,currentElement是第二个数组元素(reduceRight:最后一个数组元素和倒数第二个数组元素)。

有两种减少的方法:

Array.prototype.reduce(callback, initialValue?)

从左到右迭代,并像之前描述的那样调用回调。该方法的结果是回调返回的最后一个值。此示例计算所有数组元素的总和:

function add(prev, cur) {
    return prev + cur;
}
console.log([10, 3, -1].reduce(add)); // 12

如果你在一个只有一个元素的数组上调用reduce,那么该元素会被返回:

> [7].reduce(add)
7

如果你在一个空数组上调用reduce,你必须指定initialValue,否则你会得到一个异常:

> [].reduce(add)
TypeError: Reduce of empty array with no initial value
> [].reduce(add, 123)
123

Array.prototype.reduceRight(callback, initialValue?)

reduce()相同,但从右到左迭代。

注意

在许多函数式编程语言中,reduce被称为foldfoldl(左折叠),reduceRight被称为foldr(右折叠)。

reduce方法的另一种方式是它实现了一个 n 元运算符OP

OP[1≤i≤n] x[i]

通过一系列二元运算符op2的应用:

(...(x[1] op2 x[2]) op2 ...) op2 x[n]

这就是前面代码示例中发生的事情:我们通过 JavaScript 的二进制加法运算符实现了一个数组的 n 元求和运算符。

例如,让我们通过以下函数来检查两个迭代方向:

function printArgs(prev, cur, i) {
    console.log('prev:'+prev+', cur:'+cur+', i:'+i);
    return prev + cur;
}

如预期的那样,reduce()从左到右迭代:

> ['a', 'b', 'c'].reduce(printArgs)
prev:a, cur:b, i:1
prev:ab, cur:c, i:2
'abc'
> ['a', 'b', 'c'].reduce(printArgs, 'x')
prev:x, cur:a, i:0
prev:xa, cur:b, i:1
prev:xab, cur:c, i:2
'xabc'

reduceRight()从右到左迭代:

> ['a', 'b', 'c'].reduceRight(printArgs)
prev:c, cur:b, i:1
prev:cb, cur:a, i:0
'cba'
> ['a', 'b', 'c'].reduceRight(printArgs, 'x')
prev:x, cur:c, i:2
prev:xc, cur:b, i:1
prev:xcb, cur:a, i:0
'xcba'

陷阱:类数组对象

JavaScript 中的一些对象看起来像数组,但它们并不是数组。这通常意味着它们具有索引访问和length属性,但没有数组方法。例子包括特殊变量arguments,DOM 节点列表和字符串。类数组对象和通用方法提供了处理类数组对象的提示。

最佳实践:迭代数组

要迭代一个数组arr,你有两个选择:

  • 一个简单的for循环(参见for):

    for (var i=0; i<arr.length; i++) {
        console.log(arr[i]);
    }
    
  • 数组迭代方法之一(参见迭代(非破坏性))。例如,forEach()

    arr.forEach(function (elem) {
        console.log(elem);
    });
    

不要使用for-in循环(参见for-in)来迭代数组。它遍历索引,而不是值。在这样做的同时,它包括正常属性的键,包括继承的属性。

第十九章:正则表达式

原文:19. Regular Expressions

译者:飞龙

协议:CC BY-NC-SA 4.0

本章概述了正则表达式的 JavaScript API。它假定你对它们的工作原理有一定了解。如果你不了解,网上有很多好的教程。其中两个例子是:

正则表达式语法

这里使用的术语与 ECMAScript 规范中的语法非常接近。我有时会偏离以使事情更容易理解。

原子:一般

一般原子的语法如下:

特殊字符

以下所有字符都具有特殊含义:

\ ^ $ . * + ? ( ) [ ] { } |

你可以通过在前面加上反斜杠来转义它们。例如:

> /^(ab)$/.test('(ab)')
false
> /^\(ab\)$/.test('(ab)')
true

其他特殊字符包括:

  • 在字符类[...]中:

    -
    
  • 在以问号(?...)开头的组内:

    : = ! < >
    

尖括号仅由 XRegExp 库(参见第三十章)使用,用于命名组。

模式字符

除了前面提到的特殊字符之外,所有字符都与它们自己匹配。

.(点)

匹配任何 JavaScript 字符(UTF-16 代码单元),除了行终止符(换行符、回车符等)。要真正匹配任何字符,请使用[\s\S]。例如:

> /./.test('\n')
false
> /[\s\S]/.test('\n')
true

字符转义(匹配单个字符)

  • 特定的控制字符包括\f(换页符)、\n(换行符、新行)、\r(回车符)、\t(水平制表符)和\v(垂直制表符)。

  • \0匹配 NUL 字符(\u0000)。

  • 任何控制字符:\cA\cZ

  • Unicode 字符转义:\u0000\xFFFF(Unicode 代码单元;参见第二十四章)。

  • 十六进制字符转义:\x00\xFF

字符类转义(匹配一组字符中的一个)

  • 数字:\d匹配任何数字(与[0-9]相同);\D匹配任何非数字(与[^0-9]相同)。

  • 字母数字字符:\w匹配任何拉丁字母数字字符加下划线(与[A-Za-z0-9_]相同);\W匹配所有未被\w匹配的字符。

  • 空白:\s匹配空白字符(空格、制表符、换行符、回车符、换页符、所有 Unicode 空格等);\S匹配所有非空白字符。

原子:字符类

字符类的语法如下:

  • [«charSpecs»]匹配至少一个charSpecs中的任何一个的单个字符。

  • [^«charSpecs»]匹配任何不匹配charSpecs中任何一个的单个字符。

以下构造都是字符规范:

  • 源字符匹配它们自己。大多数字符都是源字符(甚至许多在其他地方是特殊的字符)。只有三个字符不是:

        \ ] -
    

像往常一样,您可以通过反斜杠进行转义。如果要匹配破折号而不进行转义,它必须是方括号打开后的第一个字符,或者是范围的右侧,如下所述。

  • 类转义:允许使用先前列出的任何字符转义和字符类转义。还有一个额外的转义:

  • 退格(\b):在字符类之外,\b匹配单词边界。在字符类内,它匹配控制字符退格

  • 范围包括源字符或类转义,后跟破折号(-),后跟源字符或类转义。

为了演示使用字符类,此示例解析了按照 ISO 8601 标准格式化的日期:

function parseIsoDate(str) {
    var match = /^([0-9]{4})-([0-9]{2})-([0-9]{2})$/.exec(str);

    // Other ways of writing the regular expression:
    // /^([0-9][0-9][0-9][0-9])-([0-9][0-9])-([0-9][0-9])$/
    // /^(\d\d\d\d)-(\d\d)-(\d\d)$/

    if (!match) {
        throw new Error('Not an ISO date: '+str);
    }
    console.log('Year: '  + match[1]);
    console.log('Month: ' + match[2]);
    console.log('Day: '   + match[3]);
}

以下是交互:

> parseIsoDate('2001-12-24')
Year: 2001
Month: 12
Day: 24

原子:组

组的语法如下:

  • («pattern»)是一个捕获组。由pattern匹配的任何内容都可以通过反向引用或作为匹配操作的结果来访问。

  • (?:«pattern»)是一个非捕获组。pattern仍然与输入匹配,但不保存为捕获。因此,该组没有您可以引用的编号(例如,通过反向引用)。

\1\2等被称为反向引用;它们指回先前匹配的组。反斜杠后面的数字可以是大于或等于 1 的任何整数,但第一个数字不能是 0。

在此示例中,反向引用保证了破��号之前和之后的 a 的数量相同:

> /^(a+)-\1$/.test('a-a')
true
> /^(a+)-\1$/.test('aaa-aaa')
true
> /^(a+)-\1$/.test('aa-a')
false

此示例使用反向引用来匹配 HTML 标签(显然,通常应使用适当的解析器来处理 HTML):

> var tagName = /<([^>]+)>[^<]*<\/\1>/;
> tagName.exec('<b>bold</b>')[1]
'b'
> tagName.exec('<strong>text</strong>')[1]
'strong'
> tagName.exec('<strong>text</stron>')
null

量词

任何原子(包括字符类和组)都可以后跟一个量词:

  • ?表示匹配零次或一次。

  • *表示匹配零次或多次。

  • +表示匹配一次或多次。

  • {n}表示精确匹配n次。

  • {n,}表示匹配n次或更多次。

  • {n,m}表示至少匹配n次,最多匹配m次。

默认情况下,量词是贪婪的;也就是说,它们尽可能多地匹配。您可以通过在任何前述量词(包括大括号中的范围)后加上问号(?)来获得勉强匹配(尽可能少)。例如:

> '<a> <strong>'.match(/^<(.*)>/)[1]  // greedy
'a> <strong'
> '<a> <strong>'.match(/^<(.*?)>/)[1]  // reluctant
'a'

因此,.*?是一个用于匹配直到下一个原子出现的有用模式。例如,以下是刚刚显示的 HTML 标签的正则表达式的更紧凑版本(使用[^<]*代替.*?):

/<(.+?)>.*?<\/\1>/

断言

断言,如下列表所示,是关于输入中当前位置的检查:

^仅在输入的开头匹配。
$仅在输入的末尾匹配。
\b仅在单词边界处匹配。不要与[\b]混淆,它匹配退格。
\B仅当不在单词边界时匹配。
(?=«pattern»)正向预查:仅当“模式”匹配接下来的内容时才匹配。“模式”仅用于向前查看,否则会被忽略。
(?!«pattern»)负向预查:仅当“模式”不匹配接下来的内容时才匹配。“模式”仅用于向前查看,否则会被忽略。

此示例通过\b匹配单词边界:

> /\bell\b/.test('hello')
false
> /\bell\b/.test('ello')
false
> /\bell\b/.test('ell')
true

此示例通过\B匹配单词内部:

> /\Bell\B/.test('ell')
false
> /\Bell\B/.test('hell')
false
> /\Bell\B/.test('hello')
true

注意

不支持后行断言。手动实现后行断言解释了如何手动实现它。

分歧

分歧运算符(|)分隔两个选择;任一选择必须匹配分歧才能匹配。这些选择是原子(可选包括量词)。

该运算符的绑定非常弱,因此您必须小心,以确保选择不会延伸太远。例如,以下正则表达式匹配所有以'aa'开头或以'bb'结尾的字符串:

> /^aa|bb$/.test('aaxx')
true
> /^aa|bb$/.test('xxbb')
true

换句话说,分歧比甚至^$都要弱,两个选择是^aabb$。如果要匹配两个字符串'aa''bb',则需要括号:

/^(aa|bb)$/

同样,如果要匹配字符串'aab''abb'

/^a(a|b)b$/

Unicode 和正则表达式

JavaScript 的正则表达式对 Unicode 的支持非常有限。特别是当涉及到星际飞船中的代码点时,您必须小心。第二十四章解释了详细信息。

创建正则表达式

您可以通过文字或构造函数创建正则表达式,并通过标志配置其工作方式。

文字与构造函数

有两种方法可以创建正则表达式:您可以使用文字或构造函数RegExp

文字:/xyz/i:在加载时编译

构造函数(第二个参数是可选的):new RegExp('xyz','i'):在运行时编译

文字和构造函数在编译时不同:

  • 文字在加载时编译。在评估时,以下代码将引发异常:

    function foo() {
        /[/;
    }
    
  • 构造函数在调用时编译正则表达式。以下代码不会引发异常,但调用foo()会:

    function foo() {
        new RegExp('[');
    }
    

因此,通常应使用文字,但如果要动态组装正则表达式,则需要构造函数。

标志

标志是正则表达式文字的后缀和正则表达式构造函数的参数;它们修改正则表达式的匹配行为。存在以下标志:

短名称长名称描述
g全局给定的正则表达式多次匹配。影响几种方法,特别是replace()
i忽略大小写在尝试匹配给定的正则表达式时忽略大小写。
m多行模式在多行模式下,开始运算符^和结束运算符$匹配每一行,而不是完整的输入字符串。

短名称用于文字前缀和构造函数参数(请参见下一节中的示例)。长名称用于正则表达式的属性,指示在创建期间设置了哪些标志。

正则表达式的实例属性

正则表达式具有以下实例属性:

  • 标志:表示设置了哪些标志的布尔值:

  • 全局:标志/g设置了吗?

  • 忽略大小写:标志/i设置了吗?

  • 多行:标志/m设置了吗?

  • 用于多次匹配的数据(设置了/g标志):

  • lastIndex是下次继续搜索的索引。

以下是访问标志的实例属性的示例:

> var regex = /abc/i;
> regex.ignoreCase
true
> regex.multiline
false

创建正则表达式的示例

在这个例子中,我们首先使用文字创建相同的正则表达式,然后使用构造函数,并使用test()方法来确定它是否匹配一个字符串:

> /abc/.test('ABC')
false
> new RegExp('abc').test('ABC')
false

在这个例子中,我们创建一个忽略大小写的正则表达式(标志/i):

> /abc/i.test('ABC')
true
> new RegExp('abc', 'i').test('ABC')
true

RegExp.prototype.test:是否有匹配?

test()方法检查正则表达式regex是否匹配字符串str

regex.test(str)

test()的操作方式取决于标志/g是否设置。

如果标志/g未设置,则该方法检查str中是否有匹配。例如:

> var str = '_x_x';

> /x/.test(str)
true
> /a/.test(str)
false

如果设置了标志/g,则该方法返回true,直到strregex的匹配次数。属性regex.lastIndex包含最后匹配后的索引:

> var regex = /x/g;
> regex.lastIndex
0

> regex.test(str)
true
> regex.lastIndex
2

> regex.test(str)
true
> regex.lastIndex
4

> regex.test(str)
false

String.prototype.search:有匹配的索引吗?

search()方法在str中查找与regex匹配的内容:

str.search(regex)

如果有匹配,返回找到匹配的索引。否则,结果为-1regexgloballastIndex属性在执行搜索时被忽略(lastIndex不会改变)。

例如:

> 'abba'.search(/b/)
1
> 'abba'.search(/x/)
-1

如果search()的参数不是正则表达式,则会转换为正则表达式:

> 'aaab'.search('^a+b+$')
0

RegExp.prototype.exec:捕获组

以下方法调用在匹配regexstr时捕获组:

var matchData = regex.exec(str);

如果没有匹配,matchDatanull。否则,matchData是一个匹配结果,一个带有两个额外属性的数组:

数组元素

  • 元素 0 是完整正则表达式的匹配(如果愿意的话,是第 0 组)。

  • 元素n > 1 是第n组的捕获。

属性

  • input是完整的输入字符串。

  • index是找到匹配的索引。

第一个匹配(标志/g 未设置)

如果标志/g未设置,则只返回第一个匹配:

> var regex = /a(b+)/;
> regex.exec('_abbb_ab_')
[ 'abbb',
  'bbb',
  index: 1,
  input: '_abbb_ab_' ]
> regex.lastIndex
0

所有匹配(标志/g 设置)

如果设置了标志/g,则如果反复调用exec(),所有匹配都会被返回。返回值null表示没有更多的匹配。属性lastIndex指示下次匹配将继续的位置:

> var regex = /a(b+)/g;
> var str = '_abbb_ab_';

> regex.exec(str)
[ 'abbb',
  'bbb',
  index: 1,
  input: '_abbb_ab_' ]
> regex.lastIndex
6

> regex.exec(str)
[ 'ab',
  'b',
  index: 7,
  input: '_abbb_ab_' ]
> regex.lastIndex
10

> regex.exec(str)
null

在这里我们循环匹配:

var regex = /a(b+)/g;
var str = '_abbb_ab_';
var match;
while (match = regex.exec(str)) {
    console.log(match[1]);
}

我们得到以下输出:

bbb
b

String.prototype.match:捕获组或返回所有匹配的子字符串

以下方法调用匹配regexstr

var matchData = str.match(regex);

如果regex的标志/g未设置,此方法的工作方式类似于RegExp.prototype.exec()

> 'abba'.match(/a/)
[ 'a', index: 0, input: 'abba' ]

如果设置了标志,则该方法返回一个包含str中所有匹配子字符串的数组(即每次匹配的第 0 组),如果没有匹配则返回null

> 'abba'.match(/a/g)
[ 'a', 'a' ]
> 'abba'.match(/x/g)
null

String.prototype.replace:搜索和替换

replace()方法搜索字符串str,找到与search匹配的内容,并用replacement替换它们:

str.replace(search, replacement)

有几种方式可以指定这两个参数:

search

可以是字符串或正则表达式:

  • 字符串:在输入字符串中直接查找。请注意,只有第一次出现的字符串会被替换。如果要替换多个出现,必须使用带有/g标志的正则表达式。这是一个意外和一个主要的陷阱。

  • 正则表达式:与输入字符串匹配。警告:使用global标志,否则只会尝试一次匹配正则表达式。

replacement

可以是字符串或函数:

  • 字符串:描述如何替换已找到的内容。

  • 功能:通过参数提供匹配信息来计算替换。

替换是一个字符串

如果replacement是一个字符串,它的内容将被直接使用来替换匹配。唯一的例外是特殊字符美元符号($),它启动所谓的替换指令

  • 组:$n插入匹配中的第 n 组。n必须至少为 1($0没有特殊含义)。

  • 匹配的子字符串:

  • `$``(反引号)插入匹配前的文本。

  • $&插入完整的匹配。

  • $'(撇号)插入匹配后的文本。

  • $$插入一个单独的$

这个例子涉及匹配的子字符串及其前缀和后缀:

> 'axb cxd'.replace(/x/g, "[$`,$&,$']")
'a[a,x,b cxd]b c[axb c,x,d]d'

这个例子涉及到一个组:

> '"foo" and "bar"'.replace(/"(.*?)"/g, '#$1#')
'#foo# and #bar#'

替换是一个函数

如果replacement是一个函数,它会计算要替换匹配的字符串。此函数具有以下签名:

function (completeMatch, group_1, ..., group_n, offset, inputStr)

completeMatch与以前的$&相同,offset指示找到匹配的位置,inputStr是正在匹配的内容。因此,您可以使用特殊变量arguments来访问组(通过arguments[1]访问第 1 组,依此类推)。例如:

> function replaceFunc(match) { return 2 * match }
> '3 apples and 5 oranges'.replace(/[0-9]+/g, replaceFunc)
'6 apples and 10 oranges'

标志/g的问题

设置了/g标志的正则表达式如果必须多次调用才能返回所有结果,则存在问题。这适用于两种方法:

  • RegExp.prototype.test()

  • RegExp.prototype.exec()

然后 JavaScript 滥用正则表达式作为迭代器,作为结果序列的指针。这会导致问题:

问题 1:无法内联/g正则表达式

例如:

// Don’t do that:
var count = 0;
while (/a/g.test('babaa')) count++;

前面的循环是无限的,因为每次循环迭代都会创建一个新的正则表达式,从而重新开始结果的迭代。因此,必须重写代码:

var count = 0;
var regex = /a/g;
while (regex.test('babaa')) count++;

这是另一个例子:

// Don’t do that:
function extractQuoted(str) {
    var match;
    var result = [];
    while ((match = /"(.*?)"/g.exec(str)) != null) {
        result.push(match[1]);
    }
    return result;
}

调用前面的函数将再次导致无限循环。正确的版本是(为什么lastIndex设置为 0 很快就会解释):

var QUOTE_REGEX = /"(.*?)"/g;
function extractQuoted(str) {
    QUOTE_REGEX.lastIndex = 0;
    var match;
    var result = [];
    while ((match = QUOTE_REGEX.exec(str)) != null) {
        result.push(match[1]);
    }
    return result;
}

使用该函数:

> extractQuoted('"hello", "world"')
[ 'hello', 'world' ]

提示

最好的做法是不要内联(然后您可以给正则表达式起一个描述性的名称)。但是您必须意识到您不能这样做,即使是在快速的 hack 中也不行。

问题 2:/g正则表达式作为参数

调用test()exec()多次的代码在作为参数传递给它的正则表达式时必须小心。它的标志/g必须激活,并且为了安全起见,它的lastIndex应该设置为零(下一个示例中提供了解释)。

问题 3:共享的/g正则表达式(例如,常量)

每当引用尚未新创建的正则表达式时,您应该在将其用作迭代器之前将其lastIndex属性设置为零(下一个示例中提供了解释)。由于迭代取决于lastIndex,因此这样的正则表达式不能同时在多个迭代中使用。

以下示例说明了问题 2。这是一个简单的实现函数,用于计算字符串str中正则表达式regex的匹配次数:

// Naive implementation
function countOccurrences(regex, str) {
    var count = 0;
    while (regex.test(str)) count++;
    return count;
}

以下是使用此函数的示例:

> countOccurrences(/x/g, '_x_x')
2

第一个问题是,如果正则表达式的/g标志未设置,此函数将进入无限循环。例如:

countOccurrences(/x/, '_x_x') // never terminates

第二个问题是,如果regex.lastIndex不是 0,函数将无法正确工作,因为该属性指示从哪里开始搜索。例如:

> var regex = /x/g;
> regex.lastIndex = 2;
> countOccurrences(regex, '_x_x')
1

以下实现解决了两个问题:

function countOccurrences(regex, str) {
    if (! regex.global) {
        throw new Error('Please set flag /g of regex');
    }
    var origLastIndex = regex.lastIndex;  // store
    regex.lastIndex = 0;

    var count = 0;
    while (regex.test(str)) count++;

    regex.lastIndex = origLastIndex;  // restore
    return count;
}

一个更简单的替代方法是使用match()

function countOccurrences(regex, str) {
    if (! regex.global) {
        throw new Error('Please set flag /g of regex');
    }
    return (str.match(regex) || []).length;
}

有一个可能的陷阱:如果设置了/g标志并且没有匹配项,str.match()将返回null。在前面的代码中,我们通过使用[]来避免这种陷阱,如果match()的结果不是真值。

提示和技巧

本节提供了一些在 JavaScript 中使用正则表达式的技巧和窍门。

引用文本

有时,当手动组装正则表达式时,您希望逐字使用给定的字符串。这意味着不能解释任何特殊字符(例如,*[)-所有这些字符都需要转义。JavaScript 没有内置的方法来进行这种引用,但是您可以编写自己的函数quoteText,它将按以下方式工作:

> console.log(quoteText('*All* (most?) aspects.'))
\*All\* \(most\?\) aspects\.

如果您需要进行多次搜索和替换,则此函数特别方便。然后要搜索的值必须是设置了global标志的正则表达式。使用quoteText(),您可以使用任意字符串。该函数如下所示:

function quoteText(text) {
    return text.replace(/[\\^$.*+?()[\]{}|=!<>:-]/g, '\\$&');
}

所有特殊字符都被转义,因为您可能希望在括号或方括号内引用多个字符。

陷阱:没有断言(例如,^,$),正则表达式可以在任何地方找到

如果您不使用^$等断言,大多数正则表达式方法会在任何地方找到模式。例如:

> /aa/.test('xaay')
true
> /^aa$/.test('xaay')
false

匹配一切或什么都不匹配

这是一个罕见的用例,但有时您需要一个正则表达式,它匹配一切或什么都不匹配。例如,一个函数可能有一个用于过滤的正则表达式参数。如果该参数缺失,您可以给它一个默认值,一个匹配一切的正则表达式。

匹配一切

空正则表达式匹配一切。我们可以基于该正则表达式创建一个RegExp的实例,就像这样:

> new RegExp('').test('dfadsfdsa')
true
> new RegExp('').test('')
true

但是,空正则表达式文字将是//,这在 JavaScript 中被解释为注释。因此,以下是通过文字获得的最接近的:/(?:)/(空的非捕获组)。该组匹配一切,同时不捕获任何内容,这个组不会影响exec()返回的结果。即使 JavaScript 本身在显示空正则表达式时也使用前面的表示:

> new RegExp('')
/(?:)/

匹配什么都不匹配

空正则表达式具有一个反义词——匹配什么都不匹配的正则表达式:

> var never = /.^/;
> never.test('abc')
false
> never.test('')
false

手动实现后行断言

后行断言是一种断言。与先行断言类似,模式用于检查输入中当前位置的某些内容,但在其他情况下被忽略。与先行断言相反,模式的匹配必须结束在当前位置(而不是从当前位置开始)。

以下函数将字符串'NAME'的每个出现替换为参数name的值,但前提是该出现不是由引号引导的。我们通过“手动”检查当前匹配之前的字符来处理引号:

function insertName(str, name) {
    return str.replace(
        /NAME/g,
        function (completeMatch, offset) {
            if (offset === 0 ||
                (offset > 0 && str[offset-1] !== '"')) {
                return name;
            } else {
                return completeMatch;
            }
        }
    );
}
> insertName('NAME "NAME"', 'Jane')
'Jane "NAME"'
> insertName('"NAME" NAME', 'Jane')
'"NAME" Jane'

另一种方法是在正则表达式中包含可能转义的字符。然后,您必须临时向您正在搜索的字符串添加前缀;否则,您将错过该字符串开头的匹配:

function insertName(str, name) {
    var tmpPrefix = ' ';
    str = tmpPrefix + str;
    str = str.replace(
        /([^"])NAME/g,
        function (completeMatch, prefix) {
            return prefix + name;
        }
    );
    return str.slice(tmpPrefix.length); // remove tmpPrefix
}

正则表达式速查表

原子(参见原子:一般):

  • .(点)匹配除行终止符(例如换行符)之外的所有内容。使用[\s\S]来真正匹配一切。

  • 字符类转义:

  • \d匹配数字([0-9]);\D匹配非数字([^0-9])。

  • \w匹配拉丁字母数字字符加下划线([A-Za-z0-9_]);\W匹配所有其他字符。

  • \s匹配所有空白字符(空格、制表符、换行符等);\S匹配所有非空白字符。

  • 字符类(字符集):[...][^...]

  • 源字符:[abc](除\ ] -之外的所有字符都与它们自身匹配)

  • 字符类转义(参见前文):[\d\w]

  • 范围:[A-Za-z0-9]

  • 组:

  • 捕获组:(...);反向引用:\1

  • 非捕获组:(?:...)

量词(参见量词):

  • 贪婪:

  • ? * +

  • {n} {n,} {n,m}

  • 勉强:在任何贪婪量词后面加上?

断言(参见断言):

  • 输入的开始,输入的结束:^ $

  • 在词边界处,不在词边界处:\b \B

  • 正向先行断言:(?=...)(模式必须紧跟其后,但在其他情况下被忽略)

  • 负向先行断言:(?!...)(模式不能紧跟其后,但在其他情况下被忽略)

分支:|

创建正则表达式(参见创建正则表达式):

  • 字面量:/xyz/i(在加载时编译)

  • 构造函数:new RegExp('xzy', 'i')(在运行时编译)

标志(参见标志):

  • 全局:/g(影响几种正则表达式方法)

  • ignoreCase:/i

  • 多行:/m^$按行匹配,而不是完整的输入)

方法:

有关使用标志/g的提示,请参阅Problems with the Flag /g

致谢

Mathias Bynens(@mathias)和 Juan Ignacio Dopazo(@juandopazo)建议使用match()test()来计算出现次数,Šime Vidas(@simevidas)警告我在没有匹配项时要小心使用match()。全局标志导致无限循环的陷阱来自Andrea Giammarchi 的演讲(@webreflection)。Claude Pache 告诉我在quoteText()中转义更多字符。

第二十章:日期

原文:20. Dates

译者:飞龙

协议:CC BY-NC-SA 4.0

JavaScript 的Date构造函数有助于解析、管理和显示日期。本章描述了它的工作原理。

日期 API 使用术语UTC(协调世界时)。在大多数情况下,UTC 是 GMT(格林尼治标准时间)的同义词,大致意味着伦敦,英国的时区。

日期构造函数

有四种调用Date构造函数的方法:

new Date(year, month, date?, hours?, minutes?, seconds?, milliseconds?)

从给定数据构造一个新的日期。时间相对于当前时区进行解释。Date.UTC()提供了类似的功能,但是相对于 UTC。参数具有以下范围:

  • year:对于 0 ≤ year ≤ 99,将添加 1900。

  • month:0-11(0 是一月,1 是二月,依此类推)

  • date:1-31

  • hours:0-23

  • minutes:0-59

  • seconds:0-59

  • milliseconds:0-999

以下是一些示例:

> new Date(2001, 1, 27, 14, 55)
Date {Tue Feb 27 2001 14:55:00 GMT+0100 (CET)}
> new Date(01, 1, 27, 14, 55)
Date {Wed Feb 27 1901 14:55:00 GMT+0100 (CET)}

顺便说一句,JavaScript 继承了略微奇怪的约定,将 0 解释为一月,1 解释为二月,依此类推,这一点来自 Java。

new Date(dateTimeStr)

这是一个将日期时间字符串转换为数字的过程,然后调用new Date(number)日期时间格式解释了日期时间格式。例如:

> new Date('2004-08-29')
Date {Sun Aug 29 2004 02:00:00 GMT+0200 (CEST)}

非法的日期时间字符串导致将NaN传递给new Date(number)

new Date(timeValue)

根据自 1970 年 1 月 1 日 00:00:00 UTC 以来的毫秒数创建日期。例如:

> new Date(0)
Date {Thu Jan 01 1970 01:00:00 GMT+0100 (CET)}

这个构造函数的反函数是getTime()方法,它返回毫秒数:

> new Date(123).getTime()
123

您可以使用NaN作为参数,这将产生Date的一个特殊实例,即“无效日期”:

> var d = new Date(NaN);
> d.toString()
'Invalid Date'
> d.toJSON()
null
> d.getTime()
NaN
> d.getYear()
NaN

new Date()

创建当前日期和时间的对象;它与new Date(Date.now())的作用相同。

日期构造函数方法

构造函数Date有以下方法:

Date.now()

以毫秒为单位返回当前日期和时间(自 1970 年 1 月 1 日 00:00:00 UTC 起)。它产生与new Date().getTime()相同的结果。

Date.parse(dateTimeString)

dateTimeString 转换为自 1970 年 1 月 1 日 00:00:00 UTC 以来的毫秒数。日期时间格式解释了 dateTimeString 的格式。结果可用于调用 new Date(number)。以下是一些示例:

> Date.parse('1970-01-01')
0
> Date.parse('1970-01-02')
86400000

如果无法解析字符串,此方法将返回 NaN

> Date.parse('abc')
NaN

Date.UTC(year, month, date?, hours?, minutes?, seconds?, milliseconds?)

将给定数据转换为自 1970 年 1 月 1 日 00:00:00 UTC 以来的毫秒数。它与具有相同参数的 Date 构造函数有两种不同之处:

  • 它返回一个数字,而不是一个新的日期对象。

  • 它将参数解释为世界协调时间,而不是本地时间。

日期原型方法

本节涵盖了 Date.prototype 的方法。

时间单位的获取器和设置器

时间单位的获取器和设置器可使用以下签名:

  • 本地时间:

  • Date.prototype.get«Unit»() 返回根据本地时间的 Unit

  • Date.prototype.set«Unit»(number) 根据本地时间设置 Unit

  • 世界协调时间:

  • Date.prototype.getUTC«Unit»() 返回根据世界协调时间的 Unit

  • Date.prototype.setUTC«Unit»(number) 根据世界协调时间设置 Unit

Unit 是以下单词之一:

  • FullYear:通常是四位数

  • Month:月份(0-11)

  • Date:月份中的某一天(1-31)

  • Day(仅获取器):星期几(0-6);0 代表星期日

  • Hours:小时(0-23)

  • Minutes:分钟(0-59)

  • Seconds:秒(0-59)

  • Milliseconds:毫秒(0-999)

例如:

> var d = new Date('1968-11-25');
Date {Mon Nov 25 1968 01:00:00 GMT+0100 (CET)}
> d.getDate()
25
> d.getDay()
1

各种获取器和设置器

以下方法使您能够获取和设置自 1970 年 1 月 1 日以来的毫秒数以及更多内容:

Year 单位已被弃用,推荐使用 FullYear

  • Date.prototype.getYear() 已被弃用;请改用 getFullYear()

  • Date.prototype.setYear(number) 已被弃用;请改用 setFullYear()

将日期转换为字符串

请注意,转换为字符串高度依赖于实现。以下日期用于计算以下示例中的输出(在撰写本书时,Firefox 的支持最完整):

new Date(2001,9,30, 17,43,7, 856);

时间(可读)

  • Date.prototype.toTimeString()

    17:43:07 GMT+0100 (CET)
    

以当前时区的时间显示。

  • Date.prototype.toLocaleTimeString()

    17:43:07
    

以特定于区域的格式显示的时间。此方法由 ECMAScript 国际化 API(参见ECMAScript 国际化 API)提供,并且如果没有它,就没有太多意义。

日期(可读)

  • Date.prototype.toDateString()

    Tue Oct 30 2001
    

日期。

  • Date.prototype.toLocaleDateString()

    10/30/2001
    

以特定于区域的格式显示的日期。此方法由 ECMAScript 国际化 API(参见ECMAScript 国际化 API)提供,并且如果没有它,就没有太多意义。

日期和时间(可读)

  • Date.prototype.toString()

    Tue Oct 30 2001 17:43:07 GMT+0100 (CET)
    

日期和时间,以当前时区的时间。对于没有毫秒的任何 Date 实例(即秒数已满),以下表达式为真:

    Date.parse(d.toString()) === d.valueOf()
    ```

+   `Date.prototype.toLocaleString()`:

    ```js
    Tue Oct 30 17:43:07 2001
    ```

以区域特定格式的日期和时间。此方法由 ECMAScript 国际化 API 提供(请参见[ECMAScript 国际化 API](ch30.html#i18n_api "ECMAScript 国际化 API")),如果没有它,这个方法就没有太多意义。

+   `Date.prototype.toUTCString()`:

    ```js
    Tue, 30 Oct 2001 16:43:07 GMT
    ```

日期和时间,使用 UTC。

+   `Date.prototype.toGMTString()`:

已弃用;请改用`toUTCString()`。

日期和时间(机器可读)

+   `Date.prototype.toISOString()`:

    ```js
    2001-10-30T16:43:07.856Z
    ```

所有内部属性都显示在返回的字符串中。格式符合[日期时间格式](ch20.html#date_time_formats "日期时间格式");时区始终为`Z`。

+   `Date.prototype.toJSON()`:

此方法内部调用`toISOString()`。它被`JSON.stringify()`(参见[JSON.stringify(value, replacer?, space?)](ch22.html#JSON.stringify "JSON.stringify(value, replacer?, space?)"))用于将日期对象转换为 JSON 字符串。

## 日期时间格式

本节描述了以字符串形式表示时间点的格式。有许多方法可以这样做:仅指示日期,包括一天中的时间,省略时区,指定时区等。在支持日期时间格式方面,ECMAScript 5 紧密遵循标准 ISO 8601 扩展格式。JavaScript 引擎相对完全地实现了 ECMAScript 规范,但仍然存在一些变化,因此您必须保持警惕。

最长的日期时间格式是:

```js
YYYY-MM-DDTHH:mm:ss.sssZ

每个部分代表日期时间数据的几个十进制数字。例如,YYYY表示格式以四位数年份开头。以下各小节解释了每个部分的含义。这些格式与以下方法相关:

  • Date.parse() 可以解析这些格式。

  • new Date()可以解析这些格式。

  • Date.prototype.toISOString()创建了上述“完整”格式的字符串:

    > new Date().toISOString()
    '2014-09-12T23:05:07.414Z'
    

日期格式(无时间)

以下日期格式可用:

YYYY-MM-DD
YYYY-MM
YYYY

它们包括以下部分:

  • YYYY 指的是年份(公历)。

  • MM指的是月份,从 01 到 12。

  • DD 指的是日期,从 01 到 31。

例如:

> new Date('2001-02-22')
Date {Thu Feb 22 2001 01:00:00 GMT+0100 (CET)}

时间格式(无日期)

以下时间格式可用。如您所见,时区信息Z是可选的:

THH:mm:ss.sss
THH:mm:ss.sssZ

THH:mm:ss
THH:mm:ssZ

THH:mm
THH:mmZ

它们包括以下部分:

  • T是格式时间部分的前缀(字面上的T,而不是数字)。

  • HH指的是小时,从 00 到 23。您可以使用 24 作为HH的值(指的是第二天的 00 小时),但是接下来的所有部分都必须为 0。

  • mm 表示分钟,从 00 到 59。

  • ss 表示秒,从 00 到 59。

  • sss 表示毫秒,从 000 到 999。

  • Z指的是时区,以下两者之一:

  • Z”表示 UTC

  • +”或“-”后跟时间“hh:mm

一些 JavaScript 引擎允许您仅指定时间(其他需要日期):

> new Date('T13:17')
Date {Thu Jan 01 1970 13:17:00 GMT+0100 (CET)}

日期时间格式

日期格式和时间格式也可以结合使用。在日期时间格式中,您可以使用日期或日期和时间(或在某些引擎中仅使用时间)。例如:

> new Date('2001-02-22T13:17')
Date {Thu Feb 22 2001 13:17:00 GMT+0100 (CET)}

时间值:自 1970-01-01 以来的毫秒数

日期 API 称之为time的东西在 ECMAScript 规范中被称为时间值。它是一个原始数字,以自 1970 年 1 月 1 日 00:00:00 UTC 以来的毫秒数编码日期。每个日期对象都将其状态存储为时间值,在内部属性[[PrimitiveValue]]中(与包装构造函数BooleanNumberString的实例用于存储其包装的原始值的相同属性)。

警告

时间值中忽略了闰秒。

以下方法适用于时间值:

  • new Date(timeValue) 使用时间值创建日期。

  • Date.parse(dateTimeString)解析带有日期时间字符串的字符串并返回时间值。

  • Date.now()返回当前日期时间作为时间值。

  • Date.UTC(year, month, date?, hours?, minutes?, seconds?, milliseconds?) 解释参数相对于 UTC 并返回时间值。

  • Date.prototype.getTime() 返回接收器中存储的时间值。

  • Date.prototype.setTime(timeValue)根据指定的时间值更改日期。

  • Date.prototype.valueOf()返回接收者中存储的时间值。该方法确定了如何将日期转换为原始值,如下一小节所述。

JavaScript 整数的范围(53 位加上一个符号)足够大,可以表示从 1970 年前约 285,616 年开始到 1970 年后约 285,616 年结束的时间跨度。

以下是将日期转换为时间值的几个示例:

> new Date('1970-01-01').getTime()
0
> new Date('1970-01-02').getTime()
86400000
> new Date('1960-01-02').getTime()
-315532800000

Date构造函数使您能够将时间值转换为日期:

> new Date(0)
Date {Thu Jan 01 1970 01:00:00 GMT+0100 (CET)}
> new Date(24 * 60 * 60 * 1000)  // 1 day in ms
Date {Fri Jan 02 1970 01:00:00 GMT+0100 (CET)}
> new Date(-315532800000)
Date {Sat Jan 02 1960 01:00:00 GMT+0100 (CET)}

将日期转换为数字

通过Date.prototype.valueOf()将日期转换为数字,返回一个时间值。这使您能够比较日期:

> new Date('1980-05-21') > new Date('1980-05-20')
true

您也可以进行算术运算,但要注意闰秒被忽略:

> new Date('1980-05-21') - new Date('1980-05-20')
86400000

警告

使用加号运算符(+)将日期加到另一个日期或数字会得到一个字符串,因为将日期转换为原始值的默认方式是将日期转换为字符串(请参阅加号运算符(+)了解加号运算符的工作原理):

> new Date('2024-10-03') + 86400000
'Thu Oct 03 2024 02:00:00 GMT+0200 (CEST)86400000'
> new Date(Number(new Date('2024-10-03')) + 86400000)
Fri Oct 04 2024 02:00:00 GMT+0200 (CEST)