【前端学习笔记】ES6以及代码编写注意事项

let、var和const命令

let只在所在的代码块有效,不允许重复声明,且只能在声明后使用。for循环可以使用let。

var a = []
for(let i = 0; i < 5; i++) {
	a[i] = function() {
		console.log(i)
	}
}
a[3]() // 3
i // i is not defined
var a = []
for(var i = 0; i < 5; i++) {
	a[i] = function() {
		console.log(i)
	}
}
a[3]() // 5
i // 5

const声明一个只读的常量,一旦声明,常量的值就不能改变。只在声明所在的块级作用域内有效。const是使指向的那个内存地址不得改动。对于复合类型,变量指向的是内存地址,指向的数据结构是可以被修改的。

const obj = {}
foo.name = 'test'
foo.name // test

obj = {} // 再次赋值会报错。

变量的解构赋值

数组的结构赋值

只要等号两边的模式相同,左边的变量就会被赋予对应的值。

let [a, b, c] = [1, 2, 3]
a // 1
b // 2
c // 3
let [a, b, c] = [1, [2, 3]]
a // 1
b // [2, 3]
c // undefined

结构赋值还可以指定默认值。当指定了默认值后,数组中对应的位置===undefined时,默认值才会生效。

let [a, b = '2', c = '3'] = [1, undefined, NaN]
a // 1
b // 2
c // NaN

也可以使用函数设置默认值,函数会在对应位置为undefined时才会执行。

function func() {
	return 'func'
}
let [a = func(), b = func()] = [1]
a // 1
b // func

对象的解析赋值

数组的元素时按次序排列的,变量的取值由它的位置决定。对象的属性没有次序,变量必须和属性同名,才能取到正确的值。

let { a, b, c} = { a: '1', 'b': '2', d: '3' }
a // 1
b // 2
c // undefined

如果变量名与属性名不一致,必须写成下面这样。

let { d:c } = { a: '1', 'b': '2', d: '3' }
c // 3
d // d is not defined

解析赋值的内部机制是先找到同名属性,然后再给冒号后面的变量赋值。与数组一样,对象的解析赋值也可以设置默认值。

let { a, b, c = 3} = { a: '1', 'b': '2', d: '3' }
a // 1
b // 2
c // 3

数值和布尔值的解构赋值

解构赋值时,如果等号右边是数值和布尔值,则会先转为对象。

let {toString: s} = 123;
s === Number.prototype.toString // true
let {toString: s} = true;
s === Boolean.prototype.toString // true

上面代码中,数值和布尔值的包装对象都有 toString 属性,因此变量 s 都能取到值。
解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其转为对象。由于 undefined 和 null 无法转为对象,所以对它们进行解构赋值,都会报
错。
let { prop: x } = undefined; // TypeError
let { prop: y } = null; // TypeError

函数参数的解构赋值

[[1, 2], [3, 4]].map(([a, b]) => a + b);
// [ 3, 7 ]

[1, undefined, 3].map((x = 'yes') => x);
// [ 1, 'yes', 3 ]

用途

交换变量的值

let [x, y] = [1, 2]
[x, y] = [y , x]
x // 2
y // 1

从函数返回多个值

function func() {
	return {
		name: 'test',
		age: 12
		sort: 1
	}
}
let { name, age } = func();

函数参数的定义

解构赋值可以方便地将一组参数与变量名对应起来。

// 参数是一组有次序的值
function f([x, y, z]) { ... }
f([1, 2, 3]);
// 参数是一组无次序的值
function f({x, y, z}) { ... }
f({z: 3, y: 2, x: 1});

提取 JSON 数据

提取 JSON 对象中的数据

let jsonData = {
 id: 42,
 status: "OK",
 data: [867, 5309]
};
let { id, status, data: number } = jsonData;
console.log(id, status, number);
// 42, "OK", [867, 5309]

遍历 Map 结构

const map = new Map(Object.entries({ a: 1, b: 2 }))
// 获取键名
for (let [key] of map) {
 // ...
}
// 获取键值
for (let [,value] of map) {
 // ...
}

输入模块的指定方法

加载模块时,往往需要指定输入哪些方法。解构赋值使得输入语句非常清晰。

