Math是 JavaScript 的原生对象,提供各种数学功能。该对象不是构造函数,不能生成实例,所有的属性和方法都必须在Math对象上调用。

静态属性

Math对象的静态属性,提供以下一些数学常数。

  • Math.E:常数e
  • Math.LN2:2 的自然对数。
  • Math.LN10:10 的自然对数。
  • Math.LOG2E:以 2 为底的e的对数。
  • Math.LOG10E:以 10 为底的e的对数。
  • Math.PI:常数 Pi。
  • Math.SQRT1_2:0.5 的平方根。
  • Math.SQRT2:2 的平方根。
1
2
3
4
5
6
7
8
Math.E; // 2.718281828459045
Math.LN2; // 0.6931471805599453
Math.LN10; // 2.302585092994046
Math.LOG2E; // 1.4426950408889634
Math.LOG10E; // 0.4342944819032518
Math.PI; // 3.141592653589793
Math.SQRT1_2; // 0.7071067811865476
Math.SQRT2; // 1.4142135623730951

这些属性都是只读的,不能修改。

静态方法

Math对象提供以下一些静态方法。

  • Math.abs():绝对值
  • Math.ceil():向上取整
  • Math.floor():向下取整
  • Math.max():最大值
  • Math.min():最小值
  • Math.pow():指数运算
  • Math.sqrt():平方根
  • Math.log():自然对数
  • Math.exp():e 的指数
  • Math.round():四舍五入
  • Math.random():随机数

Math.abs()

Math.abs方法返回参数值的绝对值。

1
2
Math.abs(1); // 1
Math.abs(-1); // 1

Math.max(),Math.min()

Math.max方法返回参数之中最大的那个值,Math.min返回最小的那个值。如果参数为空, Math.min返回Infinity, Math.max返回-Infinity

1
2
3
4
Math.max(2, -1, 5); // 5
Math.min(2, -1, 5); // -1
Math.min(); // Infinity
Math.max(); // -Infinity

Math.floor(),Math.ceil()

Math.floor方法返回小于参数值的最大整数(地板值)。

1
2
Math.floor(3.2); // 3
Math.floor(-3.2); // -4

Math.ceil方法返回大于参数值的最小整数(天花板值)。

1
2
Math.ceil(3.2); // 4
Math.ceil(-3.2); // -3

这两个方法可以结合起来,实现一个总是返回数值的整数部分的函数。

1
2
3
4
5
6
7
8
9
10
11
function ToInteger(x) {
x = Number(x);
return x < 0 ? Math.ceil(x) : Math.floor(x);
}

ToInteger(3.2); // 3
ToInteger(3.5); // 3
ToInteger(3.8); // 3
ToInteger(-3.2); // -3
ToInteger(-3.5); // -3
ToInteger(-3.8); // -3

上面代码中,不管正数或负数,ToInteger函数总是返回一个数值的整数部分。

Math.round()

Math.round方法用于四舍五入。

1
2
3
4
5
6
Math.round(0.1); // 0
Math.round(0.5); // 1
Math.round(0.6); // 1

// 等同于
Math.floor(x + 0.5);

注意,它对负数的处理(主要是对0.5的处理)。

1
2
3
Math.round(-1.1); // -1
Math.round(-1.5); // -1
Math.round(-1.6); // -2

Math.pow()

Math.pow方法返回以第一个参数为底数、第二个参数为幂的指数值。

1
2
3
4
// 等同于 2 ** 2
Math.pow(2, 2); // 4
// 等同于 2 ** 3
Math.pow(2, 3); // 8

下面是计算圆面积的方法。

1
2
var radius = 20;
var area = Math.PI * Math.pow(radius, 2);

Math.sqrt()

Math.sqrt方法返回参数值的平方根。如果参数是一个负值,则返回NaN

1
2
Math.sqrt(4); // 2
Math.sqrt(-4); // NaN

Math.log()

Math.log方法返回以e为底的自然对数值。

1
2
Math.log(Math.E); // 1
Math.log(10); // 2.302585092994046

如果要计算以 10 为底的对数,可以先用Math.log求出自然对数,然后除以Math.LN10;求以 2 为底的对数,可以除以Math.LN2

1
2
Math.log(100) / Math.LN10; // 2
Math.log(8) / Math.LN2; // 3

Math.exp()

Math.exp方法返回常数e的参数次方。

1
2
Math.exp(1); // 2.718281828459045
Math.exp(3); // 20.085536923187668

Math.random()

Math.random()返回 0 到 1 之间的一个伪随机数,可能等于 0,但是一定小于 1。

1
Math.random(); // 0.7151307314634323

任意范围的随机数生成函数如下。

1
2
3
4
5
6
function getRandomArbitrary(min, max) {
return Math.random() * (max - min) + min;
}

getRandomArbitrary(1.5, 6.5);
// 2.4942810038223864

任意范围的随机整数生成函数如下。

1
2
3
4
5
function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}

getRandomInt(1, 6); // 5

返回随机字符的例子如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
function random_str(length) {
var ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
ALPHABET += "abcdefghijklmnopqrstuvwxyz";
ALPHABET += "0123456789-_";
var str = "";
for (var i = 0; i < length; ++i) {
var rand = Math.floor(Math.random() * ALPHABET.length);
str += ALPHABET.substring(rand, rand + 1);
}
return str;
}

random_str(6); // "NdQKOr"

上面代码中,random_str函数接受一个整数作为参数,返回变量ALPHABET内的随机字符所组成的指定长度的字符串。

三角函数方法

Math对象还提供一系列三角函数方法。

  • Math.sin():返回参数的正弦(参数为弧度值)
  • Math.cos():返回参数的余弦(参数为弧度值)
  • Math.tan():返回参数的正切(参数为弧度值)
  • Math.asin():返回参数的反正弦(返回值为弧度值)
  • Math.acos():返回参数的反余弦(返回值为弧度值)
  • Math.atan():返回参数的反正切(返回值为弧度值)
1
2
3
4
5
6
7
8
9
Math.sin(0); // 0
Math.cos(0); // 1
Math.tan(0); // 0

Math.sin(Math.PI / 2); // 1

Math.asin(1); // 1.5707963267948966
Math.acos(1); // 0
Math.atan(1); // 0.7853981633974483

概述

正则表达式(regular expression)是一种表达文本模式(即字符串结构)的方法,有点像字符串的模板,常常用来按照“给定模式”匹配文本。比如,正则表达式给出一个 Email 地址的模式,然后用它来确定一个字符串是否为 Email 地址。JavaScript 的正则表达式体系是参照 Perl 5 建立的。

新建正则表达式有两种方法。一种是使用字面量,以斜杠表示开始和结束。

1
var regex = /xyz/;

另一种是使用RegExp构造函数。

1
var regex = new RegExp("xyz");

上面两种写法是等价的,都新建了一个内容为xyz的正则表达式对象。它们的主要区别是,第一种方法在引擎编译代码时,就会新建正则表达式,第二种方法在运行时新建正则表达式,所以前者的效率较高。而且,前者比较便利和直观,所以实际应用中,基本上都采用字面量定义正则表达式。

RegExp构造函数还可以接受第二个参数,表示修饰符(详细解释见下文)。

1
2
3
var regex = new RegExp("xyz", "i");
// 等价于
var regex = /xyz/i;

上面代码中,正则表达式/xyz/有一个修饰符i

实例属性

正则对象的实例属性分成两类。

一类是修饰符相关,返回一个布尔值,表示对应的修饰符是否设置。

  • RegExp.prototype.ignoreCase:返回一个布尔值,表示是否设置了i修饰符。
  • RegExp.prototype.global:返回一个布尔值,表示是否设置了g修饰符。
  • RegExp.prototype.multiline:返回一个布尔值,表示是否设置了m修饰符。

上面三个属性都是只读的。

1
2
3
4
5
var r = /abc/gim;

r.ignoreCase; // true
r.global; // true
r.multiline; // true

另一类是与修饰符无关的属性,主要是下面两个。

  • RegExp.prototype.lastIndex:返回一个数值,表示下一次开始搜索的位置。该属性可读写,但是只在进行连续搜索时有意义,详细介绍请看后文。
  • RegExp.prototype.source:返回正则表达式的字符串形式(不包括反斜杠),该属性只读。
1
2
3
4
var r = /abc/gim;

r.lastIndex; // 0
r.source; // "abc"

实例方法

RegExp.prototype.test()

正则实例对象的test方法返回一个布尔值,表示当前模式是否能匹配参数字符串。

