ES6简介
ECMAScript 6.0(以下简称 ES6)是 JavaScript 语言的下一代标准,已经在 2015 年 6 月正式发布了,也叫ECMAScript 2015
块级作用域
ES6之前 只有全局作用域
和函数作用域
,没有块级作用域,带来很多不合理的场景。
function f() {
var a = 'hello'
}
f()
console.log(a) // error, a is not defined 变量a只在函数作用域中有效,a在块级作用域{}里面
第一种场景
内层变量可能会覆盖外层变量
var a = 'abc'
function f() {
console.log(a) // 预解析:就是在浏览器解析代码之前,把变量的声明和函数的声明提升到该作用域的最上面
if (false) {
var a = 'efg'
}
}
f(); // undefined
相当于
var a = 'abc'
function f() {
var a
console.log(a) // 变量声明了但没有赋值,结果是undefined
if(false) {
var a = 'efg'
}
}
第二种场景
用来计数的循环变量泄露为全局变量
for (var i = 0; i < 5; i++) {
// code
}
console.log(i) // 5 由于i没有采用块级作用域,那么i的作用域是全局的,打印结果是5
ES6块级作用域概念
(1)花括号 {} 和其中代码生成一个块。
(2)在块中,let和const声明的变量和常量对外都是不可见的,称之为块级作用域。
(3)只有使用let和const声明的变量或者常量在块中对外不可见,var声明的变量对外依然可见。
function f() {
let n = 5
if (true) {
let n = 10
}
console.log(n)
}
f() // 5
上面的函数有两个代码块,都声明了变量n,运行后输出 5,这表示外层代码块不受内层代码块的影响。如果两次都使用var定义变量n,最后输出的值才是 10。
function f() {
var n = 5
if (true) {
var n = 10
}
console.log(n)
}
f() // 10
块级作用域与函数声明
ES5 规定,函数只能在顶层作用域和函数作用域之中声明,不能在块级作用域声明。
// 情况一
if (true) {
function f() {}
}
// 情况二
try {
function f() {}
} catch(e) {
// ...
}
上面两种函数声明,根据 ES5 的规定都是非法的。
但是,浏览器没有遵守这个规定,为了兼容以前的旧代码,还是支持在块级作用域之中声明函数,因此上面两种情况实际都能运行,不会报错。
// 块级作用域内部的函数声明语句,建议不要使用
{
let a = 'abc'
function f() {
return a
}
}
// 块级作用域内部,优先使用函数表达式
{
let a = 'abc'
let f = function () {
return a
}
}
// 第一种写法,报错
if (true) let x = 1
// 第二种写法,不报错
if (true) { let x = 1 }
let 和 const
let
let 用来声明变量
{
let num = 1;
let str = 'hello world'
}
不可以重复声明
{
let a = 1;
let a = 2; // Uncaught SyntaxError: Identifier 'a' has already been declared
}
不存在变量提升
{
console.log(a); // Uncaught ReferenceError: Cannot access 'a' before initialization
let a = 2;
}
{
console.log(b); // undefined
var b = 1;
}
// 相当于
{
var b;
console.log(b); // undefined 变量声明了但没有赋值,结果是undefined
b = 10;
}
只在声明所在的块级作用域内有效
{
let a = 1;
console.log(a); // 1
var b = 2;
}
console.log(a); // error
console.log(b); // 2
const
const 用来声明常量(不可改,只读)
{
const PI = 3.14;
}
不可以重新赋值
{
const a = 1;
a = 2; // Uncaught TypeError: Assignment to constant variable.
}
声明的时候必须初始化(赋值)
{
const a;
a = 10; // Uncaught SyntaxError: Missing initializer in const declaration
}
不可以重复声明
{
const a = 10;
const a = 20; // Uncaught SyntaxError: Identifier 'a' has already been declared
}
不存在变量提升
{
console.log(a); // Uncaught ReferenceError: Cannot access 'a' before initialization
const a = 5;
}
只在声明的块级作用域内有效
{
const a = 10;
console.log(a); // 10
}
console.log(a); // Uncaught ReferenceError: a is not defined
复合类型的数据(主要是对象和数组),可以这样子变动
{
const foo = {};
foo.name = 'demo';
// 将 foo 指向另一个对象,就会报错
foo = {}; // Uncaught TypeError: Assignment to constant variable
}
{
const bar = [];
bar.push(123);
}
区别
相同点:
1.都不存在变量提升
2.都只在声明的块级作用域内有效
不同点:
声明类型:let 声明变量, const 声明常量
赋值时机:let可以声明变量与给变量赋值分开,使用const声明常量的时候必须同时赋值,否则报错
推荐
// 对于 数值、字符串、布尔值 经常会变的,用 let 声明
{
let num = 1;
let str = 'demo';
let flag = true;
}
// 对象、数组和函数用 const 来声明
{
const obj = {};
const arr = [];
const fn = function() {}
}
// 如经常用到的导出 函数
export const fn = function() {}
解构赋值
数组解构赋值
一次性声明多个变量
let [a, b, c, d] = [1, 2, 3]
a // 1
b // 2
c // 3
d // undefined
结合扩展运算符
{
let a, b, c
[a, b, ...c] = [1, 2, 3, 4, 5]
console.log(a) // 1
console.log(b) // 2
console.log(c) // [3, 4, 5]
}
允许指定默认值
{
let a, b
[a, b, c = 3] = [1, 2]
console.log(a) // 1
console.log(b) // 2
console.log(c) // 3
}
应用场景
{
let a = 1;
let b = 2;
[a, b] = [b, a];
console.log(a, b); // 2,1 变量交换
}
对象解构赋值
数组中,变量的取值由它 排列的位置 决定;而对象中,变量必须与 属性 同名,才能取到正确的值。
let { a, b } = { a: 1, b: 2 };
a // 1
b // 2
let { a, b } = { b: 2, a: 1 };
a // 1
b // 2
let { c } = { a: 1, b: 2 };
c // undefined
{
let o = { a: 1, b: 2 };
let { a, b } = o;
a // 1
b // 2
}
{
let { a: 1, b: 2 } = { a: 3 };
a // 3
b // 2
}
允许指定默认值
let { x = 3 } = {};
x // 3
let { x, y = 5 } = { x: 1 };
x // 1
y // 5
应用场景
{
let data = {
tit:'abc',
sub:[{ tit:'a',dsc:'one' }]
}
let { tit:a, sub:[{ tit:b }] } = data;
console.log(a); // abc
console.log(b); // a
}
函数参数解构赋值
{
function f() {
return [1, 2]
}
let a, b;
[a, b] = f(); // 相当于 [a, b] = [1, 2]
console.log(a, b); // 1, 2
}
{
function f() {
return [1, 2, 3, 4, 5]
}
let a, b, c;
[a, , , b] = f(); // 相当于 [a, , , b] = [1, 2, 3, 4, 5]
console.log(a, b); // 1, 4
}
{
function f() {
return [1, 2, 3, 4, 5]
}
let a, b, c;
[a, ...b] = f(); // 相当于 [a, ...b] = [1, 2, 3, 4, 5]
console.log(a, b); // 1, [2, 3, 4, 5]
}
{
function f() {
return [1, 2, 3, 4, 5]
}
let a, b, c;
[a, , ...b] = f(); // [a, , ...b] = [1, 2, 3, 4, 5]
console.log(a, b); // 1, [3, 4, 5]
}
使用场景
从函数返回多个值
// 返回一个数组
{
function fn() {
let [a, b, c] = [1, 2, 3]
return [a, b, c]
}
let [a, b, c] = fn();
a // 1
b // 2
c // 3
}
// 返回一个对象
{
function fn() {
return {
foo: 1,
bar: 2
};
}
let { foo, bar } = fn();
foo // 1
bar // 2
}
// 其他
{
function fn() {
return {
foo: 1,
bar: 2
};
}
let a = fn();
a.foo // 1
a.bar // 2
}
函数参数的默认值
function fn (a = 1, b = 2){
return a + b;
}
fn() // 3 默认 a = 1, b = 2
fn(3) // 5 因为 a = 3, b = 2
fn(3,3) // 6 因为 a = 3, b = 3
输入模块的指定方法
加载模块时,往往需要指定输入哪些方法,解构赋值使得输入语句非常清晰
const { SourceMapConsumer, SourceNode } = require("source-map");
在 utils.js 中
export function A (){
console.log('A')
}
export function B (){
console.log('B')
}
export function C (){
console.log('C')
}
在组件中引用时
import { A, B, C } from "./utils.js"
A() // A
字符串解构
字符串也可以解构赋值,这是因为此时,字符串被转换成了一个类似数组的对象
const [a, b, c, d, e] = 'hello';
a // 'h'
b // 'e'
c // 'l'
d // 'l'
e // 'o'
字符串扩展
模板字符串
模板字符串
用反引号(`)标识。它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量。
传统的 JS
'There are <b>' + p.name + '</b> '
ES6模板字符串
`There are <b> ${p.name} </b>`
特性
定义多行字符串
// 原生写法
"abc '\n' 123456"
// ES6 模板字符串写法
`abc
123456`
// 上面两个方式都能输出
abc
123456
字符串中嵌套变量
原生JS写法
var a = 1;
var b = 2;
console.log('a+b=' + (a + b)); // a+b=3
var name = 'A';
var age = 18;
console.log('my name is '+ name + 'and my age is ' + age); // my name is A my age is 18
ES6 写法
// 字符串中嵌入变量 模板字符串中嵌入变量,需要将变量名写在 ${ } 之中,可放入表达式
var a = 1;
var b = 2;
console.log(`a+b=${a + b}`); // a+b=3
var name = 'A';
var age = 18;
console.log(`my name is ${name} and my age is ${age}`); // my name is A my age is 18
let obj = { a: 1, b: 2 };
`${obj.a + obj.b}` // '3'
let fn = (name) => `Hello ${name}!`;
fn('abc') // "Hello abc!"
可调用函数
// 可以调用函数
function fn() {
return 'abc';
}
`one ${fn()} one`
// one abc one
注意:如果在模板字符串中需要使用反引号,则前面要用反斜杠转义
let a = `\`Hello\` World!`; // `Hello` World!
字符串函数
-
includes()
:返回布尔值,表示是否找到了参数字符串。 -
startsWith()
:返回布尔值,表示参数字符串是否在原字符串的头部。 -
endsWith()
:返回布尔值,表示参数字符串是否在原字符串的尾部。
let s = 'Hello world!';
s.startsWith('Hello') // true
s.endsWith('!') // true
s.includes('o') // true
这三个方法都支持第二个参数,表示开始搜索的位置
let s = 'Hello world!';
s.startsWith('world', 6) // true
s.endsWith('Hello', 5) // true
s.includes('Hello', 6) // false
-
repeat()
字符串重复
{
let str = 'abc';
console.log(str.repeat(2));// abcabc
}
- 字符串填补(多用于日期:2019-09-01)
{
let str = '1';
console.log(str.padStart(2, '0')); // 01
console.log(str.padStart(3, '0')); // 001
console.log(str.padEnd(2, '0')); // 10
console.log(str.padEnd(3, '0')); // 100
}
函数的扩展
默认参数
ES6 之前,不能直接为函数的参数指定默认值,只能采用变通的方法
function f(a, b) {
b = b || 2;
console.log(a, b);
}
ES6 允许为函数的参数设置默认值,即直接写在参数定义的后面
function f(a, b = 2) {
console.log(a, b);
}
f(1) // 1 2
f(1, 3) // 1 3
function Point(a = 0, b = 0) {
this.a = a;
this.b = b;
}
const p = new Point();
p // { a: 0, b: 0 }
参数变量是默认声明的,所以不能用let
或const
再次声明
function f(a = 3) {
let a = 1; // error
const a = 2; // error
}
与解构赋值默认值结合使用
function f({a, b = 5}) {
console.log(a, b);
}
f({}) // undefined 5
f({a: 1}) // 1 5
f({a: 1, b: 2}) // 1 2
f() // error
function foo({a, b = 5} = {}) {
console.log(a, b);
}
foo() // undefined 5
上面代码指定,如果没有提供参数,函数foo
的参数默认为一个空对象。
参数默认值位置
通常情况下,定义了默认值的参数,应该是函数的尾参数。
// 例一
function f(x = 1, y) {
return [x, y];
}
f() // [1, undefined]
f(2) // [2, undefined])
f(, 1) // error
f(undefined, 1) // [1, 1]
// 例二
function f(x, y = 5, z) {
return [x, y, z];
}
f() // [undefined, 5, undefined]
f(1) // [1, 5, undefined]
f(1, ,2) // error
f(1, undefined, 2) // [1, 5, 2]
上面代码中,有默认值的参数都不是尾参数。这时,无法只省略该参数,而不省略它后面的参数,除非显式输入undefined
。
如果传入undefined
,将触发该参数等于默认值,null
则没有这个效果。
function foo(x = 5, y = 6) {
console.log(x, y);
}
foo(undefined, null)
// 5 null
上面代码中,x
参数对应undefined
,结果触发了默认值,y
参数等于null
,就没有触发默认值。
rest 参数
ES6 引入 rest
参数(形式为…变量名),用于获取函数的多余参数,这样就不需要使用 arguments 对象了。rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中
function add(...values) {
let sum = 0;
for (let val of values) {
sum += val;
}
return sum;
}
add(2, 5, 3) // 10
function add(a,...values) {
let sum = 0;
for (let val of values) {
sum += val;
}
return sum;
}
add(2, 5, 3)// 8 因为a=2, ...values=(5,3)
上面代码的 add 函数是一个求和函数,利用 rest 参数,可以向该函数传入任意数目的参数。
注意,rest 参数之后不能再有其他参数(即只能是最后一个参数),否则会报错。
// error
function f(a, ...b, c) {
// ...
}
箭头函数
特点
没有
this
、super
、arguments
不可以当作构造函数,也就不能通过
new
关键字调用没有原型属性
prototype
不可以改变
this
指向不支持重复的命名参数
箭头函数和传统函数一样都有一个name属性,这一点是不变的
注意
函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象
箭头函数未被提升,它们必须在使用前进行定义
使用 const 比使用 var 更安全,因为函数表达式始终是常量值
不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误
不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替
this指向的固定化,并不是因为箭头函数内部有绑定this的机制,实际原因是箭头函数根本没有自己的this,导致内部的this就是外层代码块的this。正是因为它没有this,所以也就不能用作构造函数
箭头函数和普通函数的区别
箭头函数相当于匿名函数,并且简化了函数定义。箭头函数有两种格式,一种只包含一个表达式,连{ ... }和return都省略掉了。还有一种可以包含多条语句,这时候就不能省略{ ... }和return。
相比普通函数,箭头函数有更简洁的语法
-
箭头函数不绑定this,会捕获其所在的上下文的this值,作为自己的this值
- 函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。
- 把动态this转换为静态this:长期以来,JavaScript 语言的this对象一直是一个令人头痛的问题,在对象方法中使用this,必须非常小心。箭头函数”绑定”this,很大程度上解决了这个困扰。
- 箭头函数可以让this指向固定化,这种特性很有利于封装回调函数。
- 原理: this指向的固定化,并不是因为箭头函数内部有绑定this的机制,实际原因是箭头函数根本没有自己的this,导致内部的this就是外层代码块的this。正是因为它没有this,所以也就不能用作构造函数。
-
箭头函数是匿名函数,不能作为构造函数,不可以使用new命令,否则会抛出一个错误。所以箭头函数也不具有new.target。
原因:构造函数的new都做了些什么?简单来说,分为四步
- JS内部首先会先生成一个对象;
- 再把函数中的this指向该对象;
- 然后执行构造函数中的语句;
- 最终返回该对象实例。
-
箭头函数不绑定arguments,取而代之用rest参数...解决;所以箭头函数也没有arguments.callee和arguments.caller
不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。想要在箭头函数中以类似数组的形式取得所有参数,可以利用展开运算符来接收参数,比如:
const testFunc = (...args)=>{ console.log(args) //数组形式输出参数 }
在 ECMAScript 6 之前的函数声明中,它们的参数都是“简单参数类型”的。在 ECMAScript 6 之后,凡是在参数声明中使用了缺省参数、剩余参数和模板参数之一的,都不再是“简单的”(non-simple parameters)。
在使用传统的简单参数时,只需要将调用该参数时传入的实际参数与参数对象(arguments)绑定就可以了;而使用“非简单参数”时,需要通过“初始器赋值”来完成名字与值的绑定。
两种绑定模式的区别在于:通常将实际参数与参数对象绑定时,只需要映射两个数组的下标即可,而“初始器赋值”需要通过名字来索引值(以实现绑定),因此一旦出现“重名参数”就无法处理了。 -
使用call()、apply()和bind()调用,对 this 没有什么影响
由于 this 已经在词法层面完成了绑定,通过 call()、 apply()、bind() 方法调用一个函数时,只传入了一个参数,对 this 并没有什么影响
箭头函数没有原型属性prototype
-
不能简单返回对象字面量
如果要直接返回对象时需要用小括号包起来,因为大括号被占用解释为代码块了
箭头函数的 this 永远指向其上下文的 this ,任何方法都改变不了其指向,如 call() , bind() , apply() ,可以说正是因为没有自己的this,才使其具备了以上介绍的大部分特点;
普通函数的this指向调用它的那个对象
语法
let f = () => 'hello'
// 等同于
let f = () => { return 'hello' }
// 等同于
let f = function() { return 'hello' }
当箭头函数只有一个参数时,就可以省略括号,直接写参数名
let f = a => a
// 等同于
let function f(a) {
return a
}
如果要传入两个或多个参数,则就需要带上括号
const sum = (a, b) => a + b
// 等同于
const sum = function(a, b) {
return a + b
};
如果箭头函数的代码块部分多于一条语句,就要使用 {}
将它们括起来,并且使用 return
语句返回。
const sum = (a, b) => {
console.log('hello')
return a + b;
}
sum(1, 2) // hello 3
由于大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号,否则会报错。
// 错误示例
let getObj = id => { id: id, name: 'A' }
// 正确示例
let getObj = id => ({ id: id, name: 'A' })
// 上面正确示例相当于
let getObj = function(id) {
return { id: id, name: 'A' }
}
箭头函数可以与变量解构结合使用。
const getName = ({ first, last }) => first + ' ' + last;
// 等同于
function getName(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 res = arr.sort(function (a, b) {
return a - b
})
// 箭头函数写法
var res = arr.sort((a, b) => a - b)
下面是 rest 参数与箭头函数结合的例子
const f = (...a) => a;
f(1, 2, 3, 4, 5)
// [1, 2, 3, 4, 5]
const f = (a, ...b) => [a, b];
fn(1, 2, 3, 4, 5)
// [1, [2, 3, 4, 5]]
注意: 函数体内的 this 对象,就是定义时所在的对象,而不是使用时所在的对象
this
对象的指向是可变的,但是在箭头函数中,它是固定的
function foo() {
setTimeout(() => {
console.log('id:', this.id);
}, 100);
}
let id = 21;
foo.call({ id: 42 });
// id: 42
上面代码中,setTimeout 的参数是一个箭头函数,这个箭头函数的定义生效是在 foo 函数生成时,而它的真正执行要等到 100 毫秒后。如果是普通函数,执行时 this 应该指向全局对象window,这时应该输出 21。但是,箭头函数导致 this 总是指向函数定义生效时所在的对象(本例是{ id: 42}),所以输出的是 42。
箭头函数没有this
箭头函数的this
值,取决于函数外部非箭头函数的this
值,如果上一层还是箭头函数,那就继续往上找,如果找不到那么this
就是window
对象
let person = {
f1: () => {
console.log(this)
},
f2() {
return () => {
console.log(this)
}
}
}
person.f1() // window
person.f2()() // person对象
箭头函数没有arguments对象
同样箭头函数也没有arguments
对象,但是如果它外层还有一层非箭头函数的话,就会去找外层的函数的arguments
对象, 如下
let test1 = () => console.log(arguments) // 执行该函数会抛出错误
function test2(a, b, c) {
return () => {
console.log(arguments) // [1, 2, 3]
}
}
test2(1, 2, 3)()
可以清楚的看到当前的箭头函数没有arguments
对象,然而就去它的外层去找非箭头函数的函数。注意:箭头函数找arguments对象只会找外层非箭头函数的函数,如果外层是一个非箭头函数的函数如果它也没有arguments对象也会中断返回,就不会在往外层去找了
function test(a) {
return function() {
return () => {
console.log(arguments) // []
}
}
}
test(1)()()
上面示例中可以看到,里面的箭头函数往外层找非箭头函数的函数,然后不管外层这个函数有没有arguments
对象都会返回。只要它是非箭头函数就可以,如果外层是箭头函数还会继续往外层找
箭头函数不能用new
关键字声明
let test = () => {}
new test() // 抛出错误,找不到constructor对象
箭头函数没有原型prototype
箭头函数没有原型,有可能面试官会问,JavaScript
中所有的函数都有prototype
属性吗
let test = () => {}
test.prototype // undefined
箭头函数不能改变this
指向
let person = {}
let test = () => console.log(this)
test.bind(person)()
test.call(person)
test.apply(person)
上面示例中,改变this
指向的方法都不会抛出错误,但是都无效,都不能改变this
指向
箭头函数不能重复命名参数
let sum = (a, a) => {} // 抛出错误,参数不能重复
数组的扩展
扩展运算符(spread)是三个点(…),它好比 rest 参数的逆运算,将一个数组转为用逗号分隔的参数序列
扩展运算符
扩展运算符
console.log(...[1, 2, 3]) // 1 2 3
console.log(1, ...[2, 3, 4], 5) // 1 2 3 4 5
注意,只有函数用时,扩展运算符才可以放在圆括号中
(...[1, 2]) // error
数组合并
const a = [1, 2, 3];
const b = [4, 5, 6];
// ES5 的合并数组
a.concat(b); // [1, 2, 3, 4, 5, 6]
// ES6 的合并数组
[...a, ...b] // [1, 2, 3, 4, 5, 6]
函数调用
function add(x, y) {
return x + y;
}
const a = [2, 3];
add(...a) // 5
**替代 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);
// ES5 的写法
Math.max.apply(null, [9, 3, 18])
// ES6 的写法
Math.max(...[9, 3, 18])
// 等同于
Math.max(9, 3, 18);
将一个数组添加到另一个数组的尾部。
// 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);
与解构赋值结合
const [a, ...b] = [1, 2, 3];
a // 1
b // [2, 3]
const [a, ...b] = [];
a // undefined
b // []
const [a, ...b] = ["foo"];
a // "foo"
b // []
如果将扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报错
const [...a, b] = [1, 2, 3]; // error
const [a, ...b] = [1, 2, 3]; // ok
复制数组
const a = [1, 2];
const b = a;
b[0] = 2;
a // [2, 2]
以下两种写法,不会修改原来的数组
const a = [1, 2];
// 写法一
const b = [...a];
// 写法二
const [...b] = a;
b[0] = 2;
a // [1, 2]
不过,是浅拷贝,使用的时候需要注意
将字符串转为真正的数组
[...'hello']
// [ "h", "e", "l", "l", "o" ]
数组函数
遍历
用 for…of 循环进行遍历
keys()
是对键名的遍历
values()
是对值的遍历
entries()
是对键值对的遍历
for (let index of [1, 2].keys()) {
console.log(index);
}
// 0
// 1
for (let val of [1, 2].values()) {
console.log(val);
}
// 1
// 2
for (let [index, val] of [1, 2].entries()) {
console.log(index, val);
}
// 0 1
// 1 2
includes()
方法返回一个布尔值,表示某个数组是否包含给定的值,与字符串的 includes 方法类似。ES2016 引入了该方法
[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
find() 和 findIndex()
[1, 2, 3, 4, 5].find(function(item){
return item > 3;
}) // 4 -可以看到只返回一个值
[1, 2, 3, 4, 5].findIndex(function(item){
return item > 3;
}) // 3 -返回索引
[1, 2, 3, 4, 5].find(function(value, index, arr) {
return value > 9;
}) // -1 -所有成员都不符合条件,则返回-1
两个方法都可以发现NaN
,弥补了数组的indexOf
方法的不足。
[NaN].indexOf(NaN) // -1
[NaN].findIndex(y => Object.is(NaN, y)) // 0
Array.of()
Array.of
方法用于将一组值,转换为数组。
{
let arr = Array.of(1, 2, 3); // [1, 2, 3]
let arr = Array.of(3) // [3]
let arr = Array.of(); // []
let arr = Array.of(undefined) // [undefined]
}
与Array()
的区别
Array() // []
Array(3) // [, , ,]
Array(1, 2, 3) // [1, 2, 3]
Array(undefined) // [undefined]
对象的扩展
属性简写
const foo = 'bar';
const baz = {foo};
console.log(baz) // {foo: "bar"}
// 等同于
const baz = {foo: foo};
上面代码中,变量foo
直接写在大括号里面。这时,属性名就是变量名
, 属性值就是变量值
let a = 1;
let b = 2;
const es5 = {
a: a
b: b
}
const es6 = {
a,
b
}
es6 // {a: 1, b: 2}
function f(a, b) {
return {a, b};
}
// 等同于
function f(a, b) {
return {a: a, b: b};
}
f(1, 2) // {a: 1, b: 2}
方法简写
const es6 = {
say() {
return "hello wrold";
}
};
// 等同于
const es5 = {
say: function() {
return "hello wrold";
}
};
属性表达式
{
let a = 'b';
let es5 = {
a:'c'
b:'c'
};
let es6 = {
[a]:'c' //相当于上面b:'c'
}
}
扩展运算符
let { a, b, ...c } = { a: 1, b: 2, c: 3, d: 4 };
a // 1
b // 2
c // { c: 3, d: 4 }
由于解构赋值要求等号右边是一个对象,所以如果等号右边是undefined
或null
,就会报错,因为它们无法转为对象。
let { ...z } = null; // error
let { ...z } = undefined; // error
解构赋值必须是最后一个参数,否则会报错。
let { ...x, y, z } = obj; // error
let { x, ...y, ...z } = obj; // error
注意,解构赋值的拷贝是浅拷贝,即如果一个键的值是复合类型的值(数组、对象、函数)、那么解构赋值拷贝的是这个值的引用,而不是这个值的副本。
let obj = { a: { b: 1 } };
let { ...x } = obj;
obj.a.b = 2;
x.a.b // 2
对象API
判断两个字符串是否相等
console.log(Object.is('abc', 'abc'), 'abc' === 'abc');//true true
判断两个数组是否相等
console.log(Object.is([], []), [] === []); // false false 两个数组引用的是两个不同的地址
拷贝对象
console.log(Object.assign({a:a}, {b:b})); // {a:a, b:b} 浅拷贝
遍历对象
{
let test = {k:123, o:456};
for(let [key,value] of Object.entries(test)) {
console.log([key,value]);
}
}
Object.assign()
Object.assign方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)
const target = { a: 1 };
const source1 = { b: 2 };
const source2 = { c: 3 };
Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}
Object.assign方法的第一个参数是目标对象,后面的参数都是源对象。
注意,如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性。
const target = { a: 1, b: 1 };
const source1 = { b: 2, c: 2 };
const source2 = { c: 3 };
Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}
Object.assign 方法实行的是浅拷贝,而不是深拷贝
const obj1 = {a: {b: 1}};
const obj2 = Object.assign({}, obj1);
obj1.a.b = 2; // 修改b
obj2.a.b // 2, 也改变了
上面代码中,源对象 obj1 的 a 属性的值是一个对象,Object.assign 拷贝得到的是这个对象的引用。这个对象的任何变化,都会反映到目标对象上面
数值的扩展
指数运算符
ES2016 新增了一个指数运算符(**)
2 ** 2 // 4
2 ** 3 // 8
2 ** 3 **2 // 512 相当于=> 2 ** (3 ** 2)
这个运算符的一个特点是右结合,而不是常见的左结合。多个指数运算符连用时,是从最右边开始计算的
指数运算符可以与等号结合,形成一个新的赋值运算符(**=)
let a = 1.5;
a **= 2;
// 等同于 a = a * a;
let b = 4;
b **= 3;
// 等同于 b = b * b * b;
Set
ES6 提供了新的数据结构 Set,它类似于数组,但是成员的值都是唯一的,没有重复的值
Set 本身是一个构造函数,用来生成 Set 数据结构(既然是构造函数,肯定需要new)
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
去除数组的重复成员
const set = new Set([1, 2, 3, 4, 4]);
[...set]
// [1, 2, 3, 4]
或者
const array = [1, 1, 2, 3, 4, 4]
[...new Set(array)]
// [1, 2, 3, 4]
Promise 教程
Promise 实例对象
Promise 是异步编程的一种解决方案 ,比传统的解决方案(回调函数和事件)更合理和更强大
所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果
Promise对象特点
(1)Promise对象可以保存异步操作的结果
(2)Promise异步操作具有三种状态,Pending、Resolved 和 Rejected
(3)Promise对象状态的改变只存在两种情况,Pending 到 Resolved 或者 Pending 到 Rejected
(4)Promise对象的状态一旦确定,那么就无法改变,要么是 Resolved,要么是 Rejected
特别说明:Pending表示等待状态,Resolved表示处于完成状态,Rejected处于未完成状态
既然Promise创建的实例对象,是一个异步操作,那么异步操作的结果,只能有两种状态:
状态1:异步执行成功,需要在内部调用成功的回调函数`resolve`把结果返回给调用者
状态2:异步操作失败,需要在内部调用失败的回调函数`reject` 把结果返回给调用者
基本用法
let p = new Promise((resolve, reject) => {
if (异步操作成功) {
resolve(value) // 异步操作成功,就会调用这个回调函数,并把异步成功的结果当做参数
} else {
reject(error) // 异步操作失败,就会调用这个回调函数,并把异步失败的结果当做参数
}
})
简单分析:
(1)通过构造函数new Promise()
可以创建一个Promise对象实例,构造函数的参数是一个回调函数
(2)构造函数的回调函数具有两个函数参数,由引擎提供,也就是不需要我们提供
(3)执行resolve函数,那么状态变为Resolved,执行reject函数,状态变为Rejected
(4)构造函数调用后,回调函数会立马执行,然后根据调用的是resolve还是reject函数,确定状态
(5)状态确定后,再利用then方法执行对应的操作
p.then((value) => {
console.log(value) // 这个value就是resolve(value)的参数value
}, (error) => {
console.log(error) // 这个error就是reject(error)的参数error
});
简单分析:
(1).then方法的参数是两个回调函数
(2)如果p处于Resolved完成状态,那么执行第一个回调函数,如果处于Rejected状态,那么执行第二个回调函数
(3)回调函数中的参数value,分别是传递给resolve和reject函数的参数
(4) 第二回调函数是可以省略的
Promise示例
const p = new Promise(function(resolve, reject) {
setTimeout(function() {
resolve('hello')
}, 5000)
})
p.then(function(value) {
console.log(value)
})
// 5秒后输出 hello
function getHello(ms) {
return new Promise((resolve, reject) => {
setTimeout(resolve, ms, 'hello') // setTimeout(函数, 延时时间ms, 参数)
})
}
getHello(5000).then((value) => { // 5s后输出hello getHello(5000)=>返回一个实例对象
console.log(value)
})
// 5秒后输出 hello
const p = new Promise((resolve, reject) => {
console.log(1);
resolve();
console.log(2)
});
p.then(function() {
console.log(3);
});
console.log(4);
// 1 2 4 3
上面代码中, Promise 新建后立即执行 ,首先输出1,然后,then方法指定的回调函数,将在当前脚本所有同步任务执行完才会执行,所以resolved最后输出。
resolve回调函数的参数除了正常的值以外,还可以返回另外一个Promise实例对象
const p1 = new Promise((res, rej) => {
setTimeout(() => rej(new Error('失败')), 3000)
})
const p2 = new Promise((res, rej) => {
setTimeout(() => res(p1), 1000)
})
p2.then(value => console.log(value), error => console.log(error)) // 3s后输出 Error: 失败
上面代码中,p1是一个 Promise,3 秒之后变为rejected。p2的状态在 1 秒之后改变,resolve方法返回的是p1。由于p2返回的是另一个 Promise,导致p2自己的状态无效了,由p1的状态决定p2的状态。所以,后面的then语句都变成针对后者(p1)。又过了 2 秒,p1变为rejected,导致触发catch方法指定的回调函数。
注意点
new Promise((resolve, reject) => {
resolve(1);
console.log(2);
}).then(v => {
console.log(v);
});
// 2
// 1
一般来说,调用resolve
或reject
以后,Promise 的使命就完成了,后继操作应该放到then
方法里面,而不应该直接写在resolve
或reject
的后面。所以,最好在它们前面加上return
语句,这样就不会有意外
new Promise((resolve, reject) => {
return resolve(1);
// 后面的语句不会执行
console.log(2);
})
.then() 方法
只要是Promise实例对象就能调用then方法
p.then(onResolved, onRejected)
// onResolved:必需,当Promise对象变为 Resolved 状态时的回调函数
// onRejected:可选,当Promise对象变为 Rejected 状态时的回调函数
Promise 实例对象具有then方法,也就是说,then方法是定义在原型对象Promise.prototype上的
调用then方法,预先为这个Promise异步操作,指定成功(resolve)和失败(reject)回调函数
let p = new Promise((resolve, reject) => {
if (true) {
resolve('resolve被调用');
} else {
reject('reject被调用');
}
});
p.then((res) => { console.log(res)}, (err) => { console.log(err) })
// resolve被调用
分析:
调用Promise构造函数后,其回调函数会立马被调用
通过if语句判断之后,resolve()会被调用,于是Promise对象状态变为Resolved
于是就会执行then的第一个回调函数,打印结果是"resolve被调用"
then()方法返回一个promise对象,所以可以使用链式调用方式
let p = new Promise((resolve, reject) => {
resolve('hello')
})
p.then((res) => {
console.log(res);
return `${res} world`
}).then((res) => {
console.log(res);
})
// hello
// hello world
分析:
上述代码中,第二个then()方法的回调函数的参数是上一个then回调函数的返回值
then方法返回的是一个新的Promise实例(注意,不是原来那个Promise实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法。
new Promise((res, rej) => {
setTimeout(res, 3000, 'hello')
}).then(v => v).then((r) => console.log(r)) // 3s后输出hello
上面代码,可以看出,第二then的成功回调函数参的数,其实就是第一个then成功回调函数return返回的值,也就是说第一个then返回的值当做第二then的参数
.catch() 方法
p.catch(onRejected);
// onRejected:必需,当 p 的状态变为 Rejected 时,此回调函数就会执行
Promise.prototype.catch()方法是
.then(null, reject)
或
.then(undefined, reject) 的别名,用于指定发生错误时的回调函数
下面代码中,
如果该对象状态变为resolved
,则会调用then()
方法指定的回调函数
如果异步操作抛出错误,状态就会变为rejected
,就会调用catch()
方法指定的回调函数,处理这个错误
另外,then()
方法指定的回调函数,如果运行中抛出错误,也会被catch()
方法捕获。
p
.then((value) => console.log('成功:', value))
.catch((error) => console.log('失败:', error))
上面代码等同于:
p
.then((value) => console.log('成功:', value))
.then(null, (error) => console.log('失败:', error))
示例
const p = new Promise((res, rej) => {
throw new Error('错误')
})
p.catch((error) => {
console.log(error)
})
// Error: 错误
上面代码中,promise
抛出一个错误,就被catch()
方法指定的回调函数捕获。注意,上面的写法与下面两种写法是等价的。
// 写法一
const p = new Promise((resolve, reject) => {
try {
throw new Error('错误')
} catch(e) {
reject(e)
}
})
p.catch((error) => {
console.log(error) // Error: 错误
})
// 写法二
const p = new Promise((resolve, reject) => {
reject(new Error('错误'))
})
p.catch((error) => {
console.log(error) // Error: 错误
})
比较上面两种写法,可以发现reject()
方法的作用,等同于抛出错误。
如果 Promise 状态已经变成resolved
,再抛出错误是无效的。
const p = new Promise((resolve, reject) => {
resolve('yes');
throw new Error('no');
});
p
.then((res) => { console.log(res) })
.catch((err) => { console.log(err) })
// yes
上面代码中,Promise 在resolve
语句后面,再抛出错误,不会被捕获,等于没有抛出。因为 Promise 的状态一旦改变,就永久保持该状态,不会再变了。
let p = new Promise(function(resolve, reject) {
if (false) {
resolve()
} else {
reject()
}
})
p
.then(function () {
console.log('异步成功')
})
.catch(function () {
console.log('异步失败')
})
// 异步失败
或者
let p = new Promise(function(resolve, reject) {
if (false) {
resolve()
} else {
reject()
}
})
p
.then(function () {
console.log('异步成功')
}, function() {
console.log('异步失败')
})
// 异步失败
或者
let p = new Promise(function(resolve, reject) {
if (false) {
resolve()
} else {
reject()
}
})
p
.then(function () {
console.log('异步成功')
})
.then(() => {}, () => { console.log('异步失败') })
// 异步失败
catch()可以捕获它之前Rejected状态变化,不必紧邻
let p = new Promise(function(resolve, reject) {
reject()
});
p.
then(function () {
//code
})
.then(function () {
//code
})
.catch(function () {
console.log("错误失败")
})
// 错误失败
.all() 方法
等待所有都完成(或第一个失败)
const p1 = Promise.resolve(3);
const p2 = 100;
const p3 = new Promise((resolve, reject) => {
setTimeout(resolve, 100, 'ok');
});
Promise.all([p1, p2, p3]).then((values) => {
console.log(values); // [3, 100, "ok"]
});
Promise.all 可以将多个Promise实例包装成一个新的Promise实例。同时,成功和失败的返回值是不同的,成功的时候返回的是一
个结果数组,而失败的时候则返回最先被reject失败状态的值
let p1 = new Promise((resolve, reject) => {
resolve('p1_success')
})
let p2 = new Promise((resolve, reject) => {
resolve('p2_success')
})
let p3 = Promise.reject('p3_fail')
Promise.all([p1, p2]).then((res) => {
console.log(res) // ['p1_success', 'p2_success']
}).catch((error) => {
console.log(err)
})
Promise.all([p1, p3 ,p2]).then((res) => {
console.log(res)
}).catch((err) => {
console.log(err) // 'p3_fail'
})
let wake = (time) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(`${time / 1000}秒后醒来`)
}, time)
})
}
let p1 = wake(3000)
let p2 = wake(2000)
Promise.all([p1, p2]).then((res) => {
console.log(res) // [ '3秒后醒来', '2秒后醒来' ]
}).catch((err) => {
console.log(err)
})
需要特别注意的是,Promise.all获得的成功结果的数组里面的数据顺序和Promise.all接收到的数组顺序是一致的,即p1的结果在前,即便p1的结果获取的比p2要晚。
.race() 方法
顾名思义,Promse.race就是赛跑的意思,意思就是说,Promise.race([p1, p2, p3])里面哪个结果获得的快,就返回那个结果,不管
结果本身是成功状态还是失败状态
let p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('success')
},1000)
})
let p2 = new Promise((resolve, reject) => {
setTimeout(() => {
reject('failed')
}, 500)
})
Promise.race([p1, p2]).then((res) => {
console.log(res)
}).catch((err) => {
console.log(err) // 打开的是 'failed'
})
解决回调地狱
function getFileByPath(path) {
return new Promise(function(resolve, reject) {
fs.readFile(path, 'utf-8', (error, data) => {
if(error) return reject(error)
resolve(data)
})
})
}
// .then先执行
getFileByPath('./files/02.txt').then(
function(data) {
console.log(data)
},
funtion(error) {
console.log(error)
})
解决回调地狱
getFileByPath('./files/01.txt')
.then(function(data) {
console.log(data)
return getFileByPath('./files/02 .txt')
})
.then(function(data2) {
console.log(data2)
return getFileByPath('./files/03 .txt')
})
.then(function(data3) {
console.log(data3)
})
如果前面的Promise执行失败,我们不想让后续的Promise被终止,可以为每个Promise指定失败回调
getFileByPath('./files/01.txt')
.then(function(data) {
console.log(data)
return getFileByPath('./files/02 .txt')
}, function(error) {
console.log(error)
return getFileByPath('./files/02 .txt')
})
.then(function(data2) {
console.log(data2)
})
有时候,我们有这样的需求,如果后续Promise执行,依赖于前面Promise执行的结果,如果前面失败了, 则后面的就没有执行下去的意义了,此时我们想要实现,一旦报错,则立即终止所有Promise的执行
getFileByPath('./files/01.txt')
.then(function(data) {
console.log(data)
return getFileByPath('./files/02 .txt')
})
.then(function(data2) {
console.log(data2)
return getFileByPath('./files/03 .txt')
})
.then(function(data3) {
console.log(data3)
})
.catch(function(error) {
console.log(error)
})
// 如果前面有任何的promise执行失败,会立即终止所有promise,并马上进入catch去处理promise中抛出的异常
const someAsyncThing = function(flag) {
return new Promise(function(resolve, reject) {
if(flag){
resolve('ok');
}else{
reject('error')
}
});
};
someAsyncThing(true).then((data)=> {
console.log('data:',data); // 输出 'ok'
}).catch((error)=>{
console.log('error:', error); // 不执行
})
someAsyncThing(false).then((data)=> {
console.log('data:',data); // 不执行
}).catch((error)=>{
console.log('error:', error); // 输出 'error'
})
上面代码中,someAsyncThing 函数成功返回 ‘OK’, 失败返回 ‘error’, 只有失败时才会被 catch 捕捉到。
async/await异步操作
含义
ES2017 标准引入了 async 函数,使得异步操作变得更加方便
async 函数是什么?一句话,它就是 Generator 函数的语法糖
async/await
从字面意思上很好理解,async
是异步的意思,await
有等待的意思,而两者的用法上也是如此。async
用于申明一个function
是异步的,而await
用于等待一个异步方法执行完成。
async
函数的使用方式,直接在普通函数前面加上 async
,表示这是一个异步函数,在要异步执行的语句前面加上 await
,表示后面的表达式需要等待
基本用法
async关键词
async的语法很简单,就是在函数开头加一个async
关键字,示例如下:
function f1() {
return 1;
}
async function f2() {
return 1;
}
console.log(f1()) // 1
console.log(f2()) // Promise{<fulfilled>:1}
凡是在前面添加了async的函数在执行后都会自动返回一个Promise对象
async函数会返回一个promise对象,如果function中返回的是一个值,async直接会用Promise.resolve()包裹一下返回:
async function f2() {
return 1;
}
async function f2() {
return Promise.resolve(1);
}
async function f2() {
return await 1;
}
// 上面3个函数作用等同
f2().then((res) => {
console.log(res) // 1
})
await关键词
关键词await
是等待的意思,其后面是一个表达式,这个表达式可以是常量、变量、Promise以及函数等
await必须在async函数里使用,不能单独使用
await后面需要跟Promise对象,不然就没有意义,而且await后面的Promise对象不必写then,因为await的作用之一就是获取后面Promise对象成功状态传递出来的参数
await
操作符等的是一个返回的结果,那么如果是同步的情况,那就直接返回了
异步的情况下,await
会阻塞整一个流程,直到结果返回之后,才会继续下面的代码
function f1() {
return 'aaa';
}
async function f2() {
return Promise.resolve('bbb');
}
async function test() {
const a = await f1();
const b = await f2();
console.log(a, b);
}
test(); // aaa bbb
阻塞代码是一个很可怕的事情,而async函数,会被包在一个promise中,异步去执行。所以await只能在async函数中使用,如果在正常程序中使用,会造成整个程序阻塞,得不偿失。
基本示例
当函数执行的时候,一旦遇到await
就会先返回,等到异步操作完成,再接着执行函数体内后面的语句
function timeout(ms) {
return new Promise((res) => {
setTimeout(res, ms)
})
}
async function asyncFn(v, ms) {
await timeout(ms) // 必须等待await后面表达式成功返回才会执行后面语句
console.log(v)
}
asyncFn('hello', 5000) // 5s后输出hello
正常情况下,await
命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值
async function f() {
return await 'hello'; // 等同于 return 'hello';
}
f().then(v => console.log(v)) // hello 【then的回调函数的接收的值是return语句返回的await语句返回值】
async function f() {
throw new Error('出错了');
}
f().then(
v => console.log(v),
e => console.log(e) // Error: 出错了
)
---------------------------------
async function f() {
await Promise.reject('出错了');
}
f().then(
v => console.log(v),
e => console.log(e) // 出错了
)
---------------------------------
async function f() {
await Promise.reject('出错了');
}
f()
.then(v => console.log(v))
.catch(e => console.log(e)) // 出错了
注意,上面代码中,await
语句前面没有return
,但是reject
方法的参数依然传入了catch
方法的回调函数。这里如果在await
前面加上return
,效果是一样的。
如果await后面的Promise变为reject状态,则reject的参数会被catch回调函数接收
如果是变为reject状态,前面不加return,reject的参数也会被catch回调函数接收
async function f() {
await Promise.reject('错误');
await 10;
console.log('hello')
}
f().then((res) => { console.log(res) }, (err) => { console.log(err) })
// 错误 【抛出错误后,会中断整个async函数的执行】
错误处理
任何一个await
语句后面的 Promise 对象变为reject
状态,那么整个async
函数都会中断执行。
async function f() {
await Promise.reject('出错了');
await Promise.resolve('hello world'); // 不会执行
}
f()
上面代码中,第二个await
语句是不会执行的,因为第一个await
语句状态变成了reject
。
有时候,我们希望即使前一个异步操作失败,也不要中断后面的异步操作。这时可以将第一个await
放在try...catch
结构里面,这样不管这个异步操作是否成功,第二个await
都会执行。
async function f() {
try {
await Promise.reject('出错了');
} catch(e) {
console.log(e); // 出错了
}
return await Promise.resolve('hello');
}
f().then(v => console.log(v))
// hello
另一种方法是await
后面的 Promise 对象再跟一个catch
方法,处理前面可能出现的错误。
async function f() {
await Promise.reject('出错了').catch(e => console.log(e));
return await Promise.resolve('hello world');
}
f().then(v => console.log(v))
// 出错了
// hello world
promise并不是只有一种resolve,还有一种reject的情况。而await只会等待一个结果,发生错误了有以下方式捕捉:
用try-catch来做错误捕捉
async function f() {
try {
await Promise.reject('error')
} catch (err) {
console.log(err)
}
}
f() // error
async function f() {
try {
await Promise.reject('失败');
} catch(e) {
}
return await Promise.resolve('成功');
}
f()
.then((res) => {
console.log('res ' + res)
})
.catch((err) => {
console.log('err ' + err)
})
// res 成功
// 可以将可能抛出错误的语句放入try catch语句中
async function f() {
await Promise.reject('失败').catch((err) => { console.log(err) });
return await Promise.resolve('成功');
}
f()
.then((res) => {
console.log(res)
})
// 失败 成功
// 也可以在可能抛出错误的promise对象后面使用catch来捕获抛出的错误
async function func() {
try {
var res1 = await a();
var res2 = await b();
var res3 = await c();
}
catch (err) {
console.error(err);
}
}
// 如果有多个await语句,那么可以将其统一放在try语句中
// 特别说明:如果具有多个await语句,且它们之间不是继发关系,建议让它们同时触发,以达到最大性能
继发并发
如果具有多个await语句,且它们之间不是继发关系,建议让它们同时触发,以达到最大性能
getA和getB是独立的异步操作,没必要是继发关系,也就是执行完a再去执行b,那么可以采用以下方式
let [a, b] = await Promise.all([getA(), getB()]);
或者
let aPromise = getA();
let bPromise = getB();
let a = await aPromise();
let b = await bPromise();
优点
在我们处理异步的时候,比起回调函数,Promise的then方法会显得较为简洁和清晰,但是在处理多个彼此之间相互依赖的请求的时候,就会显的有些累赘。这时候,用async和await更加优雅
Module 的语法
// CommonJS模块
let { a, b, c } = require('fs');
// 等同于
let _fs = require('fs');
let a = _fs.a;
let b = _fs.b;
let c = _fs.c;
严格模式
ES6 的模块自动采用严格模式,不管你有没有在模块头部加上"use strict";
。
严格模式主要有以下限制。
- 变量必须声明后再使用
- 函数的参数不能有同名属性,否则报错
- 不能使用
with
语句 - 不能对只读属性赋值,否则报错
- 不能使用前缀 0 表示八进制数,否则报错
- 不能删除不可删除的属性,否则报错
- 不能删除变量
delete prop
,会报错,只能删除属性delete global[prop]
-
eval
不会在它的外层作用域引入变量 -
eval
和arguments
不能被重新赋值 -
arguments
不会自动反映函数参数的变化 - 不能使用
arguments.callee
- 不能使用
arguments.caller
- 禁止
this
指向全局对象 - 不能使用
fn.caller
和fn.arguments
获取函数调用的堆栈 - 增加了保留字(比如
protected
、static
和interface
)
import 和 export
导出
// moudle.js
export var a = 10;
export var obj = { name: 'hello' };
export function fn(v) {
console.log(v)
};
或者(推荐)
// moudle.js
var a = 10;
var obj = { name: 'hello' };
function fn(v) {
console.log(v)
};
export { a, obj, fn }
导入
// main.js
import { a, obj, fn } from './moudle.js';
fn(a) // 10
a = 20; // error import命令输入的变量都是只读的
obj.name = 'hi'; // ok 如果变量是一个对象,改写变量的属性是允许的
别名
通常情况下,export
输出的变量就是本来的名字,但是可以使用as
关键字重命名。
var a1 = 10
var a2 = 20
export { a1 as a, a2 as b };
import { a1 as a } from './moudle.js';
注意写法
变量
export 1; // error
var a = 1;
export a; // error 没有声明变量
// 写法一
export var a = 1;
// 写法二
var a = 1;
export { a };
// 写法三
var a = 1;
export { a as b };
方法
// 报错
function fn() {}
export fn;
// 正确
export function fn() {};
// 正确
function fn() {}
export { fn };
export 导出命令
一个模块就是一个独立的文件,该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用`export`关键字输出该变量。ES6 将`moudle.js`其视为一个模块,里面用`export`命令对外部输出了三个变量。
另外,export
语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值。
export var foo = 'bar';
setTimeout(() => foo = 'baz', 500);
上面代码输出变量foo
,值为bar
,500 毫秒之后变成baz
。
最后,export
命令可以出现在模块的任何位置,只要处于模块顶层就可以。如果处于块级作用域内,就会报错,下一节的import
命令也是如此。这是因为处于条件代码块之中,就没法做静态优化了,违背了 ES6 模块的设计初衷。
function fn() {
export default 'bar' // error
}
fn()
上面代码中,export
语句放在函数之中,结果报错。
import导入命令
import
命令接受一对大括号,里面指定要从其他模块导入的变量名。大括号里面的变量名,必须与被导入模块(moudle.js
)对外接口的名称相同。
由于
import
是静态执行,所以不能使用表达式和变量。import
命令具有提升效果,会提升到整个模块的头部,首先执行
fn();
import { fn } from './moudle.js';
上面的代码不会报错,因为import
的执行早于foo
的调用。这种行为的本质是,import
命令是编译阶段执行的,在代码运行之前
模块的整体加载
除了指定加载某个输出值,还可以使用整体加载,即用星号(*
)指定一个对象,所有输出值都加载在这个对象上面。
// moudle.js
export function add(a, b) {
return a + b;
}
export function red(a, b) {
return a - b
}
普通加载
// main.js
import { add, red } from './moudle.js';
add(3, 2) // 5
red(3, 2) // 1
整体加载
// main.js
import * as com from './moudle.js';
com.add(3, 2) // 5
com.red(3, 2) // 1
export default 命令
export default
命令,为模块指定默认输出。
// moudle.js
export default function() {
console.log('hello');
}
其他模块加载该模块时,import
命令可以为该匿名函数指定任意名字。
// main.js
import sayHello from './file.js';
sayHello(); // hello
需要注意的是,这时import
命令后面,不使用大括号。
export default
命令用在非匿名函数前,也是可以的。
export default function sayHello() {
console.log('hello');
}
// 或者写成
function sayHello() {
console.log('hello');
}
export default sayHello;
上面代码中,foo
函数的函数名foo
,在模块外部是无效的。加载的时候,视同匿名函数加载。
// file.js
function add(x, y) {
return x + y;
}
export { add as default }; // 等同于 export default add;
// main.js
import { default as foo } from './file.js'; // 等同于 import foo from 'modules';
正是因为export default
命令其实只是输出一个叫做default
的变量,所以它后面不能跟变量声明语句。
// ok
export var a = 1;
// ok
var a = 1;
export default a;
// error
export default var a = 1;
同样地,因为export default
命令的本质是将后面的值,赋给default
变量,所以可以直接将一个值写在export default
之后。
// ok
export default 99;
// error
export 99;
上面代码中,后一句报错是因为没有指定对外的接口,而前一句指定对外接口为default
。
export default
也可以用来输出类。
// class.js
export default class { ... }
// main.js
import MyClass from 'class';
let o = new MyClass();
总结
当用
export default
导出时,就用import
导入(不带大括号)一个文件里,有且只能有一个
export default
。但可以有多个 export。当用
export a
时,就用import { a }
导入(记得带上大括号)当一个文件里,既有一个 export default people, 又有多个 export name 或者 export age 时,导入就用 import people, { name, age }
当一个文件里出现 n 多个 export 导出很多模块,导入时除了一个一个导入,也可以用
import * as example
class 基本语法
对于 class 在 react 中用得较多
基本用法
ES5
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.say = function () {
console.log(this.name + ' ' + this.age)
};
const p = new Person('Jack', 18)
p.say() // Jack 18
ES6
class Person {
constructor(name, age) { // 这是类的构造器
this.name = name
this.age = age
}
say() {
return this.name + ' ' + this.age
}
}
const p = new Person('Jack', 20)
p.say() // Jack 20
每个类中都有一个构造器,如果我们手动指定构造器,那么默认类内部有个隐形的、看不见的空构造器,类似于constructor() {}
构造器的作用:constructor
方法是类的默认方法,通过new
命令生成对象实例时,自动调用该方法。一个类必须有constructor
方法,如果没有显式定义,那么会默认添加一个空的constructor
方法
也就是说,ES5 的构造函数Person,对应 ES6 的Person类的构造方法
class Person {
}
// 等同于
class Person {
constructor() {}
}
实例属性和静态属性
实例方法和静态方法
通过new
出来的实例能访问到的属性,叫做实例属性
通过构造函数,直接访问到的属性,叫做静态属性
ES5
function Person(name, age) {
this.name = name // name是实例属性
this.age = age // age是实例属性
}
Person.msg = 'hello' // msg是静态属性
Person.prototype.say = function () {
console.log('这是Person的实例方法')
}
Person.show = function() {
console.log('这是Person的静态方法')
}
const p = new Person('Jack', 18)
p.name // Jack
p.age // 18
p.say() // 这是Person的实例方法
p.show() // error, p.show is not a function
Person.show() // 这是Person的静态方法
p.msg // undefined
Person.msg // hello
ES6
class Person {
constructor(name, age) { // 这是类的构造器
this.name = name
this.age = age
}
static msg = 'hello' // 在class内部,通过static修饰的属性,就是静态属性
say() { // 实例方法
console.log('这是Person的实例方法')
}
static show() { // 静态方法
console.log('这是Person的静态方法')
}
}
const p = new Person('Jack', 20)
p.msg // undefined
Person.msg // hello
类的继承
在class中,使用extends关键字实现子类继承父类
父类
class Person {
constructor(name, age) {
this.name = name
this.age = age
}
say() {
console.log('大家好')
}
}
子类
- 美国人
class American extends Person {
constructor(name, age) {
super(name, age) // 调用父类的constructor(name, age)
}
}
let a = new American('Jack', 20 )
- 中国人(有身份证号独有)
class Chinese extends Person {
constructor(name, age, IDNumber) {
super(name, age) // 调用父类的constructor(name, age)
this.IDNumber = IDNumber // 语法规范:在子类中,this只能放到super之后使用
}
}
let c = new Chinese('李三', 20, '450981******' )
1.为什么一定要在constructor
中调用super
?
答:因为如果一个子类通过extends关键字继承父类,那么在子类的constructor构造函数中必须优先调用super()
2.super
是什么东西?
答: super是一个函数,他是父类的构造器,子类中的super其实就是父类中constructor构造器的一个引用
3.如果不调用super
方法,子类就得不到 this 对象
class Point {}
typeof Point // function
Point === Point.prototype.constructor // true
class注意问题
- 在class内部,只能写构造器,实例方法,静态属性,静态方法
class Person {
var a = 10 // 报错
}
- class关键字内部,还是用原来的ES5实现,我们把class关键字称作语法糖
JavaScript 异步操作原理
同步操作,就是同一时刻只能做一件事情,如果要多件事情需要处理,需要排队
异步操作,就是同一时刻能够做多件事情,比如银行开多个窗口同时办理业务
异步操作原理
JavaScript虽然是单线程的,好像不能实现异步操作。
然而它的运行环境浏览器是多线程的,这是实现异步的决定性因素。
浏览器可以包含如下主要线程:
(1).JavaScript引擎。
(2).界面渲染。
(3).浏览器事件
(4).http(s)请求
<img src="https://www.softwhy.com/data/attachment/portal/201812/07/010435obew47vekeph6e4e.png" />
图示说明:
(1).tn:表示不同的时刻。
(2).tn下方方块:表示tn时刻正在执行或者将要执行的代码。
原理解析
(1).界面渲染:
大家知道JavaScript代码默认会阻塞代码的执行,自然包括界面的渲染。
因为JavaScript代码可以操作界面的内容,所以要等待代码执行完毕再去渲染。(2).定时器函数:
JavaScript中有两个定时器函数,分别是setTimeout与setInterval。
这两个函数都可以进行时间计数,但是需要注意的是,计数功能并不是由JavaScript引擎完成。
因为JavaScript是单线程的,如果有其他任务在执行,那就无法计数了,所以定时器也是异步的。
(3).图示分析:
t1时刻正在执行一个回调函数,t2时刻通过点击触发click事件,浏览器事件也是一个独立的线程,所以同样是异步操作,将事件处理函数放入执行队列。click事件处理函数不会立刻执行,而是要等待t1时刻的代码执行完毕。随着时间的推移,定时器函数的回调函数不断放入队列等待执行。也就是说,定时器与事件等操作都是异步的,当时放入JavaScript引擎线程的代码还是要排队执行的。举个简单的例子
setTimeout(function(){
console.log("hello");
},1000)
while(true){
//code
}
代码的初衷是1000毫秒后打印字符串"hello部落",所以会首先执行while语句。
在执行while语句的同时,计数器正常计数(因为它不属于JavaScript引擎线程,不会被堵塞)。
到达1000毫秒后,定时器将会回调函数放入JavaScript引擎线程队列。
然而,while语句是一个死循环,所以回调函数永远得不到执行。
工作中的ES6代码
扩展运算符合并对象
let a = { name: 'Jack',age: '20',love: '音乐'};
let b = { name: 'Mark'}
let ab = {...a, ...b};
ab // {name: "Mark", age: "20", love: "音乐"}
let a = { name: 'Jack',age: '20',love: '音乐'};
let b = { name: 'Mark'}
let ab = {...b, ...a};
ab // {name: "Jack", age: "20", love: "音乐"}
const { gn, vv } = this;
// 相当于
const gn = this.gn
const vv = this.vv
state.carts = state.carts.filter((c, i) => !c.check)
// 相当于
function f(c: CartData, i: number) {
return !c.check;
}
state.carts = state.carts.filter(f);
http.js
import axios from 'axios';
const http = axios.create({
baseUrl: 'http://vmiaomall.com'
})
// 添加请求拦截器
http.interceptors.request.use((config) => {
showloading(); // 显示加载
config.url = config.url + '.do'; // 在发送请求之前做些什么
return config;
}, function (error) {
return Promise.reject(error); // 对请求错误做些什么
});
// 添加响应拦截器
axios.interceptors.response.use((res) => {
hideLoading(); // 隐藏加载
if(res.data) {
return res.data; // 对响应数据做点什么
}
}, function (error) {
return Promise.reject(error); // 对响应错误做点什么
});
export default http
或者
import axios from 'axios';
axios.defaults.baseURL = 'https://vmiaomall.com/api';
export default {
get(url, data = {}, options = {}) {
return new Promise((resolve, reject) => {
axios.get(url, { params: data }).then(res => {
if (res.status === 200) {
resolve(res.data)
} else {
Toast(res.data.msg)
}
}).
catch(err = >{
reject(err) let errMsg = '请求失败!请检查网络';
if (err.response) errMsg = err.response.data.msg Toast(errMsg)
})
})
},
post(url, data = {}, options = {}) {
return new Promise((resolve, reject) => {
axios.post(url, data).then(res => {
if (res.status === 200) {
resolve(res.data.msg)
} else {
Toast(res.data)
}
}).
catch(err = >{
reject(err) let errMsg = '请求失败!请检查网络';
if (err.response) errMsg = err.response.data.msg Toast(errMsg)
})
})
}
}