const { SourceMapConsumer, SourceNode } = require("source-map");

字符串的扩展

字符的Unicode表示法

JavaScript 允许采用 \uxxxx 形式表示一个字符,其中 xxxx 表示字符的 Unicode 码点。但是,这种表示法只限于码点在 \u0000 ~ \uFFFF 之间的字符。超出这个范围的字符,必须用两个双字节的形式表示。ES6 对这一点做出了改进,只要将码点放入大括号,就能正确解读该字符。

"\u{41}\u{42}\u{43}"
// "ABC"
"\u{6F}"
// o
let hello = 123;
hell\u{6F} // 123
'\u{1F680}' === '\uD83D\uDE80'
// true

codePointAt()

JavaScript 内部,字符以 UTF-16 的格式储存,每个字符固定为 2 个字节。对于那些需要 4 个字节储存的字符(Unicode 码点大于 0xFFFF 的字符),JavaScript 会认为它们是两个字符。ES6 提供了 codePointAt 方法,能够正确处理 4 个字节储存的字符,返回一个字符的码点。

let s = '𠮷a';
s.codePointAt(0) // 134071
s.codePointAt(1) // 57271
s.codePointAt(2) // 97
// 判断字符是否是四个字节组成的
function is32Bit(c) {
	return c.codePointAt(0) > 0xFFFF;
}

String.fromCodePoint()

ES5 提供 String.fromCharCode 方法,用于从码点返回对应字符,但是这个方法不能识别 32 位的 UTF-16 字符(Unicode 编号大于 0xFFFF )。

String.fromCharCode(0x20BB7)
// "ஷ"

上面代码中, String.fromCharCode 不能识别大于 0xFFFF 的码点,所以 0x20BB7 就发生了溢出,最高位 2 被舍弃了,最后返回码点 U+0BB7 对应的字符,而不是码点 U+20BB7 对应的字符。ES6 提供了 String.fromCodePoint 方法,可以识别大于 0xFFFF 的字符,弥补了 String.fromCharCode 方法的不足。在作用上,正好与 codePointAt 方
法相反。

String.fromCodePoint(0x20BB7)
// "𠮷"
String.fromCodePoint(0x78, 0x1f680, 0x79) === 'x\uD83D\uDE80y'
// true

上面代码中,如果 String.fromCodePoint 方法有多个参数,则它们会被合并成一个字符串返回。
注意, fromCodePoint 方法定义在 String 对象上,而 codePointAt 方法定义在字符串的实例对象上。

字符串的遍历器接口

可以使用for…of遍历字符串

let text = String.fromCodePoint(020BB7)
for(let i of text) {
	console.log(i)
}
// 𠮷

at()

ES5 对字符串对象提供 charAt 方法,返回字符串给定位置的字符。该方法不能识别码点大于 0xFFFF 的字符。

'abc'.charAt(0) // "a"
'𠮷'.charAt(0) // "\uD842"

上面代码中, charAt 方法返回的是 UTF-16 编码的第一个字节,实际上是无法显示的。
目前,有一个提案,提出字符串实例的 at 方法,可以识别 Unicode 编号大于 0xFFFF 的字符,返回正确的字符。

'abc'.at(0) // "a"
'𠮷'.at(0) // "𠮷"

includes(), startsWith(), endsWith()

传统上,JavaScript 只有 indexOf 方法,可以用来确定一个字符串是否包含在另一个字符串中。ES6 又提供了三种新方法。

  • includes():返回布尔值,表示是否找到了参数字符串。
  • startsWith():返回布尔值,表示参数字符串是否在原字符串的头部。
  • endsWith():返回布尔值,表示参数字符串是否在原字符串的尾部。
let s = 'Hello world!';
s.startsWith('Hello') // true
s.endsWith('!') // true
s.includes('o') // true
s.startsWith('world', 6) // true
s.endsWith('Hello', 5) // true
s.includes('o', 3) // true

repeat()

repeat 方法返回一个新字符串,表示将原字符串重复 n 次。

'x'.repeat(3) // "xxx"
'hello'.repeat(2) // "hellohello"
'na'.repeat(0) // ""

参数如果是小数,会被取整。

'na'.repeat(2.9) // "nana"

