ES6参考手册简化版

ES6简介

ECMAScript 6.0(以下简称 ES6)是 JavaScript 语言的下一代标准,已经在 2015 年 6 月正式发布了,也叫ECMAScript 2015

块级作用域

ES5作用域缺陷

function fn() {
  var a = 'hello';
}
fn();
console.log(a); // Uncaught ReferenceError: a is not defined

// 解析:变量a只在函数作用域中有效,a在块级作用域{}里面

ES6之前 只有全局作用域函数作用域,没有块级作用域,带来很多不合理的场景

第一种场景

内层变量可能会覆盖外层变量

var a = 'aaa';

function fn() {
  console.log(a); // 预解析:就是在浏览器解析代码之前,把变量的声明和函数的声明提升到该作用域的最上面
  if (false) {
    var a = 'bbb';
  }
}

fn(); // undefined

相当于

var a = 'aaa';
function fn() {
  var a;
  console.log(a); // 变量声明了但没有赋值,结果是undefined
  if (false) {
    var a = 'bbb';
  }
}

第二种场景

用来计数的循环变量泄露为全局变量

for (var i = 0; i < 5; i++) {
  // code
}
console.log(i); // 5 由于i没有采用块级作用域,那么i的作用域是全局的,打印结果是5

解决办法

for (let i = 0; i < 5; i++) {
  // code
}
console.log(i); // Uncaught ReferenceError: i is not defined

ES6块级作用域

  • 花括号 {} 和其中代码生成一个块
  • 在块中,letconst声明的变量和常量对外都是不可见的,称之为块级作用域
  • 只有使用letconst声明的变量或者常量在块中对外不可见,var声明的变量对外依然可见
function fn() {
  let a = 5;
  if (true) {
    let a = 10;
  }
  console.log(a);
}
fn(); // 5

上面的函数有两个代码块,都声明了变量n,运行后输出 5,这表示外层代码块不受内层代码块的影响。如果两次都使用var定义变量n,最后输出的值才是 10

function fn() {
  var a = 5;
  if (true) {
    var a = 10;
  }
  console.log(a);
}
fn() // 10

let 和 const

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); // ReferenceError: a is not defined
  let a = 2;
}


// 只在声明所在的块级作用域内有效
{
  let a = 1;
  console.log(a); // 1
  var b = 2;
}
console.log(a); // Uncaught ReferenceError: a is not defined
console.log(b); // 2

示例

var a = 10;
function fn() {
  console.log(a);
  let a = 20;
}
fn(); // Uncaught ReferenceError: Cannot access 'a' before initialization

// 解析:初始化前无法访问a

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: a is not defined
  const a = 5;
}


// 只在声明的块级作用域内有效
{
  const a = 10;
  console.log(a); // 10
}
console.log(a); // Uncaught ReferenceError: a is not defined

复合类型的数据(主要是对象和数组),可以这样子变动(它们在栈中的引用地址没变)

{
    const obj = {};
    obj.name = 'abc';

    // 将 foo 指向另一个对象,就会报错
    obj = {}; // Uncaught TypeError: Assignment to constant variable
}
{
    const arr = [];
    arr.push(123);
}

区别

// 相同点:
    1. 都不存在变量提升
    2. 都只在声明的块级作用域内有效

// 不同点:
    1. 声明类型: let 声明变量, const 声明常量
  2. 赋值时机: let 可以声明变量与给变量赋值分开,使用 const 声明常量的时候必须同时赋值,否则报错

推荐

// 对于 数值、字符串、布尔值 经常会变的,用 let 声明
{
    let num = 10;
    let str = 'abc';
    let flag = true;
}

// 对象、数组和函数用 const 来声明
{
    const obj = {};
    const arr = [];
    const fn = function() {}
}

解构赋值

数组解构赋值

一次性声明多个变量

{
    let [a, b, c, d] = [1, 2, 3];
    console.log(a); // 1
    console.log(b); // 2
    console.log(c); // 3
    console.log(d); // undefined
}

结合扩展运算符

{
    let [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, c = 3] = [1, 2];

    console.log(a); // 1
    console.log(b); // 2
    console.log(c); // 3
}
{
    let [a, , , b] = [1, 2, 3, 4, 5];
    console.log(a, b); // 1, 4
}
{
    let [a, , ...b] = [1, 2, 3, 4, 5];
    console.log(a, b); // 1, [3, 4, 5]
}

