ES6
一、ECMAScript相关介绍
1.简介
- ES6是前端就业趋势,就业必备技能
- 泛指ES2015及之后的新增特性
- ES6是JS(JavaScript)的一个版本标准
- ECMAScript是一种由Ecma国际(前身为欧洲计算机制造商协会, 英文名称是European Computer Manufacturers Association)
- 目标:使得 JavaScript 语言可以用来编写复杂的大型应用程序,成为企业级开发语言。
2.ECMAScript 和 JavaScript 的关系
前者是后者的规格,后者是前者的一种实现(另外的 ECMAScript 方言还有 Jscript 和 ActionScript)。日常场合,这两个词是可以互换的。
3.ES6 与 ECMAScript 2015 的关系
ES6 既是一个历史名词,也是一个泛指,含义是5.1版以后的 JavaScript 的下一代标准,涵盖了ES2015、ES2016、ES2017等等,而ES2015 则是正式名称,特指该年发布的正式版本的语言标准。
二、let和const命令
1. let 命令
基本用法:
声明变量,但是所声明的变量只在let命令所在的代码块内有效
{
let a = 10;
var b = 1;
}
a // ReferenceError: a is not defined.
b // 1
for
循环的计数器,就很合适使用let
命令
for (let i = 0; i < 10; i++) {}
console.log(i);
//ReferenceError: i is not defined
- 变量i是var声明的,在全局范围内都有效
- 循环内被赋给数组a的function在运行时,会通过闭包读到这同一个变量i,导致最后输出的是最后一轮的i的值,也就是10。
- 而如果使用let,声明的变量仅在块级作用域内有效,最后输出的是6。
var a = [];
for (var i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 10
循环语句部分是一个父作用域,而循环体内部是一个单独的子作用域。
不存在变量提升
let
命令,它所声明的变量一定要在声明后使用,否则报错。
暂时性死区
只要块级作用域内存在let
命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响。
在代码块内,使用let
命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)。
暂时性死区的本质:只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。
不允许重复声明
let
不允许在相同作用域内,重复声明同一个变量。
块级作用域
为什么需要块级作用域?
S5 只有全局作用域和函数作用域,没有块级作用域,这带来很多不合理的场景。
-
内层变量可能会覆盖外层变量。
var tmp = new Date(); function f() { console.log(tmp); if (false) { var tmp = 'hello world'; } } f(); // undefined
变量提升,导致内层的
tmp
变量覆盖了外层的tmp
变量。 -
用来计数的循环变量泄露为全局变量。
var s = 'hello'; for (var i = 0; i < s.length; i++) { console.log(s[i]); } console.log(i); // 5
变量
i
只用来控制循环,但是循环结束后,它并没有消失,泄露成了全局变量。
ES6 的块级作用域
let
实际上为 JavaScript 新增了块级作用域。- ES6 允许块级作用域的任意嵌套。
- 外层作用域无法读取内层作用域的变量
- 内层作用域可以定义外层作用域的同名变量。
块级作用域与函数声明
- ES5 规定,函数只能在顶层作用域和函数作用域之中声明,不能在块级作用域声明。
- ES6 引入了块级作用域,明确允许在块级作用域之中声明函数。ES6 规定,块级作用域之中,函数声明语句的行为类似于
let
,在块级作用域之外不可引用。
do 表达式
本质上,块级作用域是一个语句,将多个操作封装在一起,没有返回值。
{
let t = f();
t = t * t + 1;
}
在块级作用域以外,没有办法得到t
的值,因为块级作用域不返回值,除非t
是全局变量。
在块级作用域之前加上do
,使它变为do
表达式。变量x
会得到整个块级作用域的返回值。
let x = do {
let t = f();
t * t + 1;
};
2.const 命令
基本用法
const
声明一个只读的常量。一旦声明,常量的值就不能改变。(const
一旦声明变量,就必须立即初始化,不能留到以后赋值。)- 对于
const
来说,只声明不赋值,就会报错 - 只在声明所在的块级作用域内有效
const
命令声明的常量也是不提升,同样存在暂时性死区,只能在声明的位置后面使用。
本质
保证变量指向的那个内存地址不得改动。
添加属性,可以成功;
指向另外一个对象,就会报错;
ES6 声明变量的六种方法
ES5 只有两种声明变量的方法:var
和function
。
ES6添加了let
和const
;import
命令和class
顶层对象的属性
浏览器环境:指的是window
对象
Node:global
对象
ES5之中,顶层对象的属性与全局变量是等价的。
从ES6开始----------var
命令和function
命令声明的全局变量依旧是顶层对象的属性:----------let
命令、const
命令、class
命令声明的全局变量,不属于顶层对象的属性
global 对象
在所有环境下,global
都是存在的,都可以从它拿到顶层对象。
三、变量的解构赋值
1.数组的解构赋值
基本用法
ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)。
如果解构不成功,变量的值就等于undefined
。
let [a, b, c] = [1, 2, 3];
//以下情况foo的值都会等于undefined,解构不成功
let [foo] = [];
let [bar, foo] = [1];
不完全解构,即等号左边的模式,只匹配一部分的等号右边的数组。这种情况下,解构依然可以成功。
let [x, y] = [1, 2, 3];
x // 1
y // 2
let [a, [b], d] = [1, [2, 3], 4];
a // 1
b // 2
d // 4
// 报错
let [foo] = 1;
let [foo] = false;
let [foo] = NaN;
let [foo] = undefined;
let [foo] = null;
let [foo] = {};
//前五个:为对象以后不具备 Iterator 接口
//最后一个:本身就不具备 Iterator 接口
只要某种数据结构具有 Iterator 接口,都可以采用数组形式的解构赋值。
默认值
- 解构赋值允许指定默认值。
let [foo = true] = [];
foo // true
let [x, y = 'b'] = ['a']; // x='a', y='b'
let [x, y = 'b'] = ['a', undefined]; // x='a', y='b
- ES6 内部使用严格相等运算符(
===
),判断一个位置是否有值 - 默认值可以引用解构赋值的其他变量,但该变量必须已经声明。
let [x = 1, y = x] = []; // x=1; y=1
let [x = 1, y = x] = [2]; // x=2; y=2
let [x = 1, y = x] = [1, 2]; // x=1; y=2
let [x = y, y = 1] = []; // ReferenceError
2.对象的解构赋值
- 数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。
let { bar, foo } = { foo: "aaa", bar: "bbb" };
foo // "aaa"
bar // "bbb"
let { baz } = { foo: "aaa", bar: "bbb" };
baz // undefined
- 如果变量名与属性名不一致,必须写成下面这样。
var { foo: baz } = { foo: 'aaa', bar: 'bbb' };
baz // "aaa"
let obj = { first: 'hello', last: 'world' };
let { first: f, last: l } = obj;
f // 'hello'
l // 'world'
- 对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。真正被赋值的是后者,而不是前者。
- 变量的声明和赋值是一体的。对于
let
和const
来说,变量不能重新声明,所以一旦赋值的变量以前声明过,就会报错。 - 解构也可以用于嵌套结构的对象
- 对象的解构也可以指定默认值。默认值生效的条件是,对象的属性值严格等于
undefined
。
var {x = 3} = {};
x // 3
var {x, y = 5} = {x: 1};
x // 1
y // 5
var {x:y = 3} = {};
y // 3
var {x:y = 3} = {x: 5};
y // 5
var { message: msg = 'Something went wrong' } = {};
msg // "Something went wrong"
3.字符串的解构赋值
- 字符串也可以解构赋值。这是因为此时,字符串被转换成了一个类似数组的对象。
const [a, b, c, d, e] = 'hello';
a // "h"
b // "e"
c // "l"
d // "l"
e // "o"
-
类似数组的对象都有一个
length
属性,因此还可以对这个属性解构赋值。let {length : len} = 'hello'; len // 5
4.数值和布尔值的解构赋值
- 解构赋值时,如果等号右边是数值和布尔值,则会先转为对象。
let {toString: s} = 123;
s === Number.prototype.toString // true
let {toString: s} = true;
s === Boolean.prototype.toString // true
- 数值和布尔值的包装对象都有
toString
属性,因此变量s
都能取到值。 - 解构赋值的规则:只要等号右边的值不是对象或数组,就先将其转为对象。由于
undefined
和null
无法转为对象,所以对它们进行解构赋值,都会报错。
5.函数参数的解构赋值
[[1, 2], [3, 4]].map(([a, b]) => a + b);
// [ 3, 7 ]
- 函数参数的解构也可以使用默认值。
undefined
就会触发函数参数的默认值。
[1, undefined, 3].map((x = 'yes') => x);
// [ 1, 'yes', 3 ]
不能使用圆括号的情况
(1)变量声明语句中,不能带有圆括号。
// 全部报错
let [(a)] = [1];
let {x: (c)} = {};
let ({x: c}) = {};
let {(x: c)} = {};
let {(x): c} = {};
let { o: ({ p: p }) } = { o: { p: 2 } };
(2)函数参数中,模式不能带有圆括号。
// 报错
function f([(z)]) { return z; }
(3)赋值语句中,不能将整个模式,或嵌套模式中的一层,放在圆括号之中。
// 全部报错
({ p: a }) = { p: 42 };
([a]) = [5];
可以使用圆括号的情况
赋值语句的非模式部分,可以使用圆括号
[(b)] = [3]; // 正确
({ p: (d) } = {}); // 正确
[(parseInt.prop)] = [3]; // 正确
用途
(1)交换变量的值
let x = 1;
let y = 2;
[x, y] = [y, x];
(2)从函数返回多个值
/ 返回一个数组
function example() {
return [1, 2, 3];
}
let [a, b, c] = example();
// 返回一个对象
function example() {
return {
foo: 1,
bar: 2
};
}
let { foo, bar } = example();
(3)函数参数的定义
// 参数是一组有次序的值
function f([x, y, z]) { ... }
f([1, 2, 3]);
// 参数是一组无次序的值
function f({x, y, z}) { ... }
f({z: 3, y: 2, x: 1});
(4)提取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]
(5)函数参数的默认值
(6)遍历Map结构
任何部署了Iterator接口的对象,都可以用for...of
循环遍历。Map结构原生支持Iterator接口,配合变量的解构赋值,获取键名和键值就非常方便。
var map = new Map();
map.set('first', 'hello');
map.set('second', 'world');
for (let [key, value] of map) {
console.log(key + " is " + value);
}
// first is hello
// second is world
// 获取键名
for (let [key] of map) {
// ...
}
// 获取键值
for (let [,value] of map) {
// ...
}
(7)输入模块的指定方法
const { SourceMapConsumer, SourceNode } = require("source-map");
四、字符串的扩展
JavaScript 允许采用\uxxxx
形式表示一个字符,其中xxxx
表示字符的 Unicode 码点。
但是,这种表示法只限于码点在\u0000
~\uFFFF
之间的字符。超出这个范围的字符,必须用两个双字节的形式表示。
codePointAt()
codePointAt
方法,能够正确处理4个字节储存的字符,返回一个字符的码点。
var s = 'a';
s.codePointAt(0) // 134071
s.codePointAt(1) // 57271
s.codePointAt(2) // 97
codePointAt
方法是测试一个字符由两个字节还是由四个字节组成的最简单方法。
function is32Bit(c) {
return c.codePointAt(0) > 0xFFFF;
}
is32Bit("") // true
is32Bit("a") // false
String.fromCodePoint()
String.fromCodePoint(0x20BB7)
// ""
String.fromCodePoint(0x78, 0x1f680, 0x79) === 'x\uD83D\uDE80y'
// true
注意,fromCodePoint
方法定义在String
对象上,而codePointAt
方法定义在字符串的实例对象上。
字符串的遍历器接口
ES6为字符串添加了遍历器接口,使得字符串可以被for...of
循环遍历。
除了遍历字符串,这个遍历器最大的优点是可以识别大于0xFFFF
的码点,传统的for
循环无法识别这样的码点。
var text = String.fromCodePoint(0x20BB7);
for (let i = 0; i < text.length; i++) {
console.log(text[i]);
}
// " "
// " "
for (let i of text) {
console.log(i);
}
// ""
字符串text
只有一个字符,但是for
循环会认为它包含两个字符(都不可打印),而for...of
循环会正确识别出这一个字符。
at()
可以识别Unicode编号大于0xFFFF
的字符,返回正确的字符。
'abc'.at(0) // "a"
''.at(0) // ""
这个方法可以通过垫片库实现。
normalize()
ES6提供字符串实例的normalize()
方法,用来将字符的不同表示方法统一为同样的形式,这称为Unicode正规化。
'\u01D1'.normalize() === '\u004F\u030C'.normalize()
// true
includes(), startsWith(), endsWith()
传统上,JavaScript只有indexOf
方法,可以用来确定一个字符串是否包含在另一个字符串中。ES6又提供了三种新方法。
- includes():返回布尔值,表示是否找到了参数字符串。
- startsWith():返回布尔值,表示参数字符串是否在源字符串的头部。
- endsWith():返回布尔值,表示参数字符串是否在源字符串的尾部。
var s = 'Hello world!';
s.startsWith('Hello') // true
s.endsWith('!') // true
s.includes('o') // 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()
如果某个字符串不够指定长度,会在头部或尾部补全。padStart()
用于头部补全,padEnd()
用于尾部补全。
padStart
和padEnd
一共接受两个参数,第一个参数用来指定字符串的最小长度,第二个参数是用来补全的字符串。
如果原字符串的长度,等于或大于指定的最小长度,则返回原字符串。
'xxx'.padStart(2, 'ab') // 'xxx'
'xxx'.padEnd(2, 'ab') // 'xxx'
如果用来补全的字符串与原字符串,两者的长度之和超过了指定的最小长度,则会截去超出位数的补全字符串。
'abc'.padStart(10, '0123456789')
// '0123456abc'
如果省略第二个参数,默认使用空格补全长度。
'x'.padStart(4) // ' x'
'x'.padEnd(4) // 'x '
padStart
的常见用途是为数值补全指定位数。下面代码生成10位的数值字符串。
'1'.padStart(10, '0') // "0000000001"
'12'.padStart(10, '0') // "0000000012"
'123456'.padStart(10, '0') // "0000123456"
另一个用途是提示字符串格式。
'12'.padStart(10, 'YYYY-MM-DD') // "YYYY-MM-12"
'09-12'.padStart(10, 'YYYY-MM-DD') // "YYYY-09-12"
模板字符串
模板字符串(template string)是增强版的字符串,用反引号(`)标识。它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量。
$('#result').append(`
There are <b>${basket.count}</b> items
in your basket, <em>${basket.onSale}</em>
are on sale!
`);
模板字符串,都是用反引号表示。如果在模板字符串中需要使用反引号,则前面要用反斜杠转义。
var greeting = `\`Yo\` World!`;
如果使用模板字符串表示多行字符串,所有的空格和缩进都会被保留在输出之中。
$('#list').html(`
<ul>
<li>first</li>
<li>second</li>
</ul>
`);
标签模板
模板字符串的功能,不仅仅是上面这些。它可以紧跟在一个函数名后面,该函数将被调用来处理这个模板字符串。这被称为“标签模板”功能(tagged template)。
“标签”指的就是函数,紧跟在后面的模板字符串就是它的参数。
五、正则的扩展
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
字符串的正则方法
字符串对象共有4个方法,可以使用正则表达式:
match()
、replace()
、search()
和split()
。
String.prototype.match` 调用 `RegExp.prototype[Symbol.match]
......
u修饰符
含义为“Unicode模式”,用来正确处理大于\uFFFF
的Unicode字符。也就是说,会正确处理四个字节的UTF-16编码。
/^\uD83D/u.test('\uD83D\uDC2A')
// false
/^\uD83D/.test('\uD83D\uDC2A')
// true
ES5不支持四个字节的UTF-16编码,会将其识别为两个字符,导致第二行代码结果为true
。加了u
修饰符以后,ES6就会识别其为一个字符,所以第一行代码结果为false
。
(1)点字符
点(.
)字符在正则表达式中,含义是除了换行符以外的任意单个字符。对于码点大于0xFFFF
的Unicode字符,点字符不能识别,必须加上u
修饰符。
var s = '';
/^.$/.test(s) // false
/^.$/u.test(s) // true
(2)Unicode字符表示法
ES6新增了使用大括号表示Unicode字符,这种表示法在正则表达式中必须加上u
修饰符,才能识别。
/\u{61}/.test('a') // false
/\u{61}/u.test('a') // true
/\u{20BB7}/u.test('') // true
(3)量词
使用u
修饰符后,所有量词都会正确识别码点大于0xFFFF
的Unicode字符。
/a{2}/.test('aa') // true
/a{2}/u.test('aa') // true
/{2}/.test('') // false
/{2}/u.test('') // true
(4)预定义模式
u
修饰符也影响到预定义模式,能否正确识别码点大于0xFFFF
的Unicode字符。
/^\S$/.test('') // false
/^\S$/u.test('') // true
(5)i修饰符
有些Unicode字符的编码不同,但是字型很相近,比如,\u004B
与\u212A
都是大写的K
。
/[a-z]/i.test('\u212A') // false
/[a-z]/iu.test('\u212A') // true
y 修饰符
ES6还为正则表达式添加了y
修饰符,叫做“粘连”(sticky)修饰符。
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
sticky属性
var r = /hello\d/y;
r.sticky // true
flags属性
ES6为正则表达式新增了flags
属性,会返回正则表达式的修饰符。
// ES5的source属性
// 返回正则表达式的正文
/abc/ig.source
// "abc"
// ES6的flags属性
// 返回正则表达式的修饰符
/abc/ig.flags
// 'gi'
RegExp.escape()
字符串必须转义,才能作为正则模式。
function escapeRegExp(str) {
return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&');
}
let str = '/path/to/resource.html?search=query';
escapeRegExp(str)
// "\/path\/to\/resource\.html\?search=query"
s 修饰符:dotAll 模式
正则表达式中,点(.
)是一个特殊字符,代表任意的单个字符,但是行终止符(line terminator character)除外。
以下四个字符属于”行终止符“。
- U+000A 换行符(
\n
) - U+000D 回车符(
\r
) - U+2028 行分隔符(line separator)
- U+2029 段分隔符(paragraph separator)
后行断言
JavaScript 语言的正则表达式,只支持先行断言(lookahead)和先行否定断言(negative lookahead),不支持后行断言(lookbehind)和后行否定断言(negative lookbehind)。
六、数值的扩展
二进制和八进制表示法
ES6 提供了二进制和八进制数值的新的写法,分别用前缀0b
(或0B
)和0o
(或0O
)表示。
0b111110111 === 503 // true
0o767 === 503 // true
如果要将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
们与传统的全局方法isFinite()
和isNaN()
的区别在于,传统方法先调用Number()
将非数值的值转为数值,再进行判断,而这两个新方法只对数值有效,Number.isFinite()
对于非数值一律返回false
, Number.isNaN()
只有对于NaN
才返回true
,非NaN
一律返回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.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.parseInt === parseInt // true
Number.parseFloat === parseFloat // true
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
。
Number.EPSILON
// 2.220446049250313e-16
Number.EPSILON.toFixed(20)
// '0.00000000000000022204'
引入一个这么小的量的目的,在于为浮点数计算,设置一个误差范围。我们知道浮点数计算是不精确的。
0.1 + 0.2
// 0.30000000000000004
0.1 + 0.2 - 0.3
// 5.551115123125783e-17
5.551115123125783e-17.toFixed(20)
// '0.00000000000000005551'
但是如果这个误差能够小于Number.EPSILON
,我们就可以认为得到了正确结果。
5.551115123125783e-17 < Number.EPSILON
// true
安全整数和Number.isSafeInteger()
ES6引入了Number.MAX_SAFE_INTEGER
和Number.MIN_SAFE_INTEGER
这两个常量,用来表示这个范围的上下限。
Number.isSafeInteger()
则是用来判断一个整数是否落在这个范围之内。
Number.isSafeInteger('a') // false
Number.isSafeInteger(null) // false
Number.isSafeInteger(NaN) // false
Number.isSafeInteger(Infinity) // false
Number.isSafeInteger(-Infinity) // false
Number.isSafeInteger(3) // true
Number.isSafeInteger(1.2) // false
Number.isSafeInteger(9007199254740990) // true
Number.isSafeInteger(9007199254740992) // false
Number.isSafeInteger(Number.MIN_SAFE_INTEGER - 1) // false
Number.isSafeInteger(Number.MIN_SAFE_INTEGER) // true
Number.isSafeInteger(Number.MAX_SAFE_INTEGER) // true
Number.isSafeInteger(Number.MAX_SAFE_INTEGER + 1) // false
Math对象的扩展
Math.trunc()
Math.trunc
方法用于去除一个数的小数部分,返回整数部分。
对于非数值,Math.trunc
内部使用Number
方法将其先转为数值
对于空值和无法截取整数的值,返回NaN。
Math.sign()
Math.sign
方法用来判断一个数到底是正数、负数、还是零。
它会返回五种值。
- 参数为正数,返回+1;
- 参数为负数,返回-1;
- 参数为0,返回0;
- 参数为-0,返回-0;
- 其他值,返回NaN。
Math.cbrt()
Math.cbrt
方法用于计算一个数的立方根。
对于非数值,Math.cbrt
方法内部也是先使用Number
方法将其转为数值
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.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
对数方法
(1) Math.expm1()
Math.expm1(x)
返回ex - 1,即Math.exp(x) - 1
。
(2)Math.log1p()
Math.log1p(x)
方法返回1 + x
的自然对数,即Math.log(1 + x)
。如果x
小于-1,返回NaN
。
(3)Math.log10()
Math.log10(x)
返回以10为底的x
的对数。如果x
小于0,则返回NaN。
(4)Math.log2()
Math.log2(x)
返回以2为底的x
的对数。如果x
小于0,则返回NaN。
三角函数方法
ES6新增了6个三角函数方法。
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)
Math.signbit()
Math.sign()
用来判断一个值的正负,但是如果参数是-0
,它会返回-0
。
- 如果参数是
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;
七、数组的扩展
Array.from()
用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象
let arrayLike = {
'0': 'a',
'1': 'b',
'2': 'c',
length: 3
};
// ES5的写法
var arr1 = [].slice.call(arrayLike); // ['a', 'b', 'c']
// ES6的写法
let arr2 = Array.from(arrayLike); // ['a', 'b', 'c']
值得提醒的是,扩展运算符(...
)也可以将某些数据结构转为数组。
Array.from
还可以接受第二个参数,作用类似于数组的map
方法,用来对每个元素进行处理,将处理后的值放入返回的数组。
Array.from(arrayLike, x => x * x);
// 等同于
Array.from(arrayLike).map(x => x * x);
Array.from([1, 2, 3], (x) => x * x)
// [1, 4, 9]
Array.of()
Array.of
方法用于将一组值,转换为数组。
Array.of(3, 11, 8) // [3,11,8]
Array.of(3) // [3]
Array.of(3).length // 1
这个方法的主要目的,是弥补数组构造函数Array()
的不足。因为参数个数的不同,会导致Array()
的行为有差异。
Array() // []
Array(3) // [, , ,]
Array(3, 11, 8) // [3, 11, 8]
Array.of
总是返回参数值组成的数组。如果没有参数,就返回一个空数组。
数组实例的copyWithin()
数组实例的copyWithin
方法,在当前数组内部,将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组。也就是说,使用这个方法,会修改当前数组。
Array.prototype.copyWithin(target, start = 0, end = this.length)
target(必需):从该位置开始替换数据。
start(可选):从该位置开始读取数据,默认为0。如果为负值,表示倒数。
end(可选):到该位置前停止读取数据,默认等于数组长度。如果为负值,表示倒数。
这三个参数都应该是数值,如果不是,会自动转为数值。
[1, 2, 3, 4, 5].copyWithin(0, 3)
// [4, 5, 3, 4, 5]
// 将3号位复制到0号位
[1, 2, 3, 4, 5].copyWithin(0, 3, 4)
// [4, 2, 3, 4, 5]
// -2相当于3号位,-1相当于4号位
[1, 2, 3, 4, 5].copyWithin(0, -2, -1)
// [4, 2, 3, 4, 5]
数组实例的find()和findIndex()
数组实例的find
方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为true
的成员,然后返回该成员。如果没有符合条件的成员,则返回undefined
。
[1, 4, -5, 10].find((n) => n < 0)
// -5
数组实例的findIndex
方法的用法与find
方法非常类似,返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1
。
数组实例的fill()
fill
方法使用给定值,填充一个数组。
['a', 'b', 'c'].fill(7)
// [7, 7, 7]
new Array(3).fill(7)
// [7, 7, 7]
['a', 'b', 'c'].fill(7, 1, 2)
// ['a', 7, 'c']
数组实例的entries(),keys()和values()
entries()
,keys()
和values()
——用于遍历数组,都返回一个遍历器对象,可以用for...of
循环进行遍历,唯一的区别是keys()
是对键名的遍历、values()
是对键值的遍历,entries()
是对键值对的遍历。
for (let index of ['a', 'b'].keys()) {
console.log(index);
}
// 0
// 1
for (let elem of ['a', 'b'].values()) {
console.log(elem);
}
// 'a'
// 'b'
for (let [index, elem] of ['a', 'b'].entries()) {
console.log(index, elem);
}
// 0 "a"
// 1 "b"
如果不使用for...of
循环,可以手动调用遍历器对象的next
方法,进行遍历
let letter = ['a', 'b', 'c'];
let entries = letter.entries();
console.log(entries.next().value); // [0, 'a']
console.log(entries.next().value); // [1, 'b']
console.log(entries.next().value); // [2, 'c']
数组实例的includes()
Array.prototype.includes
方法返回一个布尔值,表示某个数组是否包含给定的值
[1, 2, 3].includes(2); // true
[1, 2, 3].includes(4); // false
[1, 2, NaN].includes(NaN); // true
该方法的第二个参数表示搜索的起始位置,默认为0。如果第二个参数为负数,则表示倒数的位置,如果这时它大于数组长度(比如第二个参数为-4,但数组长度为3),则会重置为从0开始。
[1, 2, 3].includes(3, 3); // false
[1, 2, 3].includes(3, -1); // true
通常使用数组的indexOf
方法,检查是否包含某个值。
数组的空位
数组的空位指,数组的某一个位置没有任何值。比如,Array
构造函数返回的数组都是空位。
forEach()
,filter()
,every()
和some()
都会跳过空位。map()
会跳过空位,但会保留这个值join()
和toString()
会将空位视为undefined
,而undefined
和null
会被处理成空字符串。
// forEach方法
[,'a'].forEach((x,i) => console.log(i)); // 1
// filter方法
['a',,'b'].filter(x => true) // ['a','b']
// every方法
[,'a'].every(x => x==='a') // true
// some方法
[,'a'].some(x => x !== 'a') // false
// map方法
[,'a'].map(x => 1) // [,1]
// join方法
[,'a',undefined,null].join('#') // "#a##"
// toString方法
[,'a',undefined,null].toString() // ",a,,"
Array.from
方法会将数组的空位,转为undefined
,也就是说,这个方法不会忽略空位。
Array.from(['a',,'b'])
// [ "a", undefined, "b" ]
扩展运算符(...
)也会将空位转为undefined
。
[...['a',,'b']]
// [ "a", undefined, "b" ]
八、函数的扩展
函数参数的默认值
参数变量是默认声明的,所以不能用let
或const
再次声明。
function foo(x = 5) {
let x = 1; // error
const x = 2; // error
}
使用参数默认值时,函数不能有同名参数。
参数默认值是惰性求值的。
函数的 length 属性
指定了默认值以后,函数的length
属性,将返回没有指定默认值的参数个数。也就是说,指定了默认值后,length
属性将失真。
作用域
一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域(context)。等到初始化结束,这个作用域就会消失。这种语法行为,在不设置参数默认值时,是不会出现的
rest参数
ES6 引入 rest 参数(形式为“…变量名”),用于获取函数的多余参数,这样就不需要使用arguments
对象了。rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中。
函数的length
属性,不包括 rest 参数。
(function(a) {}).length // 1
(function(...a) {}).length // 0
(function(a, ...b) {}).length // 1
扩展运算符的应用
(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' ]
(2)与解构赋值结合
扩展运算符可以与解构赋值结合起来,用于生成数组。
// ES5
a = list[0], rest = list.slice(1)
// ES6
[a, ...rest] = list
如果将扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报错。
const [...butLast, last] = [1, 2, 3, 4, 5];
// 报错
const [first, ...middle, last] = [1, 2, 3, 4, 5];
// 报错
(3)函数的返回值
JavaScript的函数只能返回一个值,如果需要返回多个值,只能返回数组或对象。扩展运算符提供了解决这个问题的一种变通方法。
var dateFields = readDateFields(database);
var d = new Date(...dateFields);
上面代码从数据库取出一行数据,通过扩展运算符,直接将其传入构造函数Date
。
(4)字符串
扩展运算符还可以将字符串转为真正的数组。
[...'hello']
// [ "h", "e", "l", "l", "o" ]
(5)实现了Iterator接口的对象
任何Iterator接口的对象,都可以用扩展运算符转为真正的数组。
var nodeList = document.querySelectorAll('div');
var array = [...nodeList];
6)Map和Set结构,Generator函数
扩展运算符内部调用的是数据结构的Iterator接口,因此只要具有Iterator接口的对象,都可以使用扩展运算符,比如Map结构。
let map = new Map([
[1, 'one'],
[2, 'two'],
[3, 'three'],
]);
let arr = [...map.keys()]; // [1, 2, 3]
箭头函数有几个使用注意点。
(1)函数体内的this
对象,就是定义时所在的对象,而不是使用时所在的对象。
(2)不可以当作构造函数,也就是说,不可以使用new
命令,否则会抛出一个错误。
(3)不可以使用arguments
对象,该对象在函数体内不存在。如果要用,可以用Rest参数代替。
(4)不可以使用yield
命令,因此箭头函数不能用作Generator函数。
九、Set和Map数据结构
Set
基本用法
Set 本身是一个构造函数,用来生成 Set 数据结构。
const s = new Set();
[2, 3, 5, 4, 5, 2, 2].forEach(x => s.add(x));
for (let i of s) {
console.log(i);
}
// 2 3 5 4
Set 结构不会添加重复的值。
Set 函数可以接受一个数组(或类似数组的对象)作为参数,用来初始化。
// 去除数组的重复成员
[...new Set(array)]
Set实例添加了两个NaN
,但是只能加入一个。这表明,在Set内部,两个NaN
是相等。
另外,两个对象总是不相等的。
由于两个空对象不相等,所以它们被视为两个值。
let set = new Set();
set.add({});
set.size // 1
set.add({});
set.size // 2
Set实例的属性和方法
Set结构的实例有以下属性。
Set.prototype.constructor
:构造函数,默认就是Set
函数。Set.prototype.size
:返回Set
实例的成员总数。
Set实例的方法分为两大类:操作方法(用于操作数据)和遍历方法(用于遍历成员)。下面先介绍四个操作方法。
add(value)
:添加某个值,返回Set结构本身。delete(value)
:删除某个值,返回一个布尔值,表示删除是否成功。has(value)
:返回一个布尔值,表示该值是否为Set
的成员。clear()
:清除所有成员,没有返回值。
s.add(1).add(2).add(2);
// 注意2被加入了两次
s.size // 2
s.has(1) // true
s.has(2) // true
s.has(3) // false
s.delete(2);
s.has(2) // fals
遍历操作
Set结构的实例有四个遍历方法,可以用于遍历成员。
keys()
:返回键名的遍历器values()
:返回键值的遍历器entries()
:返回键值对的遍历器forEach()
:使用回调函数遍历每个成员
(1)keys()
,values()
,entries()
- 返回的都是遍历器对象
keys
方法和values
方法的行为完全一致。
(2)forEach()
Set结构的实例的forEach
方法,用于对每个成员执行某种操作,没有返回值。
(3)遍历的应用
扩展运算符(...
)内部使用for...of
循环,所以也可以用于Set结构。
let set = new Set(['red', 'green', 'blue']);
let arr = [...set];
// ['red', 'green', 'blue']
扩展运算符和Set结构相结合,就可以去除数组的重复成员。
let arr = [3, 5, 2, 2, 5, 5];
let unique = [...new Set(arr)];
// [3, 5, 2]
数组的map
和filter
方法也可以用于Set了。
let set = new Set([1, 2, 3]);
set = new Set([...set].map(x => x * 2));
// 返回Set结构:{2, 4, 6}
let set = new Set([1, 2, 3, 4, 5]);
set = new Set([...set].filter(x => (x % 2) == 0));
// 返回Set结构:{2, 4}
WeakSet
不重复的值的集合,
与Set有两个区别
- WeakSet的成员只能是对象,而不能是其他类型的值。
- WeakSet中的对象都是弱引用,即垃圾回收机制不考虑WeakSet对该对象的引用,WeakSet是不可遍历的
Map
Map实例的属性和操作方法
(1)size属性
size
属性返回Map结构的成员总数。
let map = new Map();
map.set('foo', true);
map.set('bar', false);
map.size // 2
(2)set(key, value)
set
方法设置key
所对应的键值,然后返回整个Map结构。如果key
已经有值,则键值会被更新,否则就新生成该键。
var m = new Map();
m.set("edition", 6) // 键是字符串
m.set(262, "standard") // 键是数值
m.set(undefined, "nah") // 键是undefined
set
方法返回的是Map本身,因此可以采用链式写法。
let map = new Map()
.set(1, 'a')
.set(2, 'b')
.set(3, 'c');
(3)get(key)
get
方法读取key
对应的键值,如果找不到key
,返回undefined
。
var m = new Map();
var hello = function() {console.log("hello");}
m.set(hello, "Hello ES6!") // 键是函数
m.get(hello) // Hello ES6!
(4)has(key)
has
方法返回一个布尔值,表示某个键是否在Map数据结构中。
var m = new Map();
m.set("edition", 6);
m.set(262, "standard");
m.set(undefined, "nah");
m.has("edition") // true
m.has("years") // false
m.has(262) // true
m.has(undefined) // true
(5)delete(key)
delete
方法删除某个键,返回true。如果删除失败,返回false。
var m = new Map();
m.set(undefined, "nah");
m.has(undefined) // true
m.delete(undefined)
m.has(undefined) // false
(6)clear()
clear
方法清除所有成员,没有返回值。
let map = new Map();
map.set('foo', true);
map.set('bar', false);
map.size // 2
map.clear()
map.size // 0
遍历方法
Map原生提供三个遍历器生成函数和一个遍历方法。
keys()
:返回键名的遍历器。values()
:返回键值的遍历器。entries()
:返回所有成员的遍历器。forEach()
:遍历Map的所有成员。
与其他数据结构的互相转换
(1)Map转为数组
前面已经提过,Map转为数组最方便的方法,就是使用扩展运算符(…)。
let myMap = new Map().set(true, 7).set({foo: 3}, ['abc']);
[...myMap]
// [ [ true, 7 ], [ { foo: 3 }, [ 'abc' ] ] ]
(2)数组转为Map
将数组转入Map构造函数,就可以转为Map。
new Map([[true, 7], [{foo: 3}, ['abc']]])
// Map {true => 7, Object {foo: 3} => ['abc']}
(3)Map转为对象
如果所有Map的键都是字符串,它可以转为对象。
function strMapToObj(strMap) {
let obj = Object.create(null);
for (let [k,v] of strMap) {
obj[k] = v;
}
return obj;
}
let myMap = new Map().set('yes', true).set('no', false);
strMapToObj(myMap)
// { yes: true, no: false }
(4)对象转为Map
function objToStrMap(obj) {
let strMap = new Map();
for (let k of Object.keys(obj)) {
strMap.set(k, obj[k]);
}
return strMap;
}
objToStrMap({yes: true, no: false})
// [ [ 'yes', true ], [ 'no', false ] ]
(5)Map转为JSON
Map转为JSON要区分两种情况。一种情况是,Map的键名都是字符串,这时可以选择转为对象JSON。
function strMapToJson(strMap) {
return JSON.stringify(strMapToObj(strMap));
}
let myMap = new Map().set('yes', true).set('no', false);
strMapToJson(myMap)
// '{"yes":true,"no":false}'
另一种情况是,Map的键名有非字符串,这时可以选择转为数组JSON。
function mapToArrayJson(map) {
return JSON.stringify([...map]);
}
let myMap = new Map().set(true, 7).set({foo: 3}, ['abc']);
mapToArrayJson(myMap)
// '[[true,7],[{"foo":3},["abc"]]]'
(6)JSON转为Map
JSON转为Map,正常情况下,所有键名都是字符串。
function jsonToStrMap(jsonStr) {
return objToStrMap(JSON.parse(jsonStr));
}
jsonToStrMap('{"yes":true,"no":false}')
// Map {'yes' => true, 'no' => false}
但是,有一种特殊情况,整个JSON就是一个数组,且每个数组成员本身,又是一个有两个成员的数组。这时,它可以一一对应地转为Map。这往往是数组转为JSON的逆操作。
function jsonToMap(jsonStr) {
return new Map(JSON.parse(jsonStr));
}
jsonToMap('[[true,7],[{"foo":3},["abc"]]]')
// Map {true => 7, Object {foo: 3} => ['abc']}