1
/cat/.test("cats and dogs"); // true

上面代码验证参数字符串之中是否包含cat,结果返回true

如果正则表达式带有g修饰符,则每一次test方法都从上一次结束的位置开始向后匹配。

1
2
3
4
5
6
7
8
9
10
11
var r = /x/g;
var s = "_x_x";

r.lastIndex; // 0
r.test(s); // true

r.lastIndex; // 2
r.test(s); // true

r.lastIndex; // 4
r.test(s); // false

上面代码的正则表达式使用了g修饰符,表示是全局搜索,会有多个结果。接着,三次使用test方法,每一次开始搜索的位置都是上一次匹配的后一个位置。

带有g修饰符时,可以通过正则对象的lastIndex属性指定开始搜索的位置。

1
2
3
4
5
var r = /x/g;
var s = "_x_x";

r.lastIndex = 4;
r.test(s); // false

上面代码指定从字符串的第五个位置开始搜索,这个位置是没有字符的,所以返回false

lastIndex属性只对同一个正则表达式有效,所以下面这样写是错误的。

1
2
var count = 0;
while (/a/g.test("babaa")) count++;

上面代码会导致无限循环,因为while循环的每次匹配条件都是一个新的正则表达式,导致lastIndex属性总是等于 0。

如果正则模式是一个空字符串,则匹配所有字符串。

1
2
new RegExp("").test("abc");
// true

RegExp.prototype.exec()

正则实例对象的exec方法,用来返回匹配结果。如果发现匹配,就返回一个数组,成员是匹配成功的子字符串,否则返回null

1
2
3
4
5
6
var s = "_x_x";
var r1 = /x/;
var r2 = /y/;

r1.exec(s); // ["x"]
r2.exec(s); // null

上面代码中,正则对象r1匹配成功,返回一个数组,成员是匹配结果;正则对象r2匹配失败,返回null

如果正则表示式包含圆括号(即含有“组匹配”),则返回的数组会包括多个成员。第一个成员是整个匹配成功的结果,后面的成员就是圆括号对应的匹配成功的组。也就是说,第二个成员对应第一个括号,第三个成员对应第二个括号,以此类推。整个数组的length属性等于组匹配的数量再加 1。

1
2
3
4
var s = "_x_x";
var r = /_(x)/;

r.exec(s); // ["_x", "x"]

上面代码的exec方法,返回一个数组。第一个成员是整个匹配的结果,第二个成员是圆括号匹配的结果。

exec方法的返回数组还包含以下两个属性:

  • input:整个原字符串。
  • index:整个模式匹配成功的开始位置(从 0 开始计数)。
1
2
3
4
5
6
7
var r = /a(b+)a/;
var arr = r.exec("_abbba_aba_");

arr; // ["abbba", "bbb"]

arr.index; // 1
arr.input; // "_abbba_aba_"

上面代码中的index属性等于 1,是因为从原字符串的第二个位置开始匹配成功。

如果正则表达式加上g修饰符,则可以使用多次exec方法,下一次搜索的位置从上一次匹配成功结束的位置开始。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var reg = /a/g;
var str = "abc_abc_abc";

var r1 = reg.exec(str);
r1; // ["a"]
r1.index; // 0
reg.lastIndex; // 1

var r2 = reg.exec(str);
r2; // ["a"]
r2.index; // 4
reg.lastIndex; // 5

var r3 = reg.exec(str);
r3; // ["a"]
r3.index; // 8
reg.lastIndex; // 9

var r4 = reg.exec(str);
r4; // null
reg.lastIndex; // 0

上面代码连续用了四次exec方法,前三次都是从上一次匹配结束的位置向后匹配。当第三次匹配结束以后,整个字符串已经到达尾部,匹配结果返回null,正则实例对象的lastIndex属性也重置为0,意味着第四次匹配将从头开始。

利用g修饰符允许多次匹配的特点,可以用一个循环完成全部匹配。

1
2
3
4
5
6
7
8
9
10
11
var reg = /a/g;
var str = "abc_abc_abc";

while (true) {
var match = reg.exec(str);
if (!match) break;
console.log("#" + match.index + ":" + match[0]);
}
// #0:a
// #4:a
// #8:a

上面代码中,只要exec方法不返回null,就会一直循环下去,每次输出匹配的位置和匹配的文本。

正则实例对象的lastIndex属性不仅可读,还可写。设置了g修饰符的时候,只要手动设置了lastIndex的值,就会从指定位置开始匹配。

字符串的实例方法

字符串的实例方法之中,有 4 种与正则表达式有关。

  • String.prototype.match():返回一个数组,成员是所有匹配的子字符串。
  • String.prototype.search():按照给定的正则表达式进行搜索,返回一个整数,表示匹配开始的位置。
  • String.prototype.replace():按照给定的正则表达式进行替换,返回替换后的字符串。
  • String.prototype.split():按照给定规则进行字符串分割,返回一个数组,包含分割后的各个成员。

String.prototype.match()

字符串实例对象的match方法对字符串进行正则匹配,返回匹配结果。

1
2
3
4
5
6
var s = "_x_x";
var r1 = /x/;
var r2 = /y/;

s.match(r1); // ["x"]
s.match(r2); // null

从上面代码可以看到,字符串的match方法与正则对象的exec方法非常类似:匹配成功返回一个数组,匹配失败返回null

如果正则表达式带有g修饰符,则该方法与正则对象的exec方法行为不同,会一次性返回所有匹配成功的结果。

1
2
3
4
5
var s = "abba";
var r = /a/g;

s.match(r); // ["a", "a"]
r.exec(s); // ["a"]

设置正则表达式的lastIndex属性,对match方法无效,匹配总是从字符串的第一个字符开始。

1
2
3
4
var r = /a|b/g;
r.lastIndex = 7;
"xaxb".match(r); // ['a', 'b']
r.lastIndex; // 0

上面代码表示,设置正则对象的lastIndex属性是无效的。

字符串对象的search方法,返回第一个满足条件的匹配结果在整个字符串中的位置。如果没有任何匹配,则返回-1

1
2
"_x_x".search(/x/);
// 1

上面代码中,第一个匹配结果出现在字符串的1号位置。

String.prototype.replace()

字符串对象的replace方法可以替换匹配的值。它接受两个参数,第一个是正则表达式,表示搜索模式,第二个是替换的内容。

1
str.replace(search, replacement);

正则表达式如果不加g修饰符,就替换第一个匹配成功的值,否则替换所有匹配成功的值。

1
2
3
"aaa".replace("a", "b"); // "baa"
"aaa".replace(/a/, "b"); // "baa"
"aaa".replace(/a/g, "b"); // "bbb"

上面代码中,最后一个正则表达式使用了g修饰符,导致所有的b都被替换掉了。

replace方法的一个应用,就是消除字符串首尾两端的空格。

1
2
3
4
var str = "  #id div.class  ";

str.replace(/^\s+|\s+$/g, "");
// "#id div.class"

replace方法的第二个参数可以使用美元符号$,用来指代所替换的内容。

  • $&:匹配的子字符串。
  • $`:匹配结果前面的文本。
  • $’:匹配结果后面的文本。
  • $n:匹配成功的第n组内容,n是从 1 开始的自然数。
  • $$:指代美元符号$
1
2
3
4
5
"hello world".replace(/(\w+)\s(\w+)/, "$2 $1");
// "world hello"

"abc".replace("b", "[$`-$&-$']");
// "a[a-b-c]c"

上面代码中,第一个例子是将匹配的组互换位置,第二个例子是改写匹配的值。

replace方法的第二个参数还可以是一个函数,将每一个匹配内容替换为函数返回值。

1
2
3
4
5
6
7
8
9
10
11
12
"3 and 5".replace(/[0-9]+/g, function (match) {
return 2 * match;
});
// "6 and 10"

var a = "The quick brown fox jumped over the lazy dog.";
var pattern = /quick|brown|lazy/gi;

a.replace(pattern, function replacer(match) {
return match.toUpperCase();
});
// The QUICK BROWN fox jumped over the LAZY dog.

作为replace方法第二个参数的替换函数,可以接受多个参数。其中,第一个参数是捕捉到的内容,第二个参数是捕捉到的组匹配(有多少个组匹配,就有多少个对应的参数)。此外,最后还可以添加两个参数,倒数第二个参数是捕捉到的内容在整个字符串中的位置(比如从第五个位置开始),最后一个参数是原字符串。下面是一个网页模板替换的例子。

