ES6简化教程

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 }

参数变量是默认声明的,所以不能用letconst再次声明

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) {
    // ...
}

箭头函数

特点

  • 没有thissuperarguments

  • 不可以当作构造函数,也就不能通过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 }

由于解构赋值要求等号右边是一个对象,所以如果等号右边是undefinednull,就会报错,因为它们无法转为对象。

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

一般来说,调用resolvereject以后,Promise 的使命就完成了,后继操作应该放到then方法里面,而不应该直接写在resolvereject的后面。所以,最好在它们前面加上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不会在它的外层作用域引入变量
  • evalarguments不能被重新赋值
  • arguments不会自动反映函数参数的变化
  • 不能使用arguments.callee
  • 不能使用arguments.caller
  • 禁止this指向全局对象
  • 不能使用fn.callerfn.arguments获取函数调用的堆栈
  • 增加了保留字(比如protectedstaticinterface

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)
        })
      })
    }
  }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,198评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,663评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,985评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,673评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,994评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,399评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,717评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,407评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,112评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,371评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,891评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,255评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,881评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,010评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,764评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,412评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,299评论 2 260

推荐阅读更多精彩内容

  • [TOC] 参考阮一峰的ECMAScript 6 入门参考深入浅出ES6 let和const let和const都...
    郭子web阅读 1,745评论 0 1
  • 第一章:块级作用域绑定 块级声明 1.var声明及变量提升机制:在函数作用域或者全局作用域中通过关键字var声明的...
    BeADre_wang阅读 760评论 0 0
  • 以下内容是我在学习和研究ES6时,对ES6的特性、重点和注意事项的提取、精练和总结,可以做为ES6特性的字典;在本...
    科研者阅读 3,027评论 2 9
  • 本文为阮一峰大神的《ECMAScript 6 入门》的个人版提纯! babel babel负责将JS高级语法转义,...
    Devildi已被占用阅读 1,927评论 0 4
  • 第5章 引用类型(返回首页) 本章内容 使用对象 创建并操作数组 理解基本的JavaScript类型 使用基本类型...
    大学一百阅读 3,148评论 0 4