如果 repeat 的参数是负数或者 Infinity ,会报错。

'na'.repeat(Infinity)
// RangeError
'na'.repeat(-1)
// RangeError

但是,如果参数是 0 到-1 之间的小数,则等同于 0,这是因为会先进行取整运算。0 到-1 之间的小数,取整以后等于 -0 , repeat 视同为 0。

'na'.repeat(-0.9) // ""

参数 NaN 等同于 0。

'na'.repeat(NaN) // ""

如果 repeat 的参数是字符串,则会先转换成数字。

'na'.repeat('na') // ""
'na'.repeat('3') // "nanana"

padStart(),padEnd()

ES2017 引入了字符串补全长度的功能。如果某个字符串不够指定长度,会在头部或尾部补全。 padStart() 用于头部补全, padEnd() 用于尾部补全。

'5'.padEnd(5, '0') // '50000'
'5'.padStart(5, '0') // '00005'

模板字符串

传统的 JavaScript 语言,输出模板通常是这样写的。

$('#result').append(
 'There are <b>' + basket.count + '</b> ' +
 'items in your basket, ' +
 '<em>' + basket.onSale +
 '</em> are on sale!'
);

上面这种写法相当繁琐不方便,ES6 引入了模板字符串解决这个问题。

$('#result').append(`
 There are <b>${basket.count}</b> items
 in your basket, <em>${basket.onSale}</em>
 are on sale!
`);
function fn() {
 return "Hello World";
}
`foo ${fn()} bar`
// foo Hello World bar

标签模板

模板字符串的可以紧跟在一个函数名后面,该函数将被调用来处理这个模板字符串。

alert`123`
// 等同于
alert(123)

标签模板其实不是模板,而是函数调用的一种特殊形式。“标签”指的就是函数,紧跟在后面的模板字符串就是它的参数。

如果模板字符里面有变量,会将模板字符串先处理成多个参数,再调用函数。

let a = 5;
let b = 10;
tag`Hello ${ a + b } world ${ a * b }`;
// 等同于
tag(['Hello ', ' world ', ''], 15, 50);

实例:

let total = 30;
let msg = passthru`The total is ${total} (${total * 1.05} with tax)`;
function passthru (literals) {
    let result = '';
    let i = 0;
    while (i < literals.length) {
        result += literals[i++];
        if (i < arguments.length) {
            result += arguments[i];
        }
    }
    return result;
}
msg // The total is 30 (31.5 with tax)

String.raw()

tring.raw 方法,往往用来充当模板字符串的处理函数,返回一个斜杠都被转义(即斜杠前面再加一个斜杠)的字符串,对应于替换变量后的模板字符串。如果原字符串的斜杠已经转义,那么 String.raw 不会做任何处理。

String.raw`Hi\n${2+3}!`;
// "Hi\\n5!"
String.raw`Hi\u000A!`;
// 'Hi\\u000A!'
String.raw`Hi\\n`
// "Hi\\n"

正则的扩展

RegExp构造函数

ES5中,RegExp 构造函数的参数有两种情况。
第一种情况是,参数是字符串,这时第二个参数表示正则表达式的修饰符(flag)。

var regex = new RegExp('xyz', 'i');
// 等价于
var regex = /xyz/i;

第二种情况是,参数是一个正则表示式,这时会返回一个原有正则表达式的拷贝。

var regex = new RegExp(/xyz/i);
// 等价于
var regex = /xyz/i;

但是,ES5 不允许此时使用第二个参数添加修饰符,否则会报错。

var regex = new RegExp(/xyz/, 'i');
// Uncaught TypeError: Cannot supply flags when constructing one RegExp from another

ES6中,如果RegExp构造函数第一个参数是一个正则对象,那么第二个参数指定修饰符。会忽略原有的修饰符,只使用新制定的修饰符。

new RegExp(/abc/ig, 'i').flags
// i

字符串的正则方法

字符串对象共有 4 个方法,可以使用正则表达式: match() 、 replace() 、 search() 和 split() 。
ES6 将这 4 个方法,在语言内部全部调用 RegExp 的实例方法,从而做到所有与正则相关的方法,全都定义在 RegExp 对象上。

  • String.prototype.match 调用 RegExp.prototype[Symbol.match]
  • String.prototype.replace 调用 RegExp.prototype[Symbol.replace]
  • String.prototype.search 调用 RegExp.prototype[Symbol.search]
  • String.prototype.split 调用 RegExp.prototype[Symbol.split]