1
2
3
4
5
6
7
8
9
10
11
12
var prices = {
p1: "$1.99",
p2: "$9.99",
p3: "$5.00"
};

var template = '<span id="p1"></span>' + '<span id="p2"></span>' + '<span id="p3"></span>';

template.replace(/(<span id=")(.*?)(">)(<\/span>)/g, function (match, $1, $2, $3, $4) {
return $1 + $2 + $3 + prices[$2] + $4;
});
// "<span id="p1">$1.99</span><span id="p2">$9.99</span><span id="p3">$5.00</span>"

上面代码的捕捉模式中,有四个括号,所以会产生四个组匹配,在匹配函数中用$1$4表示。匹配函数的作用是将价格插入模板中。

String.prototype.split()

字符串对象的split方法按照正则规则分割字符串,返回一个由分割后的各个部分组成的数组。

1
str.split(separator, [limit]);

该方法接受两个参数,第一个参数是正则表达式,表示分隔规则,第二个参数是返回数组的最大成员数。

1
2
3
4
5
6
7
8
9
10
// 非正则分隔
"a, b,c, d".split(",");
// [ 'a', ' b', 'c', ' d' ]

// 正则分隔,去除多余的空格
"a, b,c, d".split(/, */);
// [ 'a', 'b', 'c', 'd' ]

// 指定返回数组的最大成员
"a, b,c, d".split(/, */, 2)[("a", "b")];

上面代码使用正则表达式,去除了子字符串的逗号后面的空格。

1
2
3
4
5
6
7
// 例一
"aaa*a*".split(/a*/);
// [ '', '*', '*' ]

// 例二
"aaa**a*".split(/a*/);
// ["", "*", "*", "*"]

上面代码的分割规则是 0 次或多次的a,由于正则默认是贪婪匹配,所以例一的第一个分隔符是aaa,第二个分割符是a,将字符串分成三个部分,包含开始处的空字符串。例二的第一个分隔符是aaa,第二个分隔符是 0 个a(即空字符),第三个分隔符是a,所以将字符串分成四个部分。

如果正则表达式带有括号,则括号匹配的部分也会作为数组成员返回。

1
2
"aaa*a*".split(/(a*)/);
// [ '', 'aaa', '*', 'a', '*' ]

上面代码的正则表达式使用了括号,第一个组匹配是aaa,第二个组匹配是a,它们都作为数组成员返回。

匹配规则

正则表达式的规则很复杂,下面一一介绍这些规则。

字面量字符和元字符

大部分字符在正则表达式中,就是字面的含义,比如/a/匹配a/b/匹配b。如果在正则表达式之中,某个字符只表示它字面的含义(就像前面的ab),那么它们就叫做“字面量字符”(literal characters)。

1
/dog/.test("old dog"); // true

上面代码中正则表达式的dog,就是字面量字符,所以/dog/匹配old dog,因为它就表示dog三个字母连在一起。

除了字面量字符以外,还有一部分字符有特殊含义,不代表字面的意思。它们叫做“元字符”(metacharacters),主要有以下几个。

(1)点字符(.)

点字符(.)匹配除回车(\r)、换行(\n) 、行分隔符(\u2028)和段分隔符(\u2029)以外的所有字符。注意,对于码点大于0xFFFF的 Unicode 字符,点字符不能正确匹配,会认为这是两个字符。

1
/c.t/;

上面代码中,c.t匹配ct之间包含任意一个字符的情况,只要这三个字符在同一行,比如catc2tc-t等等,但是不匹配coot

(2)位置字符

位置字符用来提示字符所处的位置,主要有两个字符。

  • ^ 表示字符串的开始位置
  • $ 表示字符串的结束位置
1
2
3
4
5
6
7
8
9
// test必须出现在开始位置
/^test/.test('test123') // true

// test必须出现在结束位置
/test$/.test('new test') // true

// 从开始位置到结束位置只有test
/^test$/.test('test') // true
/^test$/.test('test test') // false

