1.let 命令
基本用法
ES6 新增了let
命令,用来声明变量。它的用法类似于var
,但是所声明的变量,只在let命令所在的代码块内有效。
for
循环的计数器,就很合适使用let
命令。
另外,for
循环还有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域。
不存在变量提升
var
命令会发生”变量提升“现象,即变量可以在声明之前使用,值为undefined
。这种现象多多少少是有些奇怪的,按照一般的逻辑,变量应该在声明语句之后才可以使用。
为了纠正这种现象,let命令改变了语法行为,它所声明的变量一定要在声明后使用,否则报错。
暂时性死区
只要块级作用域内存在let命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响。
ES6 明确规定,如果区块中存在let
和const
命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。这样的设计是为了让大家养成良好的编程习惯,变量一定要在声明之后使用,否则就报错。
总之,在代码块内,使用let
命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone , TDZ ) 的区域内。该名称并未在 ECMAScript 规范中被明确命名,但经常被用于描述 let 或 const 声明的变量为何在声明处之前无法被访问。尽管这里使用的都是 let ,但替换为const 也会有相同情况。
不允许重复声明
let
不允许在相同作用域内,重复声明同一个变量。
循环中的块级绑定
开发者最需要使用变量的块级作用域的场景,或许就是在 for 循环内,也就是想让一次性的循环计数器仅能在循环内部使用。
上例中的变量 i 仅在 for 循环内部可用,一旦循环结束,该变量在任意位置都不可访问。
循环内的函数
长期以来, var 的特点使得循环变量在循环作用域之外仍然可被访问,于是在循环内创建函数就变得很有问题。考虑如下代码:
你原本可能预期这段代码会输出 0 到 9 的数值,但它却在同一行将数值 10 输出了十次。这是因为变量 i 在循环的每次迭代中都被共享了,意味着循环内创建的那些函数都拥有对于同一变量的引用。在循环结束后,变量 i 的值会是 10 ,因此当 console.log(i) 被调用时,
每次都打印出 10 。
为了修正这个问题,开发者在循环内使用立即调用函数表达式(IIFEs) ,以便在每次迭代中强制创建变量的一个新副本,示例如下:
使用 let 与 const 的块级绑定可以在ES6 中为你简化这个循环。
循环内的 let 声明
let 声明通过有效模仿上例中 IIFE 的作用而简化了循环。在每次迭代中,都会创建一个新的同名变量并对其进行初始化。这意味着你可以完全省略 IIFE 而获得预期的结果,就像这样:
变量i是let声明的,当前的i只在本轮循环有效,所以每一次循环的i其实都是一个新的变量。你可能会问,如果每一轮循环的变量i都是重新声明的,那它怎么知道上一轮循环的值,从而计算出本轮循环的值?这是因为 JavaScript 引擎内部会记住上一轮循环的值,初始化本轮的变量i时,就在上一轮循环的基础上进行计算。
另外,for循环还有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域。
2.const 命令
基本用法
const
声明一个只读的常量。一旦声明,常量的值就不能改变。
const
一旦声明变量,就必须立即初始化,不能留到以后赋值。
const
的作用域与let
命令相同:只在声明所在的块级作用域内有效。
const
命令声明的常量也是不提升,同样存在暂时性死区,只能在声明的位置后面使用。
const
声明的常量,也与let
一样不可重复声明。
本质
const
实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const
只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。
如果真的想将对象冻结,应该使用Object.freeze
方法。
ES6 声明变量的六种方法
ES5 只有两种声明变量的方法:var
命令和function
命令。ES6 除了添加let
和const
命令,另外两种声明变量的方法:import
命令和class
命令。所以,ES6 一共有 6 种声明变量的方法。
顶层对象的属性
顶层对象,在浏览器环境指的是window
对象,在 Node
指的是global
对象。ES5 之中,顶层对象的属性与全局变量是等价的。
顶层对象的属性与全局变量挂钩,被认为是 JavaScript 语言最大的设计败笔之一。这样的设计带来了几个很大的问题,首先是没法在编译时就报出变量未声明的错误,只有运行时才能知道(因为全局变量可能是顶层对象的属性创造的,而属性的创造是动态的);其次,程序员很容易不知不觉地就创建了全局变量(比如打字出错);最后,顶层对象的属性是到处可以读写的,这非常不利于模块化编程。另一方面,window
对象有实体含义,指的是浏览器的窗口对象,顶层对象是一个有实体含义的对象,也是不合适的。
ES6 为了改变这一点,一方面规定,为了保持兼容性,var
命令和function
命令声明的全局变量,依旧是顶层对象的属性;另一方面规定,let
命令、const
命令、class
命令声明的全局变量,不属于顶层对象的属性。也就是说,从 ES6 开始,全局变量将逐步与顶层对象的属性脱钩。
补充:对象与数组的字面量在 JS 中是最常用的两种表示法,并且感谢流行的 JSON 数据格式,它们已成为这门语言中的格外重要的部分。定义对象与数组非常普遍,定义之后就能有条不紊地从这些结构中提取出相关信息。为了简化提取信息的任务, ES6 新增了解构(destructuring )。
基本用法
ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)。
以前,为变量赋值,只能直接指定值。
let a = 1;
let b = 2;
let c = 3;
ES6 允许写成下面这样。
let [a, b, c] = [1, 2, 3];
上面代码表示,可以从数组中提取值,按照对应位置,对变量赋值。
本质上,这种写法属于“模式匹配”,只要等号两边的模式相同,左边的变量就会被赋予对应的值。
如果解构不成功,变量的值就等于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 = true] = [];
foo // true
let [x, y = 'b'] = ['a']; // x='a', y='b'
对象的解构赋值
解构不仅可以用于数组,还可以用于对象。
let { foo, bar } = { foo: "aaa", bar: "bbb" };
foo // "aaa"
bar // "bbb"
对象的解构与数组有一个重要的不同。数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。
let { bar, foo } = { foo: "aaa", bar: "bbb" };
foo // "aaa"
bar // "bbb"
let { baz } = { foo: "aaa", bar: "bbb" };
baz // undefined
如果变量名与属性名不一致,必须写成下面这样。
let { foo: baz } = { foo: 'aaa', bar: 'bbb' };
这实际上说明,对象的解构赋值是下面形式的简写。
let { foo: foo, bar: bar } = { foo: "aaa", bar: "bbb" };
也就是说,对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。真正被赋值的是后者,而不是前者。
字符串的解构赋值
字符串也可以解构赋值。这是因为此时,字符串被转换成了一个类似数组的对象。
const [a, b, c, d, e] = 'hello';
a // "h"
b // "e"
c // "l"
d // "l"
e // "o"
函数参数的解构赋值
函数的参数也可以使用解构赋值。
function add([x, y]){
return x + y;}
add([1, 2]); // 3
上面代码中,函数add
的参数表面上是一个数组,但在传入参数的那一刻,数组参数就被解构成变量x
和y
。对于函数内部的代码来说,它们能感受到的参数就是x
和y
。
函数参数的解构也可以使用默认值。
用途
变量的解构赋值用途很多。
(1)交换变量的值
let x = 1;let y = 2;
[x, y] = [y, x];
上面代码交换变量x
和y
的值,这样的写法不仅简洁,而且易读,语义非常清晰。
(2)从函数返回多个值
函数只能返回一个值,如果要返回多个值,只能将它们放在数组或对象里返回。有了解构赋值,取出这些值就非常方便。
(3)函数参数的定义
解构赋值可以方便地将一组参数与变量名对应起来。
// 参数是一组有次序的值function f([x, y, z]) { ... }f([1, 2, 3]);
// 参数是一组无次序的值function f({x, y, z}) { ... }f({z: 3, y: 2, x: 1});
(4)提取 JSON 数据
解构赋值对提取 JSON 对象中的数据,尤其有用。
let jsonData = {
id: 42,
status: "OK",
data: [867, 5309]};
let { id, status, data: number } = jsonData;
console.log(id, status, number);
(5)函数参数的默认值
jQuery.ajax = function (url, {
async = true,
beforeSend = function () {},
cache = true,
complete = function () {},
crossDomain = false,
global = true,
// ... more config} = {}) {
// ... do stuff};
指定参数的默认值,就避免了在函数体内部再写var foo = config.foo || 'default foo'
;这样的语句。
函数参数的默认值
基本用法
ES6 之前,不能直接为函数的参数指定默认值,只能采用变通的方法。
function log(x, y) {
y = y || 'World';
console.log(x, y);}
log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello World
上面代码检查函数log
的参数y
有没有赋值,如果没有,则指定默认值为World
。这种写法的缺点在于,如果参数y赋值了,但是对应的布尔值为false
,则该赋值不起作用。就像上面代码的最后一行,参数y
等于空字符,结果被改为默认值。
为了避免这个问题,通常需要先判断一下参数y
是否被赋值,如果没有,再等于默认值。
if (typeof y === 'undefined') {
y = 'World';}
ES6 允许为函数的参数设置默认值,即直接写在参数定义的后面。
function log(x, y = 'World') {
console.log(x, y);}
log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello
可以看到,ES6 的写法比 ES5 简洁许多,而且非常自然。
除了简洁,ES6 的写法还有两个好处:首先,阅读代码的人,可以立刻意识到哪些参数是可以省略的,不用查看函数体或文档;其次,有利于将来的代码优化,即使未来的版本在对外接口中,彻底拿掉这个参数,也不会导致以前的代码无法运行。
function foo({x, y = 5} = {}) {
console.log(x, y);}
foo() // undefined 5
上面代码指定,如果没有提供参数,函数foo
的参数默认为一个空对象。
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
上面代码的add
函数是一个求和函数,利用 rest 参数,可以向该函数传入任意数目的参数。
rest 参数之后不能再有其他参数(即只能是最后一个参数),否则会报错。
箭头函数
基本用法
ES6 允许使用“箭头”(=>
)定义函数。
var f = v => v;
// 等同于
var f = function (v) {
return v;};
如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分。
var f = () => 5;
// 等同于
var f = function () { return 5 };
var sum = (num1, num2) => num1 + num2;
// 等同于
var sum = function(num1, num2) {
return num1 + num2;};
如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用return
语句返回。
var sum = (num1, num2) => { return num1 + num2; }
由于大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号,否则会报错。
/ 报错
let getTempItem = id => { id: id, name: "Temp" };
// 不报错
let getTempItem = id => ({ id: id, name: "Temp" });
下面是一种特殊情况,虽然可以运行,但会得到错误的结果。
let foo = () => { a: 1 };foo() // undefined
上面代码中,原始意图是返回一个对象{ a: 1 }
,但是由于引擎认为大括号是代码块,所以执行了一行语句a: 1
。这时,a
可以被解释为语句的标签,因此实际执行的语句是1
;,然后函数就结束了,没有返回值。
如果箭头函数只有一行语句,且不需要返回值,可以采用下面的写法,就不用写大括号了。
let fn = () => void doesNotReturn();
箭头函数可以与变量解构结合使用。
const full = ({ first, last }) => first + ' ' + last;
// 等同于function full(person) {
return person.first + ' ' + person.last;}
箭头函数使得表达更加简洁。
const isEven = n => n % 2 == 0;
const square = n => n * n;
上面代码只用了两行,就定义了两个简单的工具函数。如果不用箭头函数,可能就要占用多行,而且还不如现在这样写醒目。
箭头函数的一个用处是简化回调函数。
// 正常函数写法
[1,2,3].map(function (x) {
return x * x;});
// 箭头函数写法
[1,2,3].map(x => x * x);
另一个例子是
// 正常函数写法var result = values.sort(function (a, b) {
return a - b;});
// 箭头函数写法var result = values.sort((a, b) => a - b);
使用注意点
箭头函数有几个使用注意点。
(1)函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。
(2)不可以当作构造函数,也就是说,不可以使用new
命令,否则会抛出一个错误。
(3)不可以使用arguments
对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
(4)不可以使用yield
命令,因此箭头函数不能用作 Generator 函数。
上面四点中,第一点尤其值得注意。this
对象的指向是可变的,但是在箭头函数中,它是固定的。
字符串的遍历器接口
ES6 为字符串添加了遍历器接口,使得字符串可以被for...of
循环遍历。
for (let codePoint of 'foo') {
console.log(codePoint)
}
// "f"
// "o"
// "o"
includes(), startsWith(), endsWith()
传统上,JavaScript 只有indexOf
方法,可以用来确定一个字符串是否包含在另一个字符串中。ES6 又提供了三种新方法。
let 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) // ""
padStart(),padEnd()
ES2017 引入了字符串补全长度的功能。如果某个字符串不够指定长度,会在头部或尾部补全。padStart()
用于头部补全,padEnd()
用于尾部补全。
'x'.padStart(5, 'ab') // 'ababx'
'x'.padStart(4, 'ab') // 'abax'
'x'.padEnd(5, 'ab') // 'xabab'
'x'.padEnd(4, 'ab') // 'xaba'
模板字符串
传统的 JavaScript 语言,输出模板通常是这样写的(下面使用了 jQuery 的方法)。
$('#result').append(
'There are ' + basket.count + ' ' +
'items in your basket, ' +
'' + basket.onSale +
' are on sale!');
上面这种写法相当繁琐不方便,ES6 引入了模板字符串解决这个问题。
$('#result').append(`
There are ${basket.count} items
in your basket, ${basket.onSale}
are on sale!
`);
模板字符串(template string)是增强版的字符串,用反引号(`)标识。它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量。模板字符串中嵌入变量,需要将变量名写在${}之中。
// 普通字符串
`In JavaScript '\n' is a line-feed.`
// 多行字符串
`In JavaScript this is
not legal.`
console.log(`string text line 1
string text line 2`);
// 字符串中嵌入变量
let name = "Bob", time = "today";
`Hello ${name}, how are you ${time}?`
模板字符串之中还能调用函数。
function fn() {
return "Hello World";}
`foo ${fn()} bar`
// foo Hello World bar
属性的简洁表示法
ES6 允许直接写入变量和函数,作为对象的属性和方法。这样的书写更加简洁。
const foo = 'bar';
const baz = {foo};
baz // {foo: "bar"}
// 等同于const baz = {foo: foo};
方法的简写
除了属性简写,方法也可以简写。
const o = {
method() {
return "Hello!";
}};
// 等同于
const o = {
method: function() {
return "Hello!";
}};
这种写法用于函数的返回值,将会非常方便。
function getPoint() {
const x = 1;
const y = 10;
return {x, y};}
getPoint()
// {x:1, y:10}
1.set
基本用法
ES6 提供了新的数据结构 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
上面代码通过add
方法向 Set 结构加入成员,结果表明 Set 结构不会添加重复的值。
Set 函数可以接受一个数组(或者具有 iterable 接口的其他数据结构)作为参数,用来初始化。
const set = new Set([1, 2, 3, 4, 4]);
上面代码也展示了一种去除数组重复成员的方法。
Set 实例的属性和方法
Set 结构的实例有以下属性。
Set.prototype.constructor
:构造函数,默认就是Set函数。Set.prototype.size
:返回Set实例的成员总数。Set 实例的方法分为两大类:操作方法(用于操作数据)和遍历方法(用于遍历成员)。下面先介绍四个操作方法。
add(value)
:添加某个值,返回 Set 结构本身。delete(value)
:删除某个值,返回一个布尔值,表示删除是否成功。has(value)
:返回一个布尔值,表示该值是否为Set的成员。clear()
:清除所有成员,没有返回值。Array.from
方法可以将 Set 结构转为数组。遍历操作
Set 结构的实例有四个遍历方法,可以用于遍历成员。
keys()
:返回键名的遍历器values()
:返回键值的遍历器entries()
:返回键值对的遍历器forEach()
:使用回调函数遍历每个成员Set
的遍历顺序就是插入顺序。由于 Set 结构没有键名,只有键值(或者说键名和键值是同一个值),所以keys
方法和values
方法的行为完全一致。
entries
方法返回的遍历器,同时包括键名和键值,所以每次输出一个数组,它的两个成员完全相等。
Set 结构的实例默认可遍历,它的默认遍历器生成函数就是它的values
方法。
这意味着,可以省略values
方法,直接用for...of
循环遍历 Set。
2.Map
含义和基本用法
JavaScript 的对象(Object),本质上是键值对的集合(Hash 结构),但是传统上只能用字符串当作键。
这给它的使用带来了很大的限制。
ES6 提供了 Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应,是一种更完善的 Hash 结构实现。如果你需要“键值对”的数据结构,Map 比 Object 更合适。
实例的属性和操作方法
Map 结构的实例有以下属性和操作方法。
(1)size 属性
size属性返回 Map 结构的成员总数。
(2)set(key, value)
set
方法设置键名key对应的键值为value
,然后返回整个 Map
结构。如果key
已经有值,则键值会被更新,否则就新生成该键。
(3)get(key)
get
方法读取key
对应的键值,如果找不到key
,返回undefined
。
(4)has(key)
has
方法返回一个布尔值,表示某个键是否在当前 Map 对象之中。
(5)delete(key)
delete
方法删除某个键,返回true
。如果删除失败,返回false
。
(6)clear()
clear
方法清除所有成员,没有返回值。
遍历方法
Map 结构原生提供三个遍历器生成函数和一个遍历方法。
keys()
:返回键名的遍历器。values()
:返回键值的遍历器。entries()
:返回所有成员的遍历器。forEach()
:遍历 Map 的所有成员。需要特别注意的是,Map 的遍历顺序就是插入顺序。
Map 结构转为数组结构,比较快速的方法是使用扩展运算符(...
)。
结合数组的map
方法、filter
方法,可以实现 Map 的遍历和过滤(Map 本身没有map
和filter
方法)。
与其他数据结构的互相转换
(1)Map 转为对象
如果所有 Map 的键都是字符串,它可以无损地转为对象。
如果有非字符串的键名,那么这个键名会被转成字符串,再作为对象的键名。
function strMapToObj(strMap) {
let obj = Object.create(null);
for (let [k,v] of strMap) {
obj[k] = v;
}
return obj;}
const myMap = new Map()
.set('yes', true)
.set('no', false);strMapToObj(myMap)
// { yes: true, no: false }
(2)对象转为 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})
// Map {"yes" => true, "no" => false}
(3)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}'
(4)JSON 转为 Map
JSON 转为 Map,正常情况下,所有键名都是字符串。
JSON 转为 Map,正常情况下,所有键名都是字符串。
function jsonToStrMap(jsonStr) {
return objToStrMap(JSON.parse(jsonStr));}
jsonToStrMap('{"yes": true, "no": false}')
// Map {'yes' => true, 'no' => false}
Promise 的含义
Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise
对象。
所谓Promise
,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。
有了Promise
对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise
对象提供统一的接口,使得控制异步操作更加容易。
基本用法
ES6 规定,Promise
对象是一个构造函数,用来生成Promise
实例。
下面代码创造了一个Promise
实例。
const promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}});
Promise
构造函数接受一个函数作为参数,该函数的两个参数分别是resolve
和reject
。它们是两个函数,由 JavaScript 引擎提供,不用自己部署
Promise
对象代表一个异步操作,有三种状态:pending
(进行中)、fulfilled
(已成功)和rejected
(已失败)。
Promise
对象的状态改变,只有两种可能:从pending
变为fulfilled
和从pending
变为rejected
。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。
为了表述方便,后面的resolved
统一只指fulfilled
状态,不包含rejected
状态。
resolve
函数的作用是,将Promise
对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;reject
函数的作用是,将Promise
对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。
Promise
实例生成以后,可以用then
方法分别指定resolved
状态和rejected
状态的回调函数。
promise.then(function(value) {
// success}, function(error) {
// failure});
then
方法可以接受两个回调函数作为参数。第一个回调函数是Promise
对象的状态变为resolved
时调用,第二个回调函数是Promise
对象的状态变为rejected
时调用。其中,第二个函数是可选的,不一定要提供。这两个函数都接受Promise
对象传出的值作为参数。
function timeout(ms) {
return new Promise((resolve, reject) => {
setTimeout(resolve, ms, 'done');
});}
timeout(100).then((value) => {
console.log(value);});
Promise.prototype.then()
Promise 实例具有then
方法,也就是说,then
方法是定义在原型对象Promise.prototype
上的。它的作用是为 Promise 实例添加状态改变时的回调函数。前面说过,then
方法的第一个参数是resolved
状态的回调函数,第二个参数(可选)是rejected
状态的回调函数。
then
方法返回的是一个新的Promise
实例(注意,不是原来那个Promise
实例)。因此可以采用链式写法,即then
方法后面再调用另一个then
方法。
getJSON("/posts.json").then(function(json) {
return json.post;}).then(function(post) {
// ...});
上面的代码使用then
方法,依次指定了两个回调函数。第一个回调函数完成以后,会将返回结果作为参数,传入第二个回调函数.
采用链式的then
,可以指定一组按照次序调用的回调函数。这时,前一个回调函数,有可能返回的还是一个Promise
对象(即有异步操作),这时后一个回调函数,就会等待该Promise
对象的状态发生变化,才会被调用。
Promise.prototype.catch()
Promise.prototype.catch
方法是.then(null, rejection)
的别名,用于指定发生错误时的回调函数。
etJSON('/posts.json').then(function(posts) {
// ...}).catch(function(error) {
// 处理 getJSON 和 前一个回调函数运行时发生的错误 console.log('发生错误!', error);});
上面代码中,getJSON
方法返回一个 Promise 对象,如果该对象状态变为resolved
,则会调用then
方法指定的回调函数;如果异步操作抛出错误,状态就会变为rejected
,就会调用catch
方法指定的回调函数,处理这个错误。另外,then
方法指定的回调函数,如果运行中抛出错误,也会被catch
方法捕获。
Promise.prototype.finally()
Promise.all
方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。
const p = Promise.all([p1, p2, p3]);
上面代码中,不管promise
最后的状态,在执行完then
或catch
指定的回调函数以后,都会执行finally
方法指定的回调函数。
Promise.all()
Promise.all
方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。
const p = Promise.all([p1, p2, p3]);
Promise.all
方法接受一个数组作为参数,p1
、p2
、p3
都是 Promise 实例,如果不是,就会先调用下面讲到的Promise.resolve
方法,将参数转为 Promise 实例,再进一步处理。
p的状态由p1
、p2
、p3
决定,分成两种情况。
(1)只有p1
、p2
、p3
的状态都变成fulfilled
,p
的状态才会变成fulfilled
,此时p1
、p2
、p3
的返回值组成一个数组,传递给p
的回调函数。
(2)只要p1
、p2
、p3
之中有一个被rejected
,p
的状态就变成rejected
,此时第一个被reject
的实例的返回值,会传递给p的回调函数。
Promise.race()
Promise.race
方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。
const p = Promise.race([p1, p2, p3]);
上面代码中,只要p1
、p2
、p3
之中有一个实例率先改变状态,p
的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p
的回调函数。
概述
历史上,JavaScript 一直没有模块(module)体系,无法将一个大程序拆分成互相依赖的小文件,再用简单的方法拼装起来。其他语言都有这项功能,比如 Ruby 的require
、Python 的import
,甚至就连 CSS 都有@import
,但是 JavaScript 任何这方面的支持都没有,这对开发大型的、复杂的项目形成了巨大障碍。
在 ES6 之前,社区制定了一些模块加载方案,最主要的有 CommonJS 和 AMD 两种。前者用于服务器,后者用于浏览器。ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。
ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。
ES6 模块不是对象,而是通过export
命令显式指定输出的代码,再通过import
命令输入。
{ stat, exists, readFile } from 'fs';// ES6模块import
上面代码的实质是从fs
模块加载 3 个方法,其他方法不加载。这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载。
严格模式
ES6 的模块自动采用严格模式,不管你有没有在模块头部加上"use strict"
;。
严格模式主要有以下限制。
with
语句delete prop
,会报错,只能删除属性delete global[prop]
eval
不会在它的外层作用域引入变量eval
和arguments
不能被重新赋值arguments
不会自动反映函数参数的变化arguments.callee
arguments.caller
this
指向全局对象fn.caller
和fn.arguments
获取函数调用的堆栈protected
、static
和interface
)export 命令
模块功能主要由两个命令构成:export
和import
。export
命令用于规定模块的对外接口,import
命令用于输入其他模块提供的功能。
一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用export关键字输出该变量。下面是一个 JS 文件,里面使用export
命令输出变量。
// profile.jsexport var firstName = 'Michael';
export var lastName = 'Jackson';
export var year = 1958;
export
的写法,除了像上面这样,还有另外一种。
/ profile.jsvar firstName = 'Michael';var lastName = 'Jackson';var year = 1958;
export {firstName, lastName, year};
上面代码在export
命令后面,使用大括号指定所要输出的一组变量。它与前一种写法(直接放置在var
语句前)是等价的,但是应该优先考虑使用这种写法。因为这样就可以在脚本尾部,一眼看清楚输出了哪些变量。
export
命令除了输出变量,还可以输出函数或类(class)。
通常情况下,export
输出的变量就是本来的名字,但是可以使用as
关键字重命名。
function v1() { ... }function v2() { ... }
export {
v1 as streamV1,
v2 as streamV2,
v2 as streamLatestVersion};
import 命令
使用export
命令定义了模块的对外接口以后,其他 JS 文件就可以通过import
命令加载这个模块。
// main.jsimport {firstName as a, lastName, year} from './profile.js';
function setName(element) {
element.textContent = firstName + ' ' + lastName;}
上面代码的import
命令,用于加载profile.js
文件,并从中输入变量。import
命令接受一对大括号,里面指定要从其他模块导入的变量名。大括号里面的变量名,必须与被导入模块(profile.js
)对外接口的名称相同。
如果想为输入的变量重新取一个名字,import
命令要使用as
关键字,将输入的变量重命名。
import { lastName as surname } from './profile.js';
模块的整体加载
除了指定加载某个输出值,还可以使用整体加载,即用星号(*)指定一个对象,所有输出值都加载在这个对象上面。
import * as circle from './circle';
console.log('圆面积:' + circle.area(4));
console.log('圆周长:' + circle.circumference(14));
export default 命令
从前面的例子可以看出,使用import
命令的时候,用户需要知道所要加载的变量名或函数名,否则无法加载。但是,用户肯定希望快速上手,未必愿意阅读文档,去了解模块有哪些属性和方法。
为了给用户提供方便,让他们不用阅读文档就能加载模块,就要用到export default
命令,为模块指定默认输出。
// export-default.jsexport default function () {
console.log('foo');}
上面代码是一个模块文件export-default.js
,它的默认输出是一个函数。
其他模块加载该模块时,import
命令可以为该匿名函数指定任意名字。
import customName from './export-default';customName(); // 'foo' // import-default.js
上面代码的import
命令,可以用任意名称指向export-default.js
输出的方法,这时就不需要知道原模块输出的函数名。需要注意的是,这时import
命令后面,不使用大括号。
一个模块只能有一个默认输出,因此export default
命令只能使用一次。所以,import命令后面才不用加大括号,因为只可能唯一对应export default
命令。
跨模块常量 § ⇧
// constants.js 模块export const A = 1;
export const B = 3;
export const C = 4;
// test1.js 模块import * as constants from './constants';
console.log(constants.A); // 1console.log(constants.B); // 3
// test2.js 模块import {A, B} from './constants';
console.log(A); // 1console.log(B); // 3
在ES3之前js代码执行的过程中,一旦出现错误,整个js代码都会停止执行,这样就显的代码非常的不健壮。
在Java或C#等一些高级语言中,都提供了异常处理机制,可以处理出现的异常,而不会停止整个应用程序。
从ES3开始,js也提供了类似的异常处理机制,从而让js代码变的更健壮,及时执行的过程中出现了异常,也可以让程序具有了一部分的异常恢复能力。
Error具有下面一些主要属性:
一、Javascript的异常捕获机制
1.1 基本的try…catch语句
ES3开始引入了 try-catch 语句,是 JavaScript 中处理异常的标准方式。
语法:
try{
//可能发生异常的代码
}catch(error){
//发生错误执行的代码
}
说明:
**1.**把有可能出的问题的代码放在 try 语句中。try语句中可以理论上可以写任何的代码,只要有一行代码出现问题,整个程序的执行流程就会立即调到catch语句中执行。
**2.**一旦try中有一行代码发生异常,则这行出错代码的后面的try中的其他语句都不会再执行。比如上面代码中的console.log(b);这行代码会出错,则立即去执行catch中的代码。所以console.log(“我不会输出的,不要找了”)这行代码则不会再执行
**3.**在执行catch中的代码之前,js引擎会首先根据错误类型自动创建一个错误,并通过catch后面的参数传递到catch中。不同的浏览器创建的error对象不一样,但是同创他们都包含一个message属性,值是这个错误的一些信息。
**4.**catch中的代码执行完毕之后,会继续执行后面的代码,程序不会停止下来。
1.2 finally语句
在 try…catch 中,try 中一旦出现错误则其他语句不能执行,如果不出现错误则 catch 中的语句不会执行。
Javascript 参考其他编程语言,也提供了一种 finally 语句:不管 try 中的语句有没有错误,在最后都会执行 finally 中的语句。
即:try 中语句不发生错误执行完毕后会执行 finally 中的语句,try 中的语句发生错误,则执行 catch中的语句,catch 中的语句执行完毕后也会执行 finally 中的语句。
语法:
try{
}catch(error){
}finally{
}
执行代码期间可能会发生的错误有多种类型。每种错误都有对应的错误类型,而当错误发生时,就 会抛出相应类型的错误对象。js共定义了下列 7 种错误类型:
说明:
1. Error类型是基本的错误类型,其他类型都继承自这个类型。
2. EvalError 类型的错误会在使用 eval()函数而发生异常时被抛出
3. TypeError 类型在变量中保存着意外的类型时,或者在访问不存在的 方法时,都会导致这种错误 。
4. 一般情况,不同的错误,处理方式不一样。可以参考下面的处理方式。不过在实际开发中,很多程序员并没有形成处理错误的习惯。
1.4 合理使用try…catch
当 try-catch 语句中发生错误时,浏览器会认为错误已经被处理了,浏览器就不再报告错误了。这也是最简单的一种情况。
使用 try-catch 最适合处理那些我们无法控制的错误。假设你在使用一个大型 JavaScript 库中的 函数,该函数可能会有意无意地抛出一些错误。由于我们不能修改这个库的源代码,所以大可将对该函 数的调用放在 try-catch 语句当中,一有什么错误发生,也好可以恰当地处理它们。
在明明知道自己的代码会发生错误时,再使用 try-catch 语句就不太合适了。例如,如果 传给函数的参数是字符串而非数值,就会造成函数出错,那么就应该先检查参数的类型,然后再决定 如何去做。在这种情况下,不应用使用 try-catch 语句。因为try…catch语句比较是比较好资源的事情。
二、throw主动抛出异常
2.1 抛出js内置错误类型的对象
在大部分的代码执行过程中,都是出现错误的时候,由浏览器(javascript引擎)抛出异常,然后程序或者停止执行,或被try…catch 捕获。
然而有时候我们在检测到一些不合理的情况发生的时候也可以主动抛出错误。
使用 throw 关键字抛出来主动抛出异常。
注意:
1. throw后面就是我们要抛出的异常对象。在以前的时候都是出现错误的时候浏览器抛出异常对象,只是现在是我们自己主动抛出的异常对象。
2. 只要有异常对象抛出,不管是浏览器抛出的,还是代码主动抛出,都会让程序停止执行。如果想让程序继续执行,则有也可以用try…catch来捕获。
3. 每一个错误类型都可以传入一个参数,表示实际的错误信息。
4. 我们可以在适当的时候抛出任何我们想抛出的异常类型。