u修饰符

ES6 对正则表达式添加了 u 修饰符,含义为“Unicode 模式”,用来正确处理大于 \uFFFF 的 Unicode 字符。也就是说,会正确处理四个字节的 UTF-16编码。

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

一旦加上 u 修饰符号,就会修改下面这些正则表达式的行为。

  1. 点字符
    点( . )字符在正则表达式中,含义是除了换行符以外的任意单个字符。对于码点大于 0xFFFF 的 Unicode 字符,点字符不能识别,必须加上 u 修饰符。
var s = '𠮷';
/^.$/.test(s) // false
/^.$/u.test(s) // true

上面代码表示,如果不添加 u 修饰符,正则表达式就会认为字符串为两个字符,从而匹配失败。

  1. Unicode字符表示法
    ES6 新增了使用大括号表示 Unicode 字符,这种表示法在正则表达式中必须加上 u 修饰符,才能识别当中的大括号,否则会被解读为量词。
/\u{61}/.test('a') // false
/\u{61}/u.test('a') // true
/\u{20BB7}/u.test('𠮷') // true
  1. 量词
    使用 u 修饰符后,所有量词都会正确识别码点大于 0xFFFF 的 Unicode 字符。
/a{2}/.test('aa') // true
/a{2}/u.test('aa') // true
/𠮷{2}/.test('𠮷𠮷') // false
/𠮷{2}/u.test('𠮷𠮷') // true
  1. 预定义模式
    u 修饰符也影响到预定义模式,能否正确识别码点大于 0xFFFF 的 Unicode 字符。
/^\S$/.test('𠮷') // false
/^\S$/u.test('𠮷') // true

上面代码的 \S 是预定义模式,匹配所有不是空格的字符。只有加了 u 修饰符,它才能正确匹配码点大于 0xFFFF 的 Unicode 字符。利用这一点,可以写出一个正确返回字符串长度的函数。

function codePointLength(text) {
 var result = text.match(/[\s\S]/gu);
 return result ? result.length : 0;
}
var s = '𠮷𠮷';
s.length // 4
codePointLength(s) // 2
  1. i修饰符
    有些 Unicode 字符的编码不同,但是字型很相近,比如, \u004B 与 \u212A 都是大写的 K 。
/[a-z]/i.test('\u212A') // false
/[a-z]/iu.test('\u212A') // true

上面代码中,不加 u 修饰符,就无法识别非规范的 K 字符。

y修饰符

y 修饰符的作用与 g 修饰符类似,也是全局匹配,后一次匹配都从上一次匹配成功的下一个位置开始。不同之处在于, g 修饰符只要剩余位置中存在匹配就可,而 y 修饰符确保匹配必须从剩余的第一个位置开始,这也就是“粘连”的涵义。

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 修饰符号隐含了头部匹配的标志 ^ 。

sticky属性

与 y 修饰符相匹配,ES6 的正则对象多了 sticky 属性,表示是否设置了 y 修饰符。

var r = /hello\d/y;
r.sticky // true

flags属性

ES6 为正则表达式新增了 flags 属性,会返回正则表达式的修饰符。

// ES5 的 source 属性
// 返回正则表达式的正文
/abc/ig.source
// "abc"
// ES6 的 flags 属性
// 返回正则表达式的修饰符
/abc/ig.flags
// 'gi'

s修饰符:dotAll模式

正则表达式中,点( . )是一个特殊字符,代表任意的单个字符,但是行终止符(line terminator character)除外。
以下四个字符属于”行终止符“。

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

如果需要匹配包含行终止符则可以使用

/foo[^]bar/.test('foo\nbar')
// true
/foo.bar/s.test('foo\nbar')
// true

这被称为 dotAll 模式,即点(dot)代表一切字符。所以,正则表达式还引入了一个 dotAll 属性,返回一个布尔值,表示该正则表达式是否处在 dotAll模式。

Unicode 属性类