(3)选择符(|

竖线符号(|)在正则表达式中表示“或关系”(OR),即cat|dog表示匹配catdog

1
/11|22/.test("911"); // true

上面代码中,正则表达式指定必须匹配1122

多个选择符可以联合使用。

1
2
// 匹配fred、barney、betty之中的一个
/fred|barney|betty/;

选择符会包括它前后的多个字符,比如/ab|cd/指的是匹配ab或者cd,而不是指匹配b或者c。如果想修改这个行为,可以使用圆括号。

1
/a( |\t)b/.test("a\tb"); // true

上面代码指的是,ab之间有一个空格或者一个制表符。

其他的元字符还包括\\\*+?()[]{}等,将在下文解释。

转义符

正则表达式中那些有特殊含义的元字符,如果要匹配它们本身,就需要在它们前面要加上反斜杠。比如要匹配+,就要写成\+

1
2
3
4
5
/1+1/.test('1+1')
// false

/1\+1/.test('1+1')
// true

上面代码中,第一个正则表达式之所以不匹配,因为加号是元字符,不代表自身。第二个正则表达式使用反斜杠对加号转义,就能匹配成功。

正则表达式中,需要反斜杠转义的,一共有 12 个字符:^.[$()|*+?{\\。需要特别注意的是,如果使用RegExp方法生成正则对象,转义需要使用两个斜杠,因为字符串内部会先转义一次。

1
2
3
4
5
6
7
8
new RegExp("1+1")
.test("1+1")(
// false

new RegExp("1\\+1")
)
.test("1+1");
// true

上面代码中,RegExp作为构造函数,参数是一个字符串。但是,在字符串内部,反斜杠也是转义字符,所以它会先被反斜杠转义一次,然后再被正则表达式转义一次,因此需要两个反斜杠转义。

特殊字符

正则表达式对一些不能打印的特殊字符,提供了表达方法。

  • \cX 表示Ctrl-[X],其中的X是 A-Z 之中任一个英文字母,用来匹配控制字符。
  • [\b] 匹配退格键(U+0008),不要与\b混淆。
  • \n 匹配换行键。
  • \r 匹配回车键。
  • \t 匹配制表符 tab(U+0009)。
  • \v 匹配垂直制表符(U+000B)。
  • \f 匹配换页符(U+000C)。
  • \0 匹配null字符(U+0000)。
  • \xhh 匹配一个以两位十六进制数(\x00-\xFF)表示的字符。
  • \uhhhh 匹配一个以四位十六进制数(\u0000-\uFFFF)表示的 Unicode 字符。

字符类

字符类(class)表示有一系列字符可供选择,只要匹配其中一个就可以了。所有可供选择的字符都放在方括号内,比如[xyz] 表示xyz之中任选一个匹配。

1
2
/[abc]/.test('hello world') // false
/[abc]/.test('apple') // true

上面代码中,字符串hello world不包含abc这三个字母中的任一个,所以返回false;字符串apple包含字母a,所以返回true

有两个字符在字符类中有特殊含义。

(1)脱字符(^)

如果方括号内的第一个字符是[^],则表示除了字符类之中的字符,其他字符都可以匹配。比如,[^xyz]表示除了xyz之外都可以匹配。

1
2
/[^abc]/.test('hello world') // true
/[^abc]/.test('bbc') // false

上面代码中,字符串hello world不包含字母abc中的任一个,所以返回true;字符串bbc不包含abc以外的字母,所以返回false

如果方括号内没有其他字符,即只有[^],就表示匹配一切字符,其中包括换行符。相比之下,点号作为元字符(.)是不包括换行符的。

1
2
3
4
var s = "Please yes\nmake my day!";

s.match(/yes.*day/); // null
s.match(/yes[^]*day/); // [ 'yes\nmake my day']

上面代码中,字符串s含有一个换行符,点号不包括换行符,所以第一个正则表达式匹配失败;第二个正则表达式[^]包含一切字符,所以匹配成功。

注意,脱字符只有在字符类的第一个位置才有特殊含义,否则就是字面含义。

(2)连字符(-)

某些情况下,对于连续序列的字符,连字符(-)用来提供简写形式,表示字符的连续范围。比如,[abc]可以写成[a-c][0123456789]可以写成[0-9],同理[A-Z]表示 26 个大写字母。

1
2
/a-z/.test('b') // false
/[a-z]/.test('b') // true

上面代码中,当连字号(dash)不出现在方括号之中,就不具备简写的作用,只代表字面的含义,所以不匹配字符b。只有当连字号用在方括号之中,才表示连续的字符序列。

以下都是合法的字符类简写形式。

1
2
3
4
[0-9.,]
[0-9a-fA-F]
[a-zA-Z0-9-]
[1-31]

上面代码中最后一个字符类[1-31],不代表131,只代表13

连字符还可以用来指定 Unicode 字符的范围。

1
2
3
var str = "\u0130\u0131\u0132";
/[\u0128-\uFFFF]/.test(str);
// true

上面代码中,\u0128-\uFFFF表示匹配码点在0128FFFF之间的所有字符。

另外,不要过分使用连字符,设定一个很大的范围,否则很可能选中意料之外的字符。最典型的例子就是[A-z],表面上它是选中从大写的A到小写的z之间 52 个字母,但是由于在 ASCII 编码之中,大写字母与小写字母之间还有其他字符,结果就会出现意料之外的结果。

1
/[A-z]/.test("\\"); // true

上面代码中,由于反斜杠(’\‘)的 ASCII 码在大写字母与小写字母之间,结果会被选中。

预定义模式

预定义模式指的是某些常见模式的简写方式。

  • \d 匹配 0-9 之间的任一数字,相当于[0-9]
  • \D 匹配所有 0-9 以外的字符,相当于[^0-9]
  • \w 匹配任意的字母、数字和下划线,相当于[A-Za-z0-9_]
  • \W 除所有字母、数字和下划线以外的字符,相当于[^A-Za-z0-9_]
  • \s 匹配空格(包括换行符、制表符、空格符等),相等于[ \t\r\n\v\f]
  • \S 匹配非空格的字符,相当于[^ \t\r\n\v\f]
  • \b 匹配词的边界。
  • \B 匹配非词边界,即在词的内部。

下面是一些例子。

1
2
3
4
5
6
7
8
9
10
11
// \s 的例子
/\s\w*/.exec('hello world') // [" world"]

// \b 的例子
/\bworld/.test('hello world') // true
/\bworld/.test('hello-world') // true
/\bworld/.test('helloworld') // false

// \B 的例子
/\Bworld/.test('hello-world') // false
/\Bworld/.test('helloworld') // true

上面代码中,\s表示空格,所以匹配结果会包括空格。\b表示词的边界,所以world的词首必须独立(词尾是否独立未指定),才会匹配。同理,\B表示非词的边界,只有world的词首不独立,才会匹配。

通常,正则表达式遇到换行符(\n)就会停止匹配。

1
2
3
4
var html = "<b>Hello</b>\n<i>world!</i>";

/.*/.exec(html)[0];
// "<b>Hello</b>"

上面代码中,字符串html包含一个换行符,结果点字符(.)不匹配换行符,导致匹配结果可能不符合原意。这时使用\s字符类,就能包括换行符。

1
2
3
4
var html = "<b>Hello</b>\n<i>world!</i>";

/[\S\s]*/.exec(html)[0];
// "<b>Hello</b>\n<i>world!</i>"

上面代码中,[\S\s]指代一切字符。

重复类

模式的精确匹配次数,使用大括号({})表示。{n}表示恰好重复n次,{n,}表示至少重复n次,{n,m}表示重复不少于n次,不多于m次。

1
2
/lo{2}k/.test('look') // true
/lo{2,5}k/.test('looook') // true

上面代码中,第一个模式指定o连续出现 2 次,第二个模式指定o连续出现 2 次到 5 次之间。

量词符

量词符用来设定某个模式出现的次数。

  • ? 问号表示某个模式出现 0 次或 1 次,等同于{0, 1}
  • * 星号表示某个模式出现 0 次或多次,等同于{0,}
  • + 加号表示某个模式出现 1 次或多次,等同于{1,}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// t 出现0次或1次
/t?est/.test('test') // true
/t?est/.test('est') // true

// t 出现1次或多次
/t+est/.test('test') // true
/t+est/.test('ttest') // true
/t+est/.test('est') // false

// t 出现0次或多次
/t*est/.test('test') // true
/t*est/.test('ttest') // true
/t*est/.test('tttest') // true
/t*est/.test('est') // true

贪婪模式

上一小节的三个量词符,默认情况下都是最大可能匹配,即匹配直到下一个字符不满足匹配规则为止。这被称为贪婪模式。

1
2
var s = "aaa";
s.match(/a+/); // ["aaa"]

上面代码中,模式是/a+/,表示匹配 1 个a或多个a,那么到底会匹配几个a呢?因为默认是贪婪模式,会一直匹配到字符a不出现为止,所以匹配结果是 3 个a

如果想将贪婪模式改为非贪婪模式,可以在量词符后面加一个问号。

1
2
var s = "aaa";
s.match(/a+?/); // ["a"]

上面代码中,模式结尾添加了一个问号/a+?/,这时就改为非贪婪模式,一旦条件满足,就不再往下匹配。

除了非贪婪模式的加号,还有非贪婪模式的星号(*)。

  • *?:表示某个模式出现 0 次或多次,匹配时采用非贪婪模式。
  • +?:表示某个模式出现 1 次或多次,匹配时采用非贪婪模式。

修饰符

修饰符(modifier)表示模式的附加规则,放在正则模式的最尾部。

修饰符可以单个使用,也可以多个一起使用。

1
2
3
4
5
// 单个修饰符
var regex = /test/i;

// 多个修饰符
var regex = /test/gi;

(1)g 修饰符

默认情况下,第一次匹配成功后,正则对象就停止向下匹配了。g修饰符表示全局匹配(global),加上它以后,正则对象将匹配全部符合条件的结果,主要用于搜索和替换。

1
2
3
4
5
6
var regex = /b/;
var str = "abba";

regex.test(str); // true
regex.test(str); // true
regex.test(str); // true

上面代码中,正则模式不含g修饰符,每次都是从字符串头部开始匹配。所以,连续做了三次匹配,都返回true

1
2
3
4
5
6
var regex = /b/g;
var str = "abba";

regex.test(str); // true
regex.test(str); // true
regex.test(str); // false

上面代码中,正则模式含有g修饰符,每次都是从上一次匹配成功处,开始向后匹配。因为字符串abba只有两个b,所以前两次匹配结果为true,第三次匹配结果为false

(2)i 修饰符

默认情况下,正则对象区分字母的大小写,加上i修饰符以后表示忽略大小写(ignorecase)。

1
2
3
/abc/.test("ABC") / // false
abc /
i.test("ABC"); // true

上面代码表示,加了i修饰符以后,不考虑大小写,所以模式abc匹配字符串ABC

(3)m 修饰符

m修饰符表示多行模式(multiline),会修改^$的行为。默认情况下(即不加m修饰符时),^$匹配字符串的开始处和结尾处,加上m修饰符以后,^$还会匹配行首和行尾,即^$会识别换行符(\n)。

1
2
3
/world$/.test("hello world\n") / // false
world$ /
m.test("hello world\n"); // true

上面的代码中,字符串结尾处有一个换行符。如果不加m修饰符,匹配不成功,因为字符串的结尾不是world;加上以后,$可以匹配行尾。

1
/^b/m.test("a\nb"); // true

上面代码要求匹配行首的b,如果不加m修饰符,就相当于b只能处在字符串的开始处。加上b修饰符以后,换行符\n也会被认为是一行的开始。

组匹配

(1)概述

正则表达式的括号表示分组匹配,括号中的模式可以用来匹配分组的内容。

1
2
3
/fred+/.test("fredd") / // true
fred +
/.test('fredfred') / / true;

上面代码中,第一个模式没有括号,结果+只表示重复字母d,第二个模式有括号,结果+就表示匹配fred这个词。

下面是另外一个分组捕获的例子。

1
2
3
var m = "abcabc".match(/(.)b(.)/);
m;
// ['abc', 'a', 'c']

上面代码中,正则表达式/(.)b(.)/一共使用两个括号,第一个括号捕获a,第二个括号捕获c

注意,使用组匹配时,不宜同时使用g修饰符,否则match方法不会捕获分组的内容。

1
2
var m = "abcabc".match(/(.)b(.)/g);
m; // ['abc', 'abc']

上面代码使用带g修饰符的正则表达式,结果match方法只捕获了匹配整个表达式的部分。这时必须使用正则表达式的exec方法,配合循环,才能读到每一轮匹配的组捕获。

1
2
3
4
5
6
7
8
9
var str = "abcabc";
var reg = /(.)b(.)/g;
while (true) {
var result = reg.exec(str);
if (!result) break;
console.log(result);
}
// ["abc", "a", "c"]
// ["abc", "a", "c"]

正则表达式内部,还可以用\n引用括号匹配的内容,n是从 1 开始的自然数,表示对应顺序的括号。

1
2
/(.)b(.)\1b\2/.test("abcabc");
// true

上面的代码中,\1表示第一个括号匹配的内容(即a),\2表示第二个括号匹配的内容(即c)。

下面是另外一个例子。

1
/y(..)(.)\2\1/.test("yabccab"); // true

括号还可以嵌套。

1
/y((..)\2)\1/.test("yabababab"); // true

上面代码中,\1指向外层括号,\2指向内层括号。

组匹配非常有用,下面是一个匹配网页标签的例子。

1
2
3
4
var tagName = /<([^>]+)>[^<]*<\/\1>/;

tagName.exec("<b>bold</b>")[1];
// 'b'

上面代码中,圆括号匹配尖括号之中的标签,而\1就表示对应的闭合标签。

上面代码略加修改,就能捕获带有属性的标签。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var html = '<b class="hello">Hello</b><i>world</i>';
var tag = /<(\w+)([^>]*)>(.*?)<\/\1>/g;

var match = tag.exec(html);

match[1]; // "b"
match[2]; // " class="hello""
match[3]; // "Hello"

match = tag.exec(html);

match[1]; // "i"
match[2]; // ""
match[3]; // "world"

(2)非捕获组

(?:x)称为非捕获组(Non-capturing group),表示不返回该组匹配的内容,即匹配的结果中不计入这个括号。

非捕获组的作用请考虑这样一个场景,假定需要匹配foo或者foofoo,正则表达式就应该写成/(foo){1, 2}/,但是这样会占用一个组匹配。这时,就可以使用非捕获组,将正则表达式改为/(?:foo){1, 2}/,它的作用与前一个正则是一样的,但是不会单独输出括号内部的内容。

请看下面的例子。

1
2
var m = "abc".match(/(?:.)b(.)/);
m; // ["abc", "c"]

上面代码中的模式,一共使用了两个括号。其中第一个括号是非捕获组,所以最后返回的结果中没有第一个括号,只有第二个括号匹配的内容。

下面是用来分解网址的正则表达式。

1
2
3
4
5
6
7
8
9
10
11
// 正常匹配
var url = /(http|ftp):\/\/([^/\r\n]+)(\/[^\r\n]*)?/;

url.exec("http://google.com/");
// ["http://google.com/", "http", "google.com", "/"]

// 非捕获组匹配
var url = /(?:http|ftp):\/\/([^/\r\n]+)(\/[^\r\n]*)?/;

url.exec("http://google.com/");
// ["http://google.com/", "google.com", "/"]

上面的代码中,前一个正则表达式是正常匹配,第一个括号返回网络协议;后一个正则表达式是非捕获匹配,返回结果中不包括网络协议。

(3)先行断言

x(?=y)称为先行断言(Positive look-ahead),x只有在y前面才匹配,y不会被计入返回结果。比如,要匹配后面跟着百分号的数字,可以写成/\d+(?=%)/

“先行断言”中,括号里的部分是不会返回的。

1
2
var m = "abc".match(/b(?=c)/);
m; // ["b"]

上面的代码使用了先行断言,bc前面所以被匹配,但是括号对应的c不会被返回。

(4)先行否定断言

x(?!y)称为先行否定断言(Negative look-ahead),x只有不在y前面才匹配,y不会被计入返回结果。比如,要匹配后面跟的不是百分号的数字,就要写成/\d+(?!%)/

1
2
/\d+(?!\.)/.exec("3.14");
// ["14"]

上面代码中,正则表达式指定,只有不在小数点前面的数字才会被匹配,因此返回的结果就是14

“先行否定断言”中,括号里的部分是不会返回的。

1
2
var m = "abd".match(/b(?!c)/);
m; // ['b']

上面的代码使用了先行否定断言,b不在c前面所以被匹配,而且括号对应的d不会被返回。

参考链接

定义

对象是 JavaScript 语言最主要的数据类型,三种原始类型的值——数值、字符串、布尔值——在一定条件下,也会自动转为对象,也就是原始类型的“包装对象”。

所谓“包装对象”,就是分别与数值、字符串、布尔值相对应的NumberStringBoolean三个原生对象。这三个原生对象可以把原始类型的值变成(包装成)对象。

1
2
3
var v1 = new Number(123);
var v2 = new String("abc");
var v3 = new Boolean(true);

上面代码中,基于原始类型的值,生成了三个对应的包装对象。

1
2
3
4
5
6
7
typeof v1; // "object"
typeof v2; // "object"
typeof v3; // "object"

v1 === 123; // false
v2 === "abc"; // false
v3 === true; // false

包装对象的最大目的,首先是使得 JavaScript 的对象涵盖所有的值,其次使得原始类型的值可以方便地调用某些方法。

NumberStringBoolean如果不作为构造函数调用(即调用时不加new),常常用于将任意类型的值转为数值、字符串和布尔值。

1
2
3
Number(123); // 123
String("abc"); // "abc"
Boolean(true); // true

上面这种数据类型的转换,详见《数据类型转换》一节。

总结一下,这三个对象作为构造函数使用(带有new)时,可以将原始类型的值转为对象;作为普通函数使用时(不带有new),可以将任意类型的值,转为原始类型的值。

实例方法

包装对象的实例可以使用Object对象提供的原生方法,主要是valueOf方法和toString方法。

valueOf()

valueOf方法返回包装对象实例对应的原始类型的值。

1
2
3
new Number(123).valueOf(); // 123
new String("abc").valueOf(); // "abc"
new Boolean(true).valueOf(); // true

toString()

toString方法返回对应的字符串形式。

1
2
3
new Number(123).toString(); // "123"
new String("abc").toString(); // "abc"
new Boolean(true).toString(); // "true"

原始类型与实例对象的自动转换

原始类型的值,可以自动当作包装对象调用,即调用各种包装对象的属性和方法。这时,JavaScript 引擎会自动将原始类型的值转为包装对象实例,在使用后立刻销毁实例。

比如,字符串可以调用length属性,返回字符串的长度。

1
"abc".length; // 3

上面代码中,abc是一个字符串,本身不是对象,不能调用length属性。JavaScript 引擎自动将其转为包装对象,在这个对象上调用length属性。调用结束后,这个临时对象就会被销毁。这就叫原始类型与实例对象的自动转换。

1
2
3
4
5
6
7
8
9
var str = "abc";
str.length; // 3

// 等同于
var strObj = new String(str);
// String {
// 0: "a", 1: "b", 2: "c", length: 3, [[PrimitiveValue]]: "abc"
// }
strObj.length; // 3

上面代码中,字符串abc的包装对象提供了多个属性。

自动转换生成的包装对象是只读的,无法修改。所以,字符串无法添加新属性。

1
2
3
var s = "Hello World";
s.x = 123;
s.x; // undefined

上面代码为字符串s添加了一个x属性,结果无效,总是返回undefined

另一方面,调用结束后,包装对象实例会自动销毁。这意味着,下一次调用字符串的属性时,实际是调用一个新生成的对象,而不是上一次调用时生成的那个对象,所以取不到赋值在上一个对象的属性。如果要为字符串添加属性,只有在它的原型对象String.prototype上定义(参见《面向对象编程》章节)。

自定义方法

三种包装对象除了提供很多原生的实例方法(详见后文的介绍),还可以在原型上添加自定义方法和属性,供原始类型的值直接调用。

比如,我们可以新增一个double方法,使得字符串和数字翻倍。

1
2
3
4
5
6
7
8
9
10
11
12
13
String.prototype.double = function () {
return this.valueOf() + this.valueOf();
};

"abc".double();
// abcabc

Number.prototype.double = function () {
return this.valueOf() + this.valueOf();
};

(123).double();
// 246

上面代码在123外面必须要加上圆括号,否则后面的点运算符(.)会被解释成小数点。

但是,这种自定义方法和属性的机制,只能定义在包装对象的原型上,如果直接对原始类型的变量添加属性,则无效。

1
2
3
4
var s = "abc";

s.p = 123;
s.p; // undefined

上面代码直接对字符串abc添加属性,结果无效。主要原因是上面说的,这里的包装对象是自动生成的,赋值后自动销毁,所以最后一行实际上调用的是一个新的包装对象。

Boolean 对象

概述

Boolean对象是 JavaScript 的三个包装对象之一。作为构造函数,它主要用于生成布尔值的包装对象实例。

1
2
3
4
var b = new Boolean(true);

typeof b; // "object"
b.valueOf(); // true

上面代码的变量b是一个Boolean对象的实例,它的类型是对象,值为布尔值true

注意,false对应的包装对象实例,布尔运算结果也是true

1
2
3
4
5
6
7
if (new Boolean(false)) {
console.log("true");
} // true

if (new Boolean(false).valueOf()) {
console.log("true");
} // 无输出

上面代码的第一个例子之所以得到true,是因为false对应的包装对象实例是一个对象,进行逻辑运算时,被自动转化成布尔值true(因为所有对象对应的布尔值都是true)。而实例的valueOf方法,则返回实例对应的原始值,本例为false

Boolean 函数的类型转换作用

Boolean对象除了可以作为构造函数,还可以单独使用,将任意值转为布尔值。这时Boolean就是一个单纯的工具方法。

1
2
3
4
5
6
7
8
9
10
11
12
Boolean(undefined); // false
Boolean(null); // false
Boolean(0); // false
Boolean(""); // false
Boolean(NaN); // false

Boolean(1); // true
Boolean("false"); // true
Boolean([]); // true
Boolean({}); // true
Boolean(function () {}); // true
Boolean(/foo/); // true

上面代码中几种得到true的情况,都值得认真记住。

顺便提一下,使用双重的否运算符(!)也可以将任意值转为对应的布尔值。

1
2
3
4
5
6
7
8
9
10
11
!!undefined; // false
!!null; // false
!!0; // false
!!""; // false
!!NaN; // false
!!1; // true
!!"false"; // true
!![]; // true
!!{}; // true
!!function () {}; // true
!!/foo/; // true

最后,对于一些特殊值,Boolean对象前面加不加new,会得到完全相反的结果,必须小心。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if (Boolean(false)) {
console.log("true");
} // 无输出

if (new Boolean(false)) {
console.log("true");
} // true

if (Boolean(null)) {
console.log("true");
} // 无输出

if (new Boolean(null)) {
console.log("true");
} // true

在编写 CSS 时,有时可能会使用很长的选择器列表来定位具有相同样式规则的多个元素。

例如,如果您想对标题中的 <b> 标签进行颜色调整,我们应该都写过这样的代码:

1
2
3
4
5
6
7
8
h1 > b,
h2 > b,
h3 > b,
h4 > b,
h5 > b,
h6 > b {
color: hotpink;
}

现在,我们可以使用 :is() 缩减代码并提高其可读性:

1
2
3
:is(h1, h2, h3, h4, h5, h6) > b {
color: hotpink;
}

浏览器兼容性

:is():match() 改名而来,部分浏览器有其私有的 :any() 实现 :is() 的部分功能,在使用 :is() 之前,我们应该先了解其兼容性:

浏览器兼容性

:is() 和 :where()

:is() 作为一个伪类函数,其接收选择器列表作为参数,并选择该列表中任意一个选择器可以选择的元素
:where() 伪类函数与其功能相同,仅仅选择器权重不同,下面是几个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/* at the beginning */
:where(h1, h2, h3, h4, h5, h6) > b {
color: hotpink;
}

/* in the middle */
article :is(header, footer) > p {
color: gray;
}

/* at the end */
.dark-theme :where(button, a) {
color: rebeccapurple;
}

/* multiple */
:is(.dark-theme, .dim-theme) :where(button, a) {
color: rebeccapurple;
}

/* stacked */
:is(h1, h2):where(.hero, .subtitle) {
text-transform: uppercase;
}

/* nested */
.hero:is(h1, h2, :is(.header, .boldest)) {
font-weight: 900;
}

原生对象(15 种)

原生对象分为两类:原始类型(primitive type)和对象类型(object type)。原始类型又分为两类,一类是空值,一类是包装对象;对象类型也可以分为两类:一类是构造器对象,一类是单体内置对象

空值(2 种)

逻辑上,undefined 表示原始类型的空值,null 表示对象类型的空值

  • Undefined
  • Null

包装对象(3 种)

字符串 string、数字 number、布尔值 boolean 虽然属于原始类型。但是,由于其包装对象的性质,可以调用属性和方法

  • Number 数字类型
  • String 字符串类型
  • Boolean 布尔类型

构造器对象(9 种)

普通的对象是命名值的无序集合,但是通过不同的构造器,javascript 定义了功能各异的多种对象

[注意]如果显式地使用 new 构造器函数来定义包装对象,那么字符串 String、数字 number、布尔值 boolean 也属于构造器对象

单体内置对象(4 种)

不需声明或者使用构造器构造,直接在相应场景使用即可

内置对象

内置对象(built-in object),其定义为“由 ECMAScript 实现提供的、独立于宿主环境的所有对象,在 ECMAScript 程序开始执行时出现”。前半部分和原生对象很像,实际上内置对象也都是原生对象,区别在于后半句“在 ECMAScript 程序开始执行时出现”,这意味着开发者不必明确实例化内置对象,它在程序开始执行时已被实例化了,可以直接使用。 目前定义的内置对象只有两个:Global 和 Math。

内置对象是原生对象的一种,主要区别在于是否需要实例化。举个例子,在使用原生对象时,我们一般都需要 var obj = new Object()或字面量法 var obj = {}明确实例化生成一个实例再去调用 obj.toString()。但如果是内置对象 Global 和 Math,我们只需 Math.floor(2.5)直接调用,不需要再进行实例化。

宿主对象

宿主对象(host object),即由 ECMAScript 实现的宿主环境提供的对象。 那么什么是宿主环境呢?在 ECMAScript 中,“宿主”指的就是网页的运行环境,即“操作系统”和“浏览器”。比如浏览器提供的 BOM 和 DOM 都是宿主对象(对于不同的宿主环境所展示的内容不同),它不是 ECMAScript 官方提供的,而是浏览器这个宿主为了方便开发者而加上去的,所以实际上所有非原生对象都是宿主对象。

隐式类型转换

隐式类型转换指的是 JavaScript 在运行时自动将一个数据类型转换成另一个数据类型。例如,当你使用加号连接两个字符串时,JavaScript 会自动将它们转换成字符串并连接起来。虽然这看似方便,但它往往会引发一些问题,特别是对于没有经验的开发者来说。

1
2
let result = "3" + 4 + 5; // '345'
let result2 = 3 + 4 + "5"; // '75'

以上代码中,第一行输出结果是字符串 ‘345’,而第二行输出结果则是字符串 ‘75’。这是因为 JavaScript 在处理加号时会先把数字转换成字符串,然后再执行连接操作。因此,当你使用加号连接数字和字符串时,必须小心处理以避免意外的行为。

使用 var 声明变量

在 ES6 之前,JavaScript 中唯一声明变量的方法是使用 var 关键字。但是 var 存在一些作用域问题,容易造成变量污染等问题。因此,最好使用 let 或 const 来声明变量,它们都是块级作用域的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function ep() {
var foo = "bar";
if (true) {
var foo = "baz";
console.log(foo); // 'baz'
}
console.log(foo); // 'baz'
}
ep();

function ep2() {
let foo = "bar";
if (true) {
let foo = "baz";
console.log(foo); // 'baz'
}
console.log(foo); // 'bar'
}
ep2();

在上面的代码中:
第一个例子使用 var 声明变量时,内层的 foo 覆盖了外层的 foo,导致输出结果为两个都是字符串 ‘baz’。
而第二个例子使用 let 声明变量后,内层的 foo 只存在于内层作用域内,不会影响外层的 foo。

另外一个例子就是下面这个面试经常遇到的问题了:

1
2
3
4
5
6
for (var i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i);
}, 1000);
}
// 输出 5, 5, 5, 5, 5