应用场景

// 变量交换
{
    let a = 1;
    let b = 2;
    [a, b] = [b, a];
    console.log(a, b); // 2, 1
}

对象解构赋值

数组中,变量的取值由它 排列的位置 决定;而对象中,变量必须与 属性 同名,才能取到正确的值

变量与属性同名

{
    let { a, b } = { a: 1, b: 2 };
    console.log(a); // 1
    console.log(b); // 2
}
{
  let obj = { b: 2, a: 1 };
    let { a, b, c } = obj;
    console.log(a); // 1
    console.log(b); // 2
  console.log(c); // undefined
}

允许指定默认值

{
    let { a, b = 2 } = { a: 1 };
    console.log(a); // 1
    console.log(b); // 2
}

允许重命名

{
  let { a: res } = { a: 1, b: 2 };
  console.log(a); // ReferenceError: a is not defined (原名a变无效)
  console.log(res); // 1
}

函数参数解构赋值

函数参数的默认值

function fn(a = 1, b = 2){
    return a + b;
}
fn() // 3  默认 a = 1, b = 2
fn(3) // 5  因为 a = 3, b = 2
fn(33) // 6  因为 a = 3, b = 3

函数参数的默认值(对象参数)

function fn({a = 1, b = 2} = {}) {
  console.log(a, b);
}
fn(); // 1 2
fn({ a: 2}); // 2 2
fn({ a: 3, b: 6}); // 3 6

字符串解构

字符串也可以解构赋值,这是因为此时,字符串被转换成了一个类似数组的对象

字符串解构

{
  const [a, b, c, d, e] = 'hello';
    console.log(a); // h
    console.log(b); // e
    console.log(c); // l
    console.log(d); // l
    console.log(e); // o
}

把字符串转为数组

{
  const [...a] = 'hello';
    console.log(a); // ["h", "e", "l", "l", "o"]
}

字符串扩展

模板字符串

模板字符串 用反引号(``)标识。它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量。

字符串中嵌套变量

原生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
var obj = { 
    a: 1,
    b: 2
}
console.log(`${obj.a + obj.b}`); // 3
var 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 str = 'Hello world!';
    str.startsWith('Hello'); // true
    str.endsWith('!'); // true
    str.includes('o'); // true
}

这三个方法都支持第二个参数,表示开始搜索的位置

{
    let str = 'Hello world!';
    str.startsWith('world', 6); // true
    str.endsWith('Hello', 5); // true
    str.includes('Hello', 6); // false
}
函数 含义
repeat() 字符串重复
padStart()
padEnd()
{
    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
}

数组的扩展

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

扩展运算符

展开数组

console.log(...[1, 2, 3]); // 1 2 3
console.log(1, ...[2, 3, 4], 5); // 1 2 3 4 5

注意,只有函数用时,扩展运算符才可以放在圆括号中

(...[1, 2]); // 报错

数组合并

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(a, b) {
  return a + b;
}

const arr = [2, 3];
add(...arr); // 5

替代 apply

由于扩展运算符可以展开数组,所以不再需要apply方法,将数组转为函数的参数了

function fn(a, b, c) {
  console.log(a, b, c)
}
var arr = [1, 2, 3];

// ES5 的写法
fn.apply(null, arr); // 1 2 3

// ES6的写法
fn(...arr); // 1 2 3
// ES5 的写法
Math.max.apply(null, [3, 9, 6]);

// ES6 的写法
Math.max(...[3, 9, 6]);

// 等同于
Math.max(3, 9, 6);

将一个数组添加到另一个数组的尾部

// 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]; // 报错
const [a, ...b] = [1, 2, 3];

复制数组

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" ]

数组函数

函数 含义
includes() 方法返回一个布尔值,判断数组是否包含给定的值,与字符串的 includes 方法类似
find() 返回符合传入测试(函数)条件的数组元素
findIndex() 返回符合传入测试(函数)条件的数组元素索引
for…of 数组遍历
Array.of() 方法用于将一组值,转换为数组

includes()

[1, 2, 3].includes(2); // true
[1, 2, 3].includes(4); // false
[1, 2, NaN].includes(NaN); // true