引入了一种新的类的写法 \p{…} 和 \P{…} ,允许正则表达式匹配符合 Unicode 某种属性的所有字符。

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

\P{…} 是 \p{…} 的反向匹配,即匹配不满足条件的字符。注意,这两种类只对 Unicode 有效,所以使用的时候一定要加上 u 修饰符。如果不加 u 修饰符,正则表达式使用 \p 和 \P 会报错,ECMAScript 预留了这
两个类。

具名组匹配

正则表达式使用圆括号进行组匹配。

const RE_DATE = /(\d{4})-(\d{2})-(\d{2})/;
const matchObj = RE_DATE.exec('1999-12-31');
const year = matchObj[1]; // 1999
const month = matchObj[2]; // 12
const day = matchObj[3]; // 31

现在有一个“具名组匹配”(Named Capture Groups)的提案,允许为每一个组匹配指定一个名字,既便于阅读代码,又便于引用(“问号 + 尖括号 + 组名”的格式)。

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

有了具名组匹配以后,可以使用解构赋值直接从匹配结果上为变量赋值。

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

数值的扩展

数值的扩展

ES6 提供了二进制和八进制数值的新的写法,分别用前缀 0b (或 0B )和 0o (或 0O )表示。

0b111110111 === 503 // true
0o767 === 503 // true

从 ES5 开始,在严格模式之中,八进制就不再允许使用前缀 0 表示,ES6 进一步明确,要使用前缀 0o 表示。
如果要将 0b 和 0o 前缀的字符串数值转为十进制,要使用 Number 方法。

Number('0b111') // 7
Number('0o10') // 8

Number.isFinite(), Number.isNaN()

ES6 在 Number 对象上,新提供了 Number.isFinite() 和 Number.isNaN() 两个方法。
Number.isFinite() 用来检查一个数值是否为有限的(finite)。

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.isNaN(NaN) // true
Number.isNaN(15) // false
Number.isNaN('15') // false
Number.isNaN(true) // false
Number.isNaN(9/NaN) // true
Number.isNaN('true'/0) // true
Number.isNaN('true'/'true') // true

这两个新方法只对数值有效, Number.isFinite() 对于非数值一律返回 false , Number.isNaN() 只有对于 NaN 才返回 true ,非 NaN 一律返回 false。

Number.parseInt(), Number.parseFloat()

ES6 将全局方法 parseInt() 和 parseFloat() ,移植到 Number 对象上面,行为完全保持不变。

// ES5的写法
parseInt('12.34') // 12
parseFloat('123.45#') // 123.45
// ES6的写法
Number.parseInt('12.34') // 12
Number.parseFloat('123.45#') // 123.45

Number.isInteger()

Number.isInteger() 用来判断一个值是否为整数。需要注意的是,在 JavaScript 内部,整数和浮点数是同样的储存方法,所以 3 和 3.0 被视为同一个值。

Number.isInteger(25) // true
Number.isInteger(25.0) // true
Number.isInteger(25.1) // false
Number.isInteger("15") // false
Number.isInteger(true) // false

Number.EPSILON

ES6 在 Number 对象上面,新增一个极小的常量 Number.EPSILON,相当于2的-52 次方。根据规格,它表示 1 与大于 1 的最小浮点数之间的差。引入一个这么小的量的目的,在于为浮点数计算,设置一个误差范围。我们知道浮点数计算是不精确的。

0.1 + 0.2
// 0.30000000000000004
0.1 + 0.2 - 0.3
// 5.551115123125783e-17
5.551115123125783e-17.toFixed(20)
// '0.00000000000000005551'

Number.EPSILON 可以用来设置“能够接受的误差范围”。比如,误差范围设为 2 的-50 次方(即 Number.EPSILON * Math.pow(2, 2) ),即如果两个浮点数的差小于这个值,我们就认为这两个浮点数相等。

function withinErrorMargin (left, right) {
 return Math.abs(left - right) < Number.EPSILON * Math.pow(2, 2);
}
0.1 + 0.2 === 0.3 // false
withinErrorMargin(0.1 + 0.2, 0.3) // true

安全整数和 Number.isSafeInteger()

JavaScript 能够准确表示的整数范围在 -2^53 到 2^53 之间(不含两个端点),超过这个范围,无法精确表示这个值。