这是因为 setTimeout 的回调函数是异步执行的,而当它们执行时,i 的值已经变成了 5,可以使用 let 或闭包解决这个问题。

频繁操作 DOM

直接操作 DOM 是一种非常低效的方式,因为这样做会导致页面反复重新渲染,从而降低性能和用户体验。相反,应该采用更高效的方式来操作 DOM,比如减少 DOM 查询次数、使用虚拟 DOM 技术等。

1
2
3
4
5
6
7
8
9
10
// 不推荐
document.getElementById("my-button").addEventListener("click", function () {
document.getElementById("my-text").innerText = "Hello, world!";
});

// 推荐
const myText = document.getElementById("my-text");
document.getElementById("my-button").addEventListener("click", function () {
myText.innerText = "Hello, world!";
});

在上面的代码中,第一个例子每次点击按钮时都会重新查询 DOM 并更新它,而第二个例子只查询一次 DOM,然后更新元素的文本内容。

修改原型对象

修改原型对象可能会导致一些意外的问题。例如:

1
2
3
4
5
6
7
8
Array.prototype.sum = function () {
return this.reduce(function (a, b) {
return a + b;
}, 0);
};

var arr = [1, 2, 3];
console.log(arr.sum()); // 6

虽然上述代码可以正常工作,但是它可能会在其他部分产生意外的影响。为了避免这个问题,我们应该尽量避免修改原型对象。