该方法的第二个参数表示搜索的起始位置,默认为 0。如果第二个参数为负数,则表示倒数的位置,如果这时它大于数组长度(比如第二个参数为 -4,但数组长度为 3 ),则会重置为从 0 开始

[1, 2, 3].includes(3, 3); // false
[1, 2, 3].includes(3, -1); // true

find() 和 findIndex()

[1, 2, 3, 4, 5].find((item) => item > 3); // 4  -可以看到只返回一个值

[1, 2, 3, 4, 5].findIndex((item) => item > 3); // 3  -返回索引
[1, 2, 3, 4, 5].find((item, index, arr) => item > 9); // -1  -所有成员都不符合条件,则返回-1

两个方法都可以发现NaN,弥补了数组的indexOf方法的不足

[NaN].indexOf(NaN); // -1
[NaN].findIndex(y => Object.is(NaN, y)); // 0

for … of数组遍历

直接得到数组的值

var arr = [10, 20, 30];
for(var i of arr) {
    console.log(i);
}

// 10
// 20
// 30
var arr = [{a: 10}, {b: 20}, {c: 30}];
for(var i of arr) {
    console.log(i);
}

// {a: 10}
// {b: 20}
// {c: 30}

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

Array.of()

Array.of(1, 2, 3); // [1, 2, 3]

Array.of(3); // [3]

Array.of(); // []

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
}
console.log(es6); // {a: 1, b: 2}
function fn(a, b) {
  return {a, b};
}

// 等同于
function fn(a, b) {
  return {a: a, b: b};
}

fn(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 { ...a } = null; // 报错
let { ...a } = undefined; // 报错

解构赋值必须是最后一个参数,否则会报错

let { ...a, b, c } = obj; // 报错
let { a, ...b, ...c } = obj; // 报错

注意,解构赋值的拷贝是浅拷贝,即如果一个键的值是复合类型的值(数组、对象、函数)、那么解构赋值拷贝的是这个值的引用,而不是这个值的副本

let obj = { a: { b: 1 } };
let { ...newObj } = obj;
obj.a.b = 2;
console.log(newObj.a.b); // 2

对象API

判断两个字符串是否相等

'abc' === 'abc'; // true
Object.is('abc', 'abc'); // true

判断两个数组是否相等

[] === []; // false
Object.is([], []); // false

// 解析:两个数组引用的是两个不同的地址

拷贝对象

Object.assign({a: 1}, {b: 2}); // {a: 1, b: 2}  浅拷贝

遍历对象

{
    const obj = {a: 1, b: 2, c: 3};
  for(let [key, value] of Object.entries(obj)) {
      console.log([key, value]);
  }
}
// ['a', 1]
// ['b', 2]
// ['c', 3]

Object.assign()

Object.assign方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)