Math.pow(2, 53) // 9007199254740992
9007199254740992 // 9007199254740992
9007199254740993 // 9007199254740992
Math.pow(2, 53) === Math.pow(2, 53) + 1
// true

ES6 引入了 Number.MAX_SAFE_INTEGER 和 Number.MIN_SAFE_INTEGER 这两个常量,用来表示这个范围的上下限。

Number.MAX_SAFE_INTEGER === Math.pow(2, 53) - 1
// true
Number.MAX_SAFE_INTEGER === 9007199254740991
// true
Number.MIN_SAFE_INTEGER === -Number.MAX_SAFE_INTEGER
// true
Number.MIN_SAFE_INTEGER === -9007199254740991

Number.isSafeInteger() 则是用来判断一个整数是否落在这个范围之内。

Number.isSafeInteger('a') // false
Number.isSafeInteger(3) // true
Number.isSafeInteger(1.2) // false
Number.isSafeInteger(9007199254740990) // true
Number.isSafeInteger(9007199254740992) // false

Math 对象的扩展

Math.trunc()

Math.trunc 方法用于去除一个数的小数部分,返回整数部分。

Math.trunc(4.1) // 4
Math.trunc(4.9) // 4

对于非数值, Math.trunc 内部使用 Number 方法将其先转为数值。

Math.trunc('123.456') // 123
Math.trunc(true) //1
Math.trunc(false) // 0
Math.trunc(null) // 0

对于空值和无法截取整数的值,返回 NaN 。

Math.trunc(NaN); // NaN
Math.trunc('foo'); // NaN
Math.trunc(); // NaN
Math.trunc(undefined) // NaN

Math.sign()

Math.sign 方法用来判断一个数到底是正数、负数、还是零。对于非数值,会先将其转换为数值。
它会返回五种值。

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

Math.cbrt()

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

Math.cbrt(-1) // -1
Math.cbrt(8) // 2
Math.cbrt('hello') // NaN

Math.clz32()

JavaScript 的整数使用 32 位二进制形式表示, Math.clz32 方法返回一个数的 32 位无符号整数形式有多少个前导 0。

Math.clz32(0) // 32
Math.clz32(1) // 31
Math.clz32(1000) // 22
Math.clz32(0b01000000000000000000000000000000) // 1
Math.clz32(0b00100000000000000000000000000000) // 2

Math.imul()

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

Math.imul(2, 4) // 8
Math.imul(-1, 8) // -8
Math.imul(-2, -2) // 4

Math.fround()

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

Math.fround(0) // 0
Math.fround(1) // 1
Math.fround(1.337) // 1.3370000123977661
Math.fround(1.5) // 1.5
Math.fround(NaN) // NaN

Math.hypot()

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

Math.hypot(3, 4); // 5
Math.hypot(3, 4, 5); // 7.0710678118654755
Math.hypot(); // 0
Math.hypot(NaN); // NaN
Math.hypot(3, 4, 'foo'); // NaN
Math.hypot(3, 4, '5'); // 7.0710678118654755
Math.hypot(-3); // 3

上面代码中,3 的平方加上 4 的平方,等于 5 的平方。

Math.signbit()

Math.sign() 用来判断一个值的正负,但是如果参数是 -0 ,它会返回 -0 。

Math.signbit(2) //false
Math.signbit(-2) //true
Math.signbit(0) //false
Math.signbit(-0) //true
  • 如果参数是 NaN ,返回 false
  • 如果参数是 -0 ,返回 true
  • 如果参数是负值,返回 true
  • 其他情况返回 false

指数运算符

ES2016 新增了一个指数运算符( ** )。

2 ** 2 // 4
2 ** 3 // 8

指数运算符可以与等号结合,形成一个新的赋值运算符( **= )。

let a = 1.5;
a **= 2;
// 等同于 a = a * a;
let b = 4;
b **= 3;
// 等同于 b = b * b * b;

Integer 数据类型

引入了新的数据类型 Integer(整数),来解决这个问题。整数类型的数据只用来表示整数,没有位数的限制,任何位数的整数都可以精确表示。为了与 Number 类型区别,Integer 类型的数据必须使用后缀 n 表示。不能使用(==),因为会改变数据类型,也不允许混合使用。