使用全局变量

在 JavaScript 中,全局变量是一种常见的变量类型,但它们可能会导致命名冲突和其他问题。因此,最好避免使用全局变量,并采用模块化方式来组织代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 不推荐
function foo() {
globalVar = "bar";
}

// 推荐
const myModule = (function () {
const localVar = "baz";

function foo() {
// do something with localVar
}

return {
foo: foo
};
})();

在上面的代码中,第一个例子使用了全局变量 globalVar,这样就有可能会被其他代码覆盖。而第二个例子使用了 IIFE(立即执行函数表达式)来创建一个模块,其中的变量只在该模块内部可见,不会影响到其他代码。

忽略分号

JavaScript 允许在语句结尾省略分号,但这往往会导致一些错误,特别是当代码压缩或合并时。因此,建议始终在语句结尾加上分号,以避免潜在的问题。

1
2
3
4
5
6
7
// 不推荐
let x = 1;
let y = 2;

// 推荐
let x = 1;
let y = 2;

在上面的代码中,第一个例子省略了分号,可能会导致一些意外行为。而第二个例子则明确地加上了分号,更加规范。

使用 for-in 循环

for-in 循环是一种遍历对象属性的方式,但它有一些缺陷。因为它不仅遍历对象自身的属性,还会遍历从原型链继承来的属性,这可能会导致一些意外的结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
const person = {
name: "Alice",
age: 30,
gender: "female"
};