const target = { a: 1 };
const source1 = { b: 2 };
const source2 = { c: 3 };
Object.assign(target, source1, source2);
console.log(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);
console.log(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;

函数的扩展

默认参数

ES6 之前,不能直接为函数的参数指定默认值,只能采用变通的方法

function fn(a, b) {
  b = b || 2;
  console.log(a, b);
}
fn(1); // 1 2

ES6 允许为函数的参数设置默认值,即直接写在参数定义的后面

function fn(a, b = 2) {
  console.log(a, b);
}

fn(1); // 1 2
fn(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 fn(a = 3) {
  let a = 1; // 报错
  const a = 2; // 报错
}

与解构赋值默认值结合使用

function fn({a, b = 5}) {
  console.log(a, b);
}

fn({}); // undefined 5
fn({a: 1}); // 1 5
fn({a: 1, b: 2}); // 1 2
fn(); // 报错
function fn({a, b = 5} = {}) {
  console.log(a, b);
}

fn(); // undefined 5

参数默认值位置

通常情况下,定义了默认值的参数,应该是函数的尾参数

function fn(a = 1, b) {
  return [a, b];
}

fn(); // [1, undefined]
fn(2); // [2, undefined])
fn(, 1); // 报错
fn(undefined, 1); // [1, 1]

上面代码中,有默认值的参数都不是尾参数。这时,无法只省略该参数,而不省略它后面的参数,除非显式输入undefined

如果传入undefined,将触发该参数等于默认值,null则没有这个效果

function fn(a = 5, b = 6) {
    console.log(a, b);
}

fn(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 参数之后不能再有其他参数(即只能是最后一个参数),否则会报错

// 报错
function fn(a, ...b, c) {
    // ...
}

箭头函数(ES6)

箭头函数语法

总结:

箭头函数相当于匿名函数,所以需要变量接收

如果参数只有一个,不需要括号,参数没有或者多个则需要括号

如果代码块部分多于一条语句,需要大括号{}

如果需要返回一个对象,必须在对象外面加上括号

var f = () => 'hello'

// 等同于
var f = () => { return 'hello' }

// 等同于
var f = function() { return 'hello' }
// ES5
var f = function(a, b) { return a * b };

// ES6
var f = (a, b) => a * b;

当箭头函数只有一个参数时,可以省略括号,直接写参数名

var f = a => a;

当箭头函数没有参数或者多个参数是,不能省略括号

var f = () => 'hello';
var sum = (a, b) => a + b;

当箭头函数的代码块部分多于一条语句,就要使用 {} 将它们括起来,并且使用 return 语句返回

var sum = (a, b) => { 
  console.log('hello');
  return a + b; 
}
sum(1, 2);
// hello
// 3

由于大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号,否则会报错

let getObj = id => ({ id: id, name: 'aaa' });

箭头函数示例

箭头函数可以与变量解构结合使用

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(i) { 
    return i * i;
})

// 箭头函数写法
[1, 2, 3].map(i => i * i);

另一个例子是

// 正常函数写法
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  super  arguments
* 不可以当作构造函数,也就不能通过 new 关键字调用
* 没有原型属性 prototype
* 不可以改变 this 指向
* 不支持重复的命名参数

注意:箭头函数和传统函数一样都有一个 name 属性,这一点是不变的

this指向的固定化,并不是因为箭头函数内部有绑定this的机制,实际原因是箭头函数根本没有自己的this,导致内部的this就是外层代码块的this。正是因为它没有this,所以也就不能用作构造函数

箭头函数没有this,只能从上下文获取this

箭头函数的this值,取决于函数外部非箭头函数的this值,如果上一层还是箭头函数,那就继续往上找,如果找不到那么this就是Window对象

var person = {
    f1: () => {
        console.log(this)
    },
    f2: function() {
        return () => { console.log(this) }
    }
}
person.f1();  // Window
person.f2()();  // person对象

箭头函数没有arguments对象

箭头函数也没有arguments对象,但是如果它外层还有一层非箭头函数的话,就会去找外层的非箭头函数的arguments对象,注意:箭头函数找arguments对象只会找外层非箭头函数的函数,如果外层是一个非箭头函数的函数如果它也没有arguments对象也会中断返回,就不会在往外层去找了,如下

var f1 = () => console.log(arguments);  // 执行该函数会抛出错误

function f2(a, b, c) {
    return () => {
        console.log(arguments); // [1, 2, 3]
    }
}
f2(1, 2, 3)();

可以清楚的看到当前的箭头函数没有arguments对象,然而就去它的外层去找非箭头函数的函数。``

function fn(a) {
    return function() {
        return () => {
            console.log(arguments); // []
        }
    }
}
fn(1)()();

上面示例中可以看到,里面的箭头函数往外层找非箭头函数的函数,然后不管外层这个函数有没有arguments对象都会返回。只要它是非箭头函数就可以,如果外层是箭头函数还会继续往外层找

var f = (...a) => console.log(a);
f(1, 2, 3); // [1, 2, 3]

箭头函数不能用new关键字声明

var Fn = () => {};
new Fn(); // 抛出错误,找不到constructor对象

箭头函数没有原型prototype

箭头函数没有原型,有可能面试官会问,JavaScript中所有的函数都有prototype属性吗

var fn = () => {};
fn.prototype; // undefined

箭头函数不能改变this指向

var person = {};
var fn = () => console.log(this);

fn.bind(person)(); // Window (不是person)
fn.call(person); // Window (不是person)
fn.apply(person); // Window (不是person)

箭头函数和普通函数的区别

* 相比普通函数, 箭头函数有更简洁的语法
* 箭头函数不绑定 this ,会捕获其所在的上下文的 this 值,作为自己的 this* 箭头函数是匿名函数, 不能作为构造函数,不可以使用 new 命令,否则会抛出一个错误
* 箭头函数没有 arguments,取而代之用 rest参数...解决
* 箭头函数的 this 永远指向其上下文的 this ,任何方法都改变不了其指向,如 call()、bind()、apply() ,可以说正是因为没有自己的 this
* 箭头函数没有原型属性 prototype

Set

ES6 提供了新的数据结构 Set,它类似于数组,但是成员的值都是唯一的,没有重复的值

Set 本身是一个构造函数,用来生成 Set 数据结构(既然是构造函数,肯定需要new)

const s = new Set();
[2, 3, 5, 4, 5, 2, 2].forEach(i => s.add(i));
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 arr = [1, 1, 2, 3, 4, 4];
[...new Set(arr)];
// [1, 2, 3, 4]

或者

const arr = [1, 1, 2, 3, 4, 4];
Array.from(new Set(arr));
// [1, 2, 3, 4]

Map

Map 数据结构类似于对象,是键值对的集合,传统的键只能用字符串,Map 的键不限于字符串,各种类型的值(包括对象)都可以当作键。
属性和操作方法

size 属性,返回 Map 结构的成员总数
set(key,value)方法,设置set方法设置键名key对应的键值为value,然后返回整个 Map 结构。如果key已经有值,则键值会被更新,否则就新生成该
get(key) 方法,读取key对应的键值,如果找不到key,返回undefined
has(key) 方法,返回一个布尔值,表示某个键是否在当前 Map 对象之中。
delete(key) 方法,删除某个键,返回true。如果删除失败,返回falseclear(key) 方法,清除所有成员,没有返回值。

Map 遍历

keys() // 返回键名的遍历器
values() // 返回键值的遍历器
entries() // 返回所有成员的遍历器
forEach() // 遍历Map的所有成员

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

导出 export

// moudle.js
export var num = 10;
export var obj = { name: 'hello' };
export function fn(v) {
    console.log(v)
};

或者(推荐)

// moudle.js
var num = 10;
var obj = { name: 'hello' };
function fn(v) {
    console.log(v)
};
export { num, obj, fn }

导入 import

// main.js
import { num, obj, fn } from './moudle.js';

fn(num); // 10
num = 20; // 报错,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; // 报错

var a = 1;
export a; // 报错,没有声明变量
// 写法一
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 reduce(a, b) {
  return a - b
}

普通加载

// main.js
import { add, reduce } from './moudle.js';

add(3, 2) // 5
reduce(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 './moudle.js';

sayHello(); // hello

需要注意的是,这时import命令后面,不使用大括号

export default命令用在非匿名函数前,也是可以的

export default function sayHello() {
  console.log('hello');
}

// 或者写成
function sayHello() {
  console.log('hello');
}

export default sayHello;

上面代码中,sayHello函数的函数名sayHello,在模块外部是无效的,加载的时候,视同匿名函数加载

// moudle.js
function add(a, b) {
  return a + b;
}
export { add as default }; // 等同于 export default add;

// main.js
import { default as foo } from './moudle.js'; // 等同于 import foo from './modules.js';

正是因为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

总结

1. 当用 export default 导出时,就用 import 导入(不带大括号)

2. 一个文件里,有且只能有一个 export default ,但可以有多个 export

3. 当用 export a 时,就用 import { a } 导入(记得带上大括号)

4. 当一个文件里,既有一个 export default people, 又有多个 export name 或者 export age 时,导入就用
    import people, { name, age } from './moudle.js';

5. 当一个文件里出现 n 多个 export 导出很多模块,导入时除了一个一个导入,也可以用
  import * as com from './moudle.js';

class 基本语法

基本用法

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() {
        console.log(this.name, this.age);
    }
}

const p = new Person('Jack', 20);
p.say(); // Jack 20

每个类中都有一个构造器,如果我们手动指定构造器,那么默认类内部有个隐形的、看不见的空构造器,类似于constructor() {}

构造器的作用:constructor方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。一个类必须有constructor方法,如果没有显式定义,那么会默认添加一个空的constructor方法

也就是说,ES5 的构造函数Person,对应 ES6 的Person类的构造方法

function 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关键字称作语法糖

   转载规则


《ES6参考手册简化版》 小乐 采用 知识共享署名 4.0 国际许可协议 进行许可。
  目录