1n + 2n // 3n
typeof 123n
// 'integer
Integer(123) // 123n
Integer('123') // 123n
Integer(false) // 0n
Integer(true) // 1n
new Integer() // TypeError
Integer(undefined) //TypeError
Integer(null) // TypeError
Integer('123n') // SyntaxError
Integer('abc') // SyntaxError

函数的扩展

ES6 之前,不能直接为函数的参数指定默认值,ES6 允许为函数的参数设置默认值,即直接写在参数定义的后面。

function log(x, y = 'World') {
 console.log(x, y);
}

参数变量是默认声明的,所以不能用 let 或 const 再次声明,否则会报错。
参数默认值可以与解构赋值的默认值,结合起来使用。

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

function fetch(url, { body = '', method = 'GET', headers = {} } = {}) {
 console.log(method);
}
fetch('http://example.com')

函数的 length 属性

指定了默认值以后,函数的 length 属性,将返回没有指定默认值的参数个数。也就是说,指定了默认值后, length 属性将失真。

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

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

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

rest 参数

ES6 引入 rest 参数(形式为 …变量名 ),用于获取函数的多余参数,这样就不需要使用 arguments 对象了。rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中。

function add(...values) {
 let sum = 0;
 for (var val of values) {
 sum += val;
 }
 return sum;
}
add(2, 5, 3) // 10
// arguments变量的写法
function sortNumbers() {
 return Array.prototype.slice.call(arguments).sort();
}
// rest参数的写法
const sortNumbers = (...numbers) => numbers.sort();

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

严格模式

从 ES5 开始,函数内部可以设定为严格模式。

function doSomething(a, b) {
 'use strict';
 // code
}

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

// 报错
function doSomething(a, b = a) {
 'use strict';
 // code
}
// 报错
const doSomething = function ({a, b}) {
 'use strict';
 // code
};
// 报错
const doSomething = (...a) => {
 'use strict';
 // code
};

两种方法可以规避这种限制。第一种是设定全局性的严格模式,这是合法的。

'use strict';
function doSomething(a, b = a) {
 // code
}

第二种是把函数包在一个无参数的立即执行函数里面。

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

name属性

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

function foo() {}
foo.name // "foo"

如果将一个匿名函数赋值给一个变量,ES5 的 name 属性,会返回空字符串,而 ES6 的 name 属性会返回实际的函数名。如果将一个具名函数赋值给一个变量,则 ES5 和 ES6 的 name 属性都返回这个具名函数原本的名字。

ar f = function () {};
// ES5
f.name // ""
// ES6
f.name // "f"

const bar = function baz() {};
// ES5
bar.name // "baz"
// ES6
bar.name // "baz"

(new Function).name // "anonymous"

function foo() {};
foo.bind({}).name // "bound foo"
(function(){}).bind({}).name // "bound "

箭头函数

var f = v => v;
// 上面的箭头函数等同于:
var f = function(v) {
 return v;
};

双冒号运算符

箭头函数可以绑定 this 对象,大大减少了显式绑定 this 对象的写法( call 、 apply 、 bind )。但是,箭头函数并不适用于所有场合,所以现在有一个提案,提出了“函数绑定”(function bind)运算符,用来取代 call 、 apply 、 bind 调用。
函数绑定运算符是并排的两个冒号( :: ),双冒号左边是一个对象,右边是一个函数。该运算符会自动将左边的对象,作为上下文环境(即 this 对象),绑定到右边的函数上面。

foo::bar;
// 等同于
bar.bind(foo);
foo::bar(...arguments);
// 等同于
bar.apply(foo, arguments);
const hasOwnProperty = Object.prototype.hasOwnProperty;
function hasOwn(obj, key) {
 return obj::hasOwnProperty(key);
}

如果双冒号左边为空,右边是一个对象的方法,则等于将该方法绑定在该对象上面。

var method = obj::obj.foo;
// 等同于
var method = ::obj.foo;
let log = ::console.log;
// 等同于
var log = console.log.bind(console);

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

尾调用(Tail Call)是函数式编程的一个重要概念,本身非常简单,一句话就能说清楚,就是指某个函数的最后一步是调用另一个函数。