// 不推荐
for (const key in person) {
console.log(person[key]);
}

// 推荐
Object.keys(person).forEach((key) => console.log(person[key]));

在上面的代码中,第一个例子使用了 for-in 循环遍历对象属性,而第二个例子则使用了 Object.keys() 方法来获取对象自身的属性名,并配合 forEach() 方法进行遍历。

比较 NaN

NaN 是一种特殊的数值类型,表示 Not-a-Number(非数字)。但是,由于 NaN 与任何值都不相等,因此在比较时需要使用 isNaN() 函数来判断。

1
2
3
4
5
6
7
8
9
// 不推荐
if (x == NaN) {
// do something
}

// 推荐
if (isNaN(x)) {
// do something
}

在上面的代码中,第一个例子错误地使用了相等运算符来比较 NaN,而第二个例子则使用了 isNaN() 函数进行判断。

使用 eval()

eval() 是一种执行字符串代码的方式,但它往往会导致安全问题和性能问题。因此,最好避免在生产环境中使用 eval()。

1
2
3
4
5
6
// 不推荐
eval("console.log('Hello, world!')");

// 推荐
const code = "console.log('Hello, world!')";
Function(code)();

在上面的代码中,第一个例子使用了 eval() 函数来执行字符串代码,而第二个例子则使用了 Function() 函数来创建一个新函数并执行它。

忽略错误处理

忽略错误处理是一个常见的错误,因为它可能会导致代码崩溃。例如:

1
2
3
try {
// some code
} catch (e) {}

尽管这个抛出异常的代码可能无关紧要,但是如果没有正确处理异常,就可能在开发或运行时期间导致问题。

为了避免这个问题,我们应该始终正确处理异常,例如通过日志记录或显示用户友好的错误信息。

将函数作为参数传递时忘记绑定 this

将函数作为参数传递时,需要注意绑定 this 的值。例如:

1
2
3
4
5
6
7
const obj = {
name: "Alice",
greet: function () {
console.log("Hello, " + this.name);
}
};
setTimeout(obj.greet, 1000); // Hello, undefined

正确的写法应该是使用 bind 方法:

1
2
3
4
5
6
7
const obj = {
name: "Alice",
greet: function () {
console.log("Hello, " + this.name);
}
};
setTimeout(obj.greet.bind(obj), 1000); // Hello, Alice

前言

递归函数简单的定义是:一个自调用函数,这意味着它将在执行的某个时刻调用自己

从理论上讲,递归是一种需要两个属性的行为:

  • 结束点:停止递归的情况
  • 一组规则:负责将所有的操作减少到结束点

如果没有结束点,递归将成为一个无限循环,但是如果一组规则就不能实现期望的行为,所以两者都存在才能使它正常工作。

场景

树形结构数据渲染页面

  • 渲染列表数据的时候,列表的子项还是列表,列表的层级多或者层级不确定
  • 在页面上显示文件和子文件但是子文件的数量不确定,同时也不确定子文件中是否还有子文件

举例:组织架构图,文件夹目录,导航菜单,评论列表分级展示

递归组件

在组件中内使用组件本身

代码展示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<template>
<div class="tree-container">
<div class="tree-item" v-for="item in treeData" :key="item.id" @click="itemClick(item)">
<div class="item-title">{{ item.name }}</div>
<Tree v-if="item.children && item.children.length" :treeData="item.children" @itemClick="$emit('itemClick', $event)"></Tree>
</div>
</div>
</template>

<script>
export default {
name: "Tree",
props: {
treeData: {
type: Array,
default: () => []
}
},
methods: {
itemClick(item) {
this.$emit("itemClick", item);
}
}
};
</script>

<style lang="scss" scoped>
.tree-container {
.tree-item {
.tree-item {
margin-left: 20px;
}
.item-title {
padding: 4px 8px;
}

/* 每个层级之间画线 */
&::before {
content: "";
display: block;
position: absolute;
top: 5px;
left: 10px;
height: calc(100% - 50px);
width: 1px;
background-color: #f7f8f9;
}
}
}
</style>

子组件说明

循环体

1
<div class="item-title">{{ item.name }}</div>

循环条件

item.children && item.children.length

获取节点数据

  1. 使用 $emit,将一级节点的 item 传递出去
  2. 将内层节点的数据传递出去,给组件里面的 Tree 绑定 @itemClick="$emit('itemClick', $event)"

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<template>
<Tree :tree-data="treeData"></Tree>
</template>

<script>
const treeData = [
{ id: 1, name: "一级1" },
{
id: 2,
name: "一级2",
children: [
{ id: 3, name: "二级2-1" },
{ id: 4, name: "二级2-2" }
]
},
{
id: 5,
name: "一级3",
children: [
{
id: 6,
name: "二级3-1",
children: [
{ id: 7, name: "三级3-1-1" },
{ id: 8, name: "三级3-1-2" }
]
},
{ id: 9, name: "二级3-2" },
{ id: 10, name: "二级3-3" }
]
}
];
export default {
data() {
return {
treeData
};
}
};
</script>

CSS 变量(CSS variable)又叫做”CSS 自定义属性”(CSS custom properties),可以通过 JS 动态改变。

  • var() 函数用于插入 CSS 变量的值。

  • CSS 变量可以访问 DOM,这意味着您可以创建具有局部或全局范围的变量,使用 JavaScript 来修改变量,以及基于媒体查询来修改变量。

  • 使用 CSS 变量的一种好方法涉及设计的颜色。您可以将它们放在变量中,而不必一遍又一遍地复制和粘贴相同的颜色。

var() 函数的语法

var() 函数用于插入 CSS 变量的值。

var() 函数的语法如下:

var(name, value)

描述
name必需。变量名(以两条破折号开头)。
value可选。回退值(在未找到变量时使用)。

注释:变量名称必须以两个破折号(–)开头,且区分大小写

如何工作

首先:CSS 变量可以有全局或局部作用域。

全局变量可以在整个文档中进行访问/使用,而局部变量只能在声明它的选择器内部使用。

  • 如需创建具有全局作用域的变量,请在 :root 选择器中声明它。 :root 选择器匹配文档的根元素

  • 如需创建具有局部作用域的变量,请在将要使用它的选择器中声明它。

优势

  • 使代码更易于阅读(更容易理解)

  • 使修改颜色值更加容易

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<div class="variable" :style="{ '--colorFu': colorFu, '--colorTop': colorTop }" />
</template>

<script>
export default {
data() {
return {
colorFu: "",
colorTop: ""
};
}
};
</script>

