进制转换

可以使用toString(a),进行进制转换,a填需要转换的进制

let a = 10
a.toString(2) // '1010'
a.toString(16) // 'a'

字符串新增方法

  1. includes():是否找到了参数字符串。
  2. startsWith():参数字符串是否在原字符串的头部。
  3. endsWith():参数字符串是否在原字符串的尾部。
  4. repeat(n):将原字符串重复n次。会先将n取整,若仍为负数,则报错Infinity;若n为字符串,则转换成数字。
  5. padStart():用于头部补全。
  6. padEnd():用于尾部补全。
  7. trimStart()、trimLeft():消除字符串头部的空格。
  8. trimEnd()、trimRight():消除尾部的空格。
  9. matchAll():返回一个正则表达式在当前字符串的所有匹配,返回的是一个遍历器(Iterator),需要使用for…of取出,也可以使用结解构将其转成数组[...string.matchAll(regex)]
  10. replaceAll():可以一次性替换所有匹配,searchValue必须是全局的正则表达式。
  11. at():返回参数指定位置的字符。

padStart用途:

  1. 用于尾部补全
'1'.padStart(10, '0') // "0000000001"
'12'.padStart(10, '0') // "0000000012"
'123456'.padStart(10, '0') // "0000123456"
  1. 提示字符串格式
'12'.padStart(10, 'YYYY-MM-DD') // "YYYY-MM-12"
'09-12'.padStart(10, 'YYYY-MM-DD') // "YYYY-09-12"

正则表达式

符号 含义
. 匹配任何字符
* 匹配前一个表达式零次或多次
+ 匹配前一个表达式一次或多次
? 匹配前一个表达式零次或一次
^ 匹配输入字符串的开始位置
$ 匹配输入字符串的结束位置
\d 匹配数字字符
\D 匹配非数字字符
\w 匹配字母、数字或下划线字符
\W 匹配非字母、数字或下划线字符
\s 匹配空白字符
\S 匹配非空白字符
[] 匹配方括号中的任意一个字符
() 分组,用于限制操作符的作用范围、改变操作符的优先级、捕获匹配项等。

1. RegExp 构造函数

// 第一种情况
var regex = new RegExp('xyz', 'i');
// 等价于
var regex = /xyz/i;

// 第二种情况
var regex = new RegExp('xyz', 'i');
// 等价于
var regex = /xyz/i;

2. 正则表达式方法

  1. match()
  2. replace()
  3. search()
  4. split()

3. u 修饰符

用来正确处理大于\uFFFF的 Unicode 字符

/^\uD83D/u.test('\uD83D\uDC2A') // false
/^\uD83D/.test('\uD83D\uDC2A') // true

4. y 修饰符

exec方法

var s = 'aaa_aa_a';
var r1 = /a+/g;
var r2 = /a+/y;

r1.exec(s) // ["aaa"]
r2.exec(s) // ["aaa"]

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

y修饰符和g修饰符类似,都是从上一次搜索到的位置开始匹配字符,但是y修饰符不一样,y修饰符要求在字符串的头部就匹配到数据,否则返回空。

代码分析:执行第一次匹配后,都是输出aaa,剩余字符串为_aa_a;第二次匹配,都是从第一次匹配剩余的字符串的_开始,g修饰符会搜索剩余字符串所有符合的结果,而y修饰符会从头开始匹配,如果不合适(即_a不匹配),则直接返回null

实际上,y修饰符号隐含了头部匹配的标志^

match方法

'a1a2a3'.match(/a\d/y) // ["a1"]
'a1a2a3'.match(/a\d/gy) // ["a1", "a2", "a3"]

代码分析:匹配到第一次字符a1,将匹配结果返回,进行匹配下一个字符,由于y修饰符限制,正则表达式只能从上次成功匹配的字符a之后开始匹配,也就是a后面的1开始匹配,由于头部匹配不合适,则返回null。

5. s 修饰符

点(.)是一个特殊字符,代表任意的单个字符,但是有两个例外:一个是四个字节的 UTF-16 字符,这个可以用u修饰符解决;另一个是行终止符(line terminator character)。

行终止符:

  • U+000A 换行符(\n
  • U+000D 回车符(\r
  • U+2028 行分隔符(line separator)
  • U+2029 段分隔符(paragraph separator)
/foo.bar/s.test('foo\nbar') // true

6. 断言

先行断言(lookahead)x只有在y前面才匹配,必须写成/x(?=y)/

先行否定断言(negative lookahead)x只有在y前面才匹配,必须写成/x(?=y)/

/\d+(?=%)/.exec('100% of US presidents have been male')  // ["100"]
/\d+(?!%)/.exec('that’s all 44 of them') // ["44"]

后行断言(lookbehind)x只有在y后面才匹配,必须写成/(?<=y)x/

后行否定断言(negative lookbehind)x只有在y后面才匹配,必须写成/(?<=y)x/

/\d+(?=%)/.exec('100% of US presidents have been male')  // ["100"]
/\d+(?!%)/.exec('that’s all 44 of them') // ["44"]

7. Unicode 属性类

匹配满足条件的所有字符

const regexGreekSymbol = /\p{Script=Greek}/u;
regexGreekSymbol.test('π') // true

\p{Script=Greek}表示匹配一个希腊文字母,所以匹配π成功。

8. v 修饰符

可以向某个 Unicode 属性类添加或减少字符

// 差集运算(A 减去 B)
[A--B]

// 交集运算(A 与 B 的交集)
[A&&B]
// 十进制字符去除 ASCII 码的0到9
[\p{Decimal_Number}--[0-9]]

// Emoji 字符去除 ASCII 码字符
[\p{Emoji}--\p{ASCII}]

9. 具名组匹配

const RE_DATE = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;

const matchObj = RE_DATE.exec('1999-12-31');
const year = matchObj.groups.year; // "1999"
const month = matchObj.groups.month; // "12"
const day = matchObj.groups.day; // "31"

“具名组匹配”在圆括号内部,模式的头部添加“问号 + 尖括号 + 组名“(?<year>),然后就可以在exec方法返回结果的groups属性上引用该组名。同时,数字序号(matchObj[1])依然有效。

解构赋值和替换

let {groups: {one, two}} = /^(?<one>.*):(?<two>.*)$/u.exec('foo:bar');
one // foo
two // bar
let re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u;

'2015-01-02'.replace(re, '$<day>/$<month>/$<year>')
// '02/01/2015'

引用

如果要在正则表达式内部引用某个“具名组匹配”,可以使用\k<组名>的写法。

const RE_TWICE = /^(?<word>[a-z]+)!\k<word>$/;
RE_TWICE.test('abc!abc') // true
RE_TWICE.test('abc!ab') // false

数字引用(\1)依然有效。

const RE_TWICE = /^(?<word>[a-z]+)!\1$/;
RE_TWICE.test('abc!abc') // true
RE_TWICE.test('abc!ab') // false

10. d 修饰符

这个修饰符可以让exec()match()的返回结果添加indices属性,在该属性上面可以拿到匹配的开始位置和结束位置。

const text = 'zabbcdef';
const re = /ab/d;
const result = re.exec(text);

result.index // 1
result.indices // [ [1, 3] ]

结束位置是匹配结果的下一个字符

数值

1. 数值分隔符

类似1000可以写作1,000

let budget = 1_000_000_000_000;
budget === 10 ** 12 // true

数值分隔符没有指定间隔的位数,可以每三位添加一个分隔符,也可以每一位、每两位、每四位添加一个。其他进制的数值也可以使用分隔符。数值分隔符只是一种书写便利,对于 JavaScript 内部数值的存储和输出,并没有影响。

数值分隔符有几个使用注意点:

  • 不能放在数值的最前面(leading)或最后面(trailing)。
  • 不能两个或两个以上的分隔符连在一起。
  • 小数点的前后不能有分隔符。
  • 科学计数法里面,表示指数的eE前后不能有分隔符。
  • 分隔符不能紧跟着进制的前缀0b0B0o0O0x0X

下面三个将字符串转成数值的函数,不支持数值分隔符。

  • Number()
  • parseInt()
  • parseFloat()
Number('123_456') // NaN
parseInt('123_456') // 123

2. Number

Number.isFinite()用来检查一个数值是否为有限的(finite),不是数值直接返回false。

Number.isFinite(15); // true
Number.isFinite(0.8); // true
Number.isFinite(NaN); // false
Number.isFinite(Infinity); // false
Number.isFinite(-Infinity); // false
Number.isFinite('foo'); // false
Number.isFinite('15'); // false
Number.isFinite(true); // false

Number.isNaN()用来检查一个值是否为NaN

Number的这两个方法和直接调用这两个方法的区别

isFinite会将非数值转为数值,在进行判断,而Number的isFinite会直接判断数值,如果不为数值,则直接返回false。

isFinite(25) // true
isFinite("25") // true
Number.isFinite(25) // true
Number.isFinite("25") // false

isNaN(NaN) // true
isNaN("NaN") // true
Number.isNaN(NaN) // true
Number.isNaN("NaN") // false
Number.isNaN(1) // false

Number.isInteger()用来判断一个数值是否为整数。

整数和浮点数采用的是同样的储存方法,所以 25 和 25.0 被视为同一个值。如果参数不是数值,则直接返回false

Number.isInteger(25) // true
Number.isInteger(25.0) // true

如果数值超过精度会将后面位舍弃,可能会出现误判;或者一个数的绝对值小于Number.MIN_VALUE(5E-324),即最小分辨值,会自动转为0,这是也会误判。

Number.isInteger(3.0000000000000002) // true

Number.isInteger(5E-324) // false
Number.isInteger(5E-325) // true 5E-325由于值太小,会被自动转为0

Number.EPSILON表示 1 与大于 1 的最小浮点数之间的差。等于 2 的 -52 次方。

Number.EPSILON === Math.pow(2, -52)
// true
Number.EPSILON
// 2.220446049250313e-16
Number.EPSILON.toFixed(20)
// "0.00000000000000022204"

误差范围设为 2 的-50 次方(即Number.EPSILON * Math.pow(2, 2)),即如果两个浮点数的差小于这个值,我们就认为这两个浮点数相等。

/**
* 判断0.1+0.2是否等于0.3
*/
0.1 + 0.2 === 0.3 // false

0.1 + 0.2 - 0.3
// 5.551115123125783e-17

5.551115123125783e-17 < Number.EPSILON * Math.pow(2, 2)
// true

// 判断两个浮点数是否相等
function withinErrorMargin (left, right) {
return Math.abs(left - right) < Number.EPSILON * Math.pow(2, 2);
}

Number.isSafeInteger()用来判断一个整数是否在这个范围之内,JavaScript 能够准确表示的整数范围在-2^532^53之间。

大整数计算时,只检验计算结果不行,可能会出现错误,如果a远比b大,进行计算时,a会以最大上限整数进行存储,而不是a的真实值。

Number.isSafeInteger(9007199254740993)
// false
Number.isSafeInteger(990)
// true
Number.isSafeInteger(9007199254740993 - 990)
// true
9007199254740993 - 990
// 返回结果 9007199254740002
// 正确答案应该是 9007199254740003

3. Math

Math.trunc方法用于去除一个数的小数部分,返回整数部分,不会进行四舍五入。

Math.sign方法用来判断一个数到底是正数、负数、还是零。

  • 参数为正数,返回+1
  • 参数为负数,返回-1
  • 参数为 0,返回0
  • 参数为-0,返回-0;
  • 其他值,返回NaN

Math.cbrt()方法用于计算一个数的立方根。

Math.clz32()方法将参数转为 32 位无符号整数的形式,然后返回这个 32 位值里面有多少个前导 0。左移运算符(<<)与Math.clz32方法直接相关。对于小数,Math.clz32方法只考虑整数部分。

Math.imul方法返回两个数以 32 位带符号整数形式相乘的结果,返回的也是一个 32 位的带符号整数。主要用于大整数计算。

Math.fround方法返回一个数的32位单精度浮点数形式。

Math.hypot方法返回所有参数的平方和的平方根。

Math.hypot(3, 4);        // 5
// 3 的平方加上 4 的平方,等于 5 的平方。

Math.expm1(x)返回e的x次方-1
$$
e^x-1
$$
Math.log1p(x)方法返回1 + x的自然对数
$$
ln(1+x)
$$
Math.log1p(x)方法返回1 + x的自然对数
$$
log_{10}(x)
$$
Math.log2(x)返回以 2 为底的x的对数。
$$
log_2(x)
$$

  • Math.sinh(x) 返回x的双曲正弦(hyperbolic sine)
  • Math.cosh(x) 返回x的双曲余弦(hyperbolic cosine)
  • Math.tanh(x) 返回x的双曲正切(hyperbolic tangent)
  • Math.asinh(x) 返回x的反双曲正弦(inverse hyperbolic sine)
  • Math.acosh(x) 返回x的反双曲余弦(inverse hyperbolic cosine)
  • Math.atanh(x) 返回x的反双曲正切(inverse hyperbolic tangent)

4. BigInt

大整数,BigInt 类型的数据必须添加后缀n

除法运算会舍弃小数位;

BigInt不能与普通数值进行混合运算,|+

比较运算符可以与其他类型值混合计算;

BigInt 与字符串混合运算时,会先转为字符串,再进行运算。

函数

1. 基本用法

参数默认值不是传值的,而是每次都重新计算默认值表达式的值。

let x = 99;
function foo(p = x + 1) {
console.log(p);
}

foo() // 100

x = 100;
foo() // 101

2. 与参数解构赋值结合

function foo({x, y = 5} = {}) {
console.log(x, y);
}

foo() // undefined 5

3. 参数默认位置

function foo(x = 5, y = 6) {
console.log(x, y);
}

foo(undefined, null)
// 5 null

undefined触发默认值,null不会

4. 函数的length属性

指定了默认值以后,函数的length属性,将返回没有指定默认值的参数个数,rest参数也不在length中。

如果设置了默认值的参数不是尾参数,那么length属性也不再计入后面的参数了。

(function (a = 0, b, c) {}).length // 0
(function (a, b = 1, c) {}).length // 1
(function (...a) {}).length // 0

5. 作用域

let x = 1;

function f(y = x) {
let x = 2;
console.log(y);
}

f() // 1

函数f调用时,参数y = x形成一个单独的作用域,并没有定义变量x,所以从全局变量找x

var x = 1;
function foo(x, y = function() { x = 2; }) {
var x = 3;
y();
console.log(x);
}

foo() // 3
x // 1

执行y()的时候,y()和函数参数的x在同一作用域内,所以函数的参数x的值被改为2;函数体内使用var声明了一个和外部同名的变量x,并且赋值为3,这个变量只是一个局部变量,虽然和外面的全局变量同名但是本质不同,所以外部的x没有变,仍为1,所以foo最后一行代码输出的为3。

刚开始:

全局作用域:

变量
x 1

foo函数的参数作用域(当前作用域由于y参数没有执行,所以x参数仍为undefined):

变量
x undefined
y function ( ) { x = 2; }

执行foo()函数:

全局作用域:

变量
x 1

foo函数内部作用域:

变量
x 3

foo函数的参数作用域(此时已经执行y()函数,所以x的值从undefined变为2):

变量
x 2
y function ( ) { x = 2; }

6. rest 参数

rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中。 rest 参数的形式为...变量名

function add(...values) {
let sum = 0;

for (var val of values) {
sum += val;
}

return sum;
}

add(2, 5, 3) // 10

arguments对象,是一个类似数组的对象,所以需要使用Array.from将其转为数组

function add(...values) {
let sum = 0;

for (var val of values) {
sum += val;
}

return sum;
}

add(2, 5, 3) // 10

注意,rest 参数之后不能再有其他参数(即只能是最后一个参数),否则会报错。

7. 严格模式

只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式,否则会报错。

原因是:函数执行的时候,先执行函数参数,然后再执行函数体,这样不合理。

严格模式下不能用前缀0表示八进制

解决方法(把函数包在一个无参数的立即执行函数里面):

const doSomething = (function () {
'use strict';
return function(value = 42) {
return value;
};
}());

8. name 属性

函数的name属性,返回该函数的函数名。

9. 箭头函数

var f = v => v;

// 等同于
var f = function (v) {
return v;
};

如果箭头函数只有一行语句,且不需要返回值,可以采用下面的写法,就不用写大括号了。

let fn = () => void doesNotReturn();

注意:

  1. 箭头函数没有自己的this对象,使用箭头函数的this指向上一层的作用域。
  2. 不可以当作构造函数,也就是说,不可以对箭头函数使用new命令,否则会抛出一个错误。
  3. 不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
  4. 不可以使用yield命令,因此箭头函数不能用作 Generator 函数。
  5. 箭头函数也没有argumentssupernew.target变量,都指向外层的变量
  6. 箭头函数不能使用call()apply()bind()这些方法
(function() {
return [
(() => this.x).bind({ x: 'inner' })()
];
}).call({ x: 'outer' });
// ['outer']

部署管道机制(pipeline)的例子

const pipeline = (...funcs) =>
val => funcs.reduce((a, b) => b(a), val);

const plus1 = a => a + 1;
const mult2 = a => a * 2;
const addThenMult = pipeline(plus1, mult2);

addThenMult(5)
// 12

10. 尾调用

尾调用(Tail Call)是函数式编程的一个重要概念,指某个函数的最后一步是调用另一个函数。

以下三种情况,都不属于尾调用。

// 情况一
function f(x){
let y = g(x);
return y;
}

// 情况二
function f(x){
return g(x) + 1;
}

// 情况三
function f(x){
g(x);
}

ES6 的尾调用优化只在严格模式下开启,正常模式是无效的。

在正常模式下,函数内部有两个变量,可以跟踪函数的调用栈。尾调用优化发生时,函数的调用栈会改写,因此上面两个变量就会失真。严格模式禁用这两个变量,所以尾调用模式仅在严格模式下生效。

  • func.arguments:返回调用时函数的参数。
  • func.caller:返回调用当前函数的那个函数。

尾递归

尾递归优化过的 Fibonacci 数列实现如下。

// 原函数
function Fibonacci (n) {
if ( n <= 1 ) {return 1};

return Fibonacci(n - 1) + Fibonacci(n - 2);
}

Fibonacci(10) // 89
Fibonacci(100) // 超时
Fibonacci(500) // 超时

// 尾递归
function Fibonacci2 (n , ac1 = 1 , ac2 = 1) {
if( n <= 1 ) {return ac2};

return Fibonacci2 (n - 1, ac2, ac1 + ac2);
}

Fibonacci2(100) // 573147844013817200000
Fibonacci2(1000) // 7.0330367711422765e+208
Fibonacci2(10000) // Infinity

尾递归优化

function sum(x, y) {
if (y > 0) {
return sum(x + 1, y - 1);
} else {
return x;
}
}

sum(1, 100000)
// Uncaught RangeError: Maximum call stack size exceeded(…)

报错,提示超出调用栈的最大次数

蹦床函数(trampoline)可以将递归执行转为循环执行。

function sum(x, y) {
if (y > 0) {
return sum.bind(null, x + 1, y - 1);
} else {
return x;
}
}

这里是返回一个函数,然后执行该函数,而不是函数里面调用函数,这样就避免了递归执行,从而就消除了调用栈过大的问题。

蹦床函数并不是真正的尾递归优化,下面的实现才是。

function tco(f) {
var value;
var active = false;
var accumulated = [];

return function accumulator() {
accumulated.push(arguments);
if (!active) {
active = true;
while (accumulated.length) {
value = f.apply(this, accumulated.shift());
}
active = false;
return value;
}
};
}

var sum = tco(function(x, y) {
if (y > 0) {
return sum(x + 1, y - 1)
}
else {
return x
}
});

sum(1, 100000)
// 100001

分析:待分析

11. 函数参数的尾逗号

ES2017 允许函数的最后一个参数有尾逗号(trailing comma)。

function clownsEverywhere(
param1,
param2,
) { /* ... */ }

clownsEverywhere(
'foo',
'bar',
);

12. Function.prototype.toString()

返回函数一模一样的原始代码。

function /* foo comment */ foo () {}

foo.toString()
// "function /* foo comment */ foo () {}"

13. catch 命令的参数省略

使用try...catch的时候catch后面可以不接参数,但是捕获不到异常

try {
// ...
} catch {
// ...
}

数组

实例

eg1:直接使用Math.max()求最大值,参数为数组时可以使用...解构

// ES5 的写法
Math.max.apply(null, [14, 3, 77])

// ES6 的写法
Math.max(...[14, 3, 77])

// 等同于
Math.max(14, 3, 77);

eg2:push()函数,将一个数组添加到另一个数组的尾部

ES5之前由于push()方法的参数不能是数组,所以只好通过apply()方法变通使用push()方法。

// ES5 的写法
var arr1 = [0, 1, 2];
var arr2 = [3, 4, 5];
Array.prototype.push.apply(arr1, arr2);

// ES6 的写法
let arr1 = [0, 1, 2];
let arr2 = [3, 4, 5];
arr1.push(...arr2);

eg3:创建时间

let date = new Date(...[2023, 6, 20, 17, 40, 59])
date.toLocaleString()
// 2023/7/20 17:40:59

eg4:复制数组

// ES5 的写法
const a1 = [1, 2];
const a2 = a1.concat();

a2[0] = 2;
a1 // [1, 2]

// ES6
const a1 = [1, 2];
// 写法一
const a2 = [...a1];
// 写法二
const [...a2] = a1;

eg5:合并数组

这两种方法属于浅拷贝

const arr1 = ['a', 'b'];
const arr2 = ['c'];
const arr3 = ['d', 'e'];

// ES5 的合并数组
arr1.concat(arr2, arr3);
// [ 'a', 'b', 'c', 'd', 'e' ]

// ES6 的合并数组
[...arr1, ...arr2, ...arr3]
// [ 'a', 'b', 'c', 'd', 'e' ]

es6:与解构赋值结合

扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报错。

// ES5
a = list[0], rest = list.slice(1)

// ES6
[a, ...rest] = list

1. 扩展运算符

只有函数调用时,扩展运算符才可以放在圆括号中,否则会报错。

(...[1, 2])
// Uncaught SyntaxError: Unexpected number

console.log((...[1, 2]))
// Uncaught SyntaxError: Unexpected number

console.log(...[1, 2])
// 1 2

2. 替代函数的 apply() 方法

不再需要apply()方法将数组转为函数的参数

// ES5 的写法
function f(x, y, z) {
// ...
}
var args = [0, 1, 2];
f.apply(null, args);

// ES6 的写法
function f(x, y, z) {
// ...
}
let args = [0, 1, 2];
f(...args);

3. 字符串

扩展运算符还可以将字符串转为真正的数组。

能够正确识别四个字节的 Unicode 字符。

[...'hello']
// [ "h", "e", "l", "l", "o" ]

'x\uD83D\uDE80y' // x🚀y
'x\uD83D\uDE80y'.length // 4
[...'x\uD83D\uDE80y'].length // 3

4. 类数组的对象

类数组的对象也可以通过扩展运算符得到数组

let arrObj = {

}

脚本技巧

获取子节点

使用document.getElementsByClassName().childNodes可以获取子节点的列表

例:全选复选框

let divs = document.getElementsByClassName('board-item')
for (let div of divs){
div.childNodes[0].checked = true
}

// div.childNodes返回的结果:NodeList(2) [input, span]