function f(x){
 return g(x);
}

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

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

函数调用会在内存形成一个“调用记录”,又称“调用帧”(call frame),保存调用位置和内部变量等信息。如果在函数 A 的内部调用函数 B ,那么在 A 的调用帧上方,还会形成一个 B 的调用帧。等到 B 运行结束,将结果返回到 A , B 的调用帧才会消失。如果函数 B 内部还调用函数 C ,那就还有一个 C 的调用帧,以此类推。所有的调用帧,就形成一个“调用栈”(call stack)。
尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用帧,取代外层函数的调用帧就可以了。

function f() {
 let m = 1;
 let n = 2;
 return g(m + n);
}
f();
// 等同于
function f() {
 return g(3);
}
f();
// 等同于
g(3);

这就叫做“尾调用优化”(Tail call optimization),即只保留内层函数的调用帧。如果所有函数都是尾调用,那么完全可以做到每次执行时,调用帧只有一项,这将大大节省内存。这就是“尾调用优化”的意义。注意,只有不再用到外层函数的内部变量,内层函数的调用帧才会取代外层函数的调用帧,否则就无法进行“尾调用优化”。

function addOne(a){
 var one = 1;
 function inner(b){
 return b + one;
 }
 return inner(a);
}

函数调用自身,称为递归。如果尾调用自身,就称为尾递归。递归非常耗费内存,因为需要同时保存成千上百个调用帧,很容易发生“栈溢出”错误(stack overflow)。但对于尾递归来说,由于只存在一个调用帧,所以永远不会发生“栈溢出”错误。
下面是一个阶乘函数,计算 n 的阶乘,最多需要保存 n 个调用记录,复杂度 O(n) 。

function factorial(n) {
 if (n === 1) return 1;
 return n * factorial(n - 1);
}
factorial(5) // 120

如果改写成尾递归,只保留一个调用记录,复杂度 O(1) 。

function factorial(n, total) {
 if (n === 1) return total;
 return factorial(n - 1, n * total);
}
factorial(5, 1) // 120

数组的扩展

扩展运算符

扩展运算符(spread)是三个点( … )。它好比 rest 参数的逆运算,将一个数组转为用逗号分隔的参数序列。

console.log(...[1, 2, 3])
// 1 2 3
可以替代数组的 apply 方法
// ES5 的写法
Math.max.apply(null, [14, 3, 77])
// ES6 的写法
Math.max(...[14, 3, 77])
// 等同于
Math.max(14, 3, 77); 
扩展运算符的应用
  1. 复制数组
const a1 = [1, 2];
// 写法一
const a2 = [...a1];
// 写法二
const [...a2] = a1;
  1. 合并数组
// ES5
[1, 2].concat(more)
// ES6
[1, 2, ...more]
var arr1 = ['a', 'b'];
var arr2 = ['c'];
var arr3 = ['d', 'e'];
// ES5的合并数组
arr1.concat(arr2, arr3);
// [ 'a', 'b', 'c', 'd', 'e' ]
// ES6的合并数组
[...arr1, ...arr2, ...arr3]
// [ 'a', 'b', 'c', 'd', 'e' ]
  1. 与解构赋值结合
    扩展运算符可以与解构赋值结合起来,用于生成数组,如果将扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报错。
const [first, ...rest] = [1, 2, 3, 4, 5];
first // 1
rest // [2, 3, 4, 5]
const [first, ...rest] = [];
first // undefined
rest // []
const [first, ...rest] = ["foo"];
first // "foo"
rest // []
  1. 字符串
    扩展运算符还可以将字符串转为真正的数组。
[...'hello']
// [ "h", "e", "l", "l", "o" ]
// 可以正确识别四个字节的 Unicode 字符。
'x\uD83D\uDE80y'.length // 4
[...'x\uD83D\uDE80y'].length // 3
  1. 实现了 Iterator 接口的对象
    任何 Iterator 接口的对象(参阅 Iterator 一章),都可以用扩展运算符转为真正的数组。
let nodeList = document.querySelectorAll('div');
let array = [...nodeList];
  1. Map 和 Set 结构,Generator 函数
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值