<style lang="scss" scoped>
.variable {
color: var(--colorFu);
background-color: var(--colorTop);
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
:root {
--blue: #1e90ff;
--white: #ffffff;
}

body {
background-color: var(--blue);
}

h2 {
border-bottom: 2px solid var(--blue);
}

.container {
color: var(--blue);
background-color: var(--white);
padding: 15px;
}

button {
background-color: var(--white);
color: var(--blue);
border: 1px solid var(--blue);
padding: 5px;
}

跨域是指在同一网站下,由于浏览器的安全机制,不能直接访问其他网站的数据。而前端开发中,由于业务的需要,经常需要和其他网站进行数据交互。因此,解决跨域问题成为了前端开发必备的技能。

本文将详细介绍跨域的八种实现方式,以及它们的使用场景、优缺点。

JSONP

JSONP 是一种利用 <script> 标签进行跨域请求的方式。它的原理是在请求地址中添加一个回调函数名,服务端在返回数据时会将数据作为参数传入该回调函数中,并返回给客户端。客户端则可以通过定义这个回调函数来处理返回的数据。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function jsonp(url, callback) {
const script = document.createElement("script");
script.src = `${url}&callback=${callback}`;
document.body.appendChild(script);
window[callback] = function (data) {
document.body.removeChild(script);
delete window[callback];
callback(data);
};
}

jsonp("http://example.com/data", function (data) {
console.log(data);
});

使用场景:

  • 只支持 GET 请求。
  • 能够支持跨域请求,但只能处理 JSON 数据。
  • 适用于数据量较小、不需要特别保密的场景。

优缺点:

  • 优点:实现简单、支持跨域请求。
  • 缺点:只能处理 JSON 数据、无法处理错误信息、存在安全风险。

CORS

CORS 是一种官方推荐的跨域解决方案。它的原理是在发送请求时,服务端在响应头中添加一个 Access-Control-Allow-Origin 字段,标识允许哪个源进行访问。客户端则可以通过设置 withCredentials 属性来携带认证信息,以及在响应头中获取对应的字段。

示例代码:

1
2
3
4
5
6
7
8
9
const xhr = new XMLHttpRequest();
xhr.open("GET", "http://example.com/data");
xhr.withCredentials = true;
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
console.log(xhr.responseText);
}
};
xhr.send();

使用场景:

  • 支持 GET、POST 等类型的请求。
  • 能够支持跨域请求,同时支持多种数据类型。
  • 适用于大部分的场景。

优缺点:

  • 优点:能够支持跨域请求、支持多种数据类型。
  • 缺点:需要服务端配合设置响应头、性能稍低。

WebSocket

WebSocket 是一种双向通信协议,它可以在浏览器和服务端之间建立一个持久化的连接,以实现实时通信。由于它是基于 HTTP 协议的,因此支持跨域请求。

示例代码:

1
2
3
4
const ws = new WebSocket("ws://example.com/socket");
ws.onmessage = function (event) {
console.log(event.data);
};

使用场景:

  • 可以实现双向通信、实时更新。
  • 适用于聊天室、游戏等实时性较高的场景。

优缺点:

  • 优点:能够实现双向通信、实时更新。
  • 缺点:需要服务端配合实现、只能处理文本数据、不稳定。

postMessage

postMessage 是 HTML5 中新增的一种跨窗口通信机制,它可以在不同窗口、甚至不同域名之间进行数据传递。它的原理是通过调用 window.postMessage() 方法,在目标窗口中触发一个 message 事件,从而实现数据的传递。

示例代码:

1
2
3
4
5
6
7
8
9
// 发送消息
window.parent.postMessage("hello", "http://example.com");

// 接收消息
window.addEventListener("message", function (event) {
if (event.origin === "http://example.com") {
console.log(event.data);
}
});

使用场景:

  • 可以进行不同窗口、不同域名之间的数据传递。
  • 适用于嵌套页面、多窗口通信等场景。

优缺点:

  • 优点:可以进行不同窗口、不同域名之间的数据传递。
  • 缺点:需要进行安全性检查、可能存在 XSS 攻击风险。

nginx 反向代理

通过配置 nginx 的反向代理,可以实现对目标地址的访问,并将响应返回给客户端。由于是在服务端进行请求,因此不会受到浏览器的同源策略限制。

示例代码:

1
2
3
location /api/ {
proxy_pass http://example.com/data/;
}

使用场景:

  • 用于解决前端直接访问跨域 API 的问题。
  • 适用于请求量较大、需要保密的场景。

优缺点:

  • 优点:能够支持跨域请求、保护了 API 的安全性。
  • 缺点:需要配置服务器,增加了服务器端的压力。

HTML5 XHR2

HTML5 中新增的 XHR2 对象,支持跨域请求,并且可以处理二进制数据和进度信息。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
const xhr = new XMLHttpRequest();
xhr.open("GET", "http://example.com/data");
xhr.responseType = "blob";
xhr.onload = function () {
const reader = new FileReader();
reader.onloadend = function () {
console.log(reader.result);
};
reader.readAsText(xhr.response);
};
xhr.send();

使用场景:

  • 支持跨域请求,能够处理二进制数据和进度信息。
  • 适用于需要上传、下载文件等场景。

优缺点:

  • 优点:支持跨域请求、能够处理二进制数据和进度信息。
  • 缺点:浏览器兼容性不一、需要服务端配合设置响应头。

document.domain

通过设置 document.domain 的值,可以实现不同子域名之间的跨域通信。但是该方式只适用于主域名相同、子域名不同的情况下。

示例代码:

1
2
3
4
5
// 在 A 页面中设置
document.domain = "example.com";

// 在 B 页面中设置
document.domain = "example.com";

使用场景:

  • 只适用于主域名相同、子域名不同的情况下。
  • 适用于不同子域名之间进行数据传递的场景。

优缺点:

  • 优点:实现简单、不需要浏览器支持。
  • 缺点:只适用于主域名相同、子域名不同的情况下。

iframe

通过在页面中插入一个 iframe,可以在其中加载目标页面,从而实现跨域通信。但是该方式存在一些安全问题,容易遭受 CSRF 攻击。

示例代码:

1
<iframe src="http://example.com"></iframe>

使用场景:

  • 适用于嵌套页面、跨域数据传输等场景。

优缺点:

  • 优点:能够实现跨域通信。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
function createPoster(val) {
let videoCanList = [],
curDateList = [];
new Promise((reslove, reject) => {
// 在缓存中创建video标签
let video = document.createElement("VIDEO");
// 通过setAttribute给video dom元素添加自动播放的属性,因为视频播放才能获取封面图
video.muted = true;
// 通过setAttribute给video dom元素添加自动播放的属性,因为视频播放才能获取封面图
video.autoplay = true;
//允许跨域访问
video.crossOrigin = "anonymous";
// 上面我们只是创建了video标签,视频播放需要内部的source的标签,scr为播放源
video.innerHTML = "<source src=" + val + ' type="audio/mp4">';
// 再创建canvas画布标签
let canvas = document.createElement("canvas");
let ctx = canvas.getContext("2d");
// video注册canplay自动播放事件
//视频的总长度
// video.currentTime = newVideo.duration / 2; // app报错
//视频的总长度
// video.currentTime = newVideo.duration / 2; // app报错
video.addEventListener(
"canplay",
(e) => {
// 创建画布的宽高属性节点,就是图片的大小,单位PX
let width = video.videoWidth;
let height = video.videoHeight;

//默认最大尺度的尺寸限制在(1920 * 1080)
const maxWidth = 1024,
maxHeight = 1024,
ratio = maxWidth / maxHeight;
//目标尺寸
let targetWidth = width,
targetHeight = height;
//当图片的宽度或者高度大于指定的最大宽度或者最大高度时,进行缩放图片
if (width > maxWidth || height > maxHeight) {
//超过最大宽高比例
if (width / height > ratio) {
//宽度取最大宽度值maxWidth,缩放高度
targetWidth = maxWidth;
targetHeight = Math.round(maxWidth * (height / width));
} else {
//高度取最大高度值maxHeight,缩放宽度
targetHeight = maxHeight;
targetWidth = Math.round(maxHeight * (width / height));
}
}

var anw = document.createAttribute("width");
anw.nodeValue = targetWidth;
var anh = document.createAttribute("height");
anh.nodeValue = targetHeight;
canvas.setAttributeNode(anw);
canvas.setAttributeNode(anh);
// 画布渲染
ctx.drawImage(video, 0, 0, targetWidth, targetHeight);
// 生成图片
var base64 = canvas.toDataURL("image/png", 0.8); // 这就是封面图片的base64编码
// 视频结束播放的事件
video.pause();

reslove(base64); // promise函数成功的回调
},
false
);
});
}