ES6学习笔记

作者: shisaq 日期: May 21, 2018

最近的文章全都是初学、初探、初尝,方向也大相径庭。因为对未来的发展方向产生了些许困惑。还好这样的时光总是短暂的,云开雾散,我就继续做一个略偏前端的全栈工程师吧。钻研的前提是确定一个点。

语法

var的替代:letconst

let:可被重新赋值,但不可在同一个作用域中被再次声明。 const:必须在声明时赋予初始值,且不可被重新声明或赋值。

即:在同一作用域内(2 个花括号之间),定义变量则用let,定义常量则用constvar是时候歇歇了。

注:用const定义的对象,是 可以 修改内部的属性的:

1
2
3
const student = { name: 'James', age: 26, gender: 'male' };
student.name = 'Lucy';
console.log(student); // {name: "Lucy", age: 26, gender: "male"}

模板字符串

` `包裹的字符串内可包含以${}包裹的变量名。例子:

1
2
3
const myName = 'shisaq';
const greeting = `Hello, my name is ${myName}`;
console.log(greeting); // Hello, my name is shisaq

字符串若为多行,同样奏效(在长字符串拼接 HTML 时,用此属性可省去大批的''、+等符号):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 定义teacher和student对象
const teacher = {
  name: 'John'
};

const student = {
  name: 'Alex',
  guardian: 'Lisa'
};
// 定义长字符串
const note = `${teacher.name},
    Please excuse ${student.name}.
    He is recovering from the flu.

    Thank you,
    ${student.guardian}`;

console.log(note);
/* 打印结果
John,
    Please excuse Alex.
    He is recovering from the flu.

    Thank you,
    Lisa
*/

从数组和对象中解构变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 数组
const fruits = ['apple', 'banana', 'orange'];
const [a, b, c] = fruits;
console.log(a, b, c); // apple banana orange

// 对象
const room = {
  name: 'blitz',
  width: 10,
  length: 10,
  height: 10,
  size: function() {
    return this.width * this.length * this.height;
  },
  type: 'seaview'
};

const { name, size, type } = room;
console.log(name, size(), type); // blitz 1000 seaview

对象字面量简写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 早期写法
let type = 'quartz';
let color = 'rose';
let carat = 21.29;

const gemstone = {
  type: type,
  color: color,
  carat: carat,
  calculateWorth: function() {
    return this.carat * 3;
  }
};

console.log(gemstone); // {type: "quartz", color: "rose", carat: 21.29, calculateWorth: ƒ}

// ES6写法
let type = 'quartz';
let color = 'rose';
let carat = 21.29;

const gemstone = {
  type,
  color,
  carat,
  calculateWorth() {
    return this.carat * 3;
  }
};
console.log(gemstone); // {type: "quartz", color: "rose", carat: 21.29, calculateWorth: ƒ}
console.log(gemstone.calculateWorth()); // 63.87

For…of 循环

  • 相对 For/ForEach:For…of 循环可以在不定义索引变量的情况下,直接获得可迭代对象的值
  • 相对于 ForEach:For…of 循环可在规定的条件下跳过或退出循环
  • 相对 For…in:For…of 循环只会取对象中元素的值,不会把 function 一并当成需要迭代的对象处理(如果数组被定义了一个额外的方法,for…in 会将该方法一起迭代出来,而 for…of 不会)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const digits = [0, 1, 2, 3, 4, 5];

for (const digit of digits) {
  if (digit % 2 === 0) {
    continue;
  }
  console.log(digit);
}

/** 打印结果
0
1
3
5
*/

扩展运算符(…)Spread Operator

将一个数组转换成以逗号分隔的参数序列。

1
2
3
4
5
6
7
const books = [
  'Don Quixote',
  'The Hobbit',
  'Alice in Wonderland',
  'Tale of Two Cities'
];
console.log(...books); // Don Quixote The Hobbit Alice in Wonderland Tale of Two Cities

简单用法:合并 2 个数组

1
2
3
4
5
6
const fruits = ['apples', 'bananas', 'pears'];
const vegetables = ['corn', 'potatoes', 'carrots'];

const produce = [...fruits, ...vegetables];

console.log(produce); // [ 'apples', 'bananas', 'pears', 'corn', 'potatoes', 'carrots' ]

遗留参数(Rest Parameter)

将遗留的值归为一个数组。多用于传参。

1
2
3
const order = [20.17, 18.67, 1.5, 'cheese', 'eggs', 'milk', 'bread'];
const [total, subtotal, tax, ...items] = order;
console.log(total, subtotal, tax, items); // 20.17 18.67 1.5 ["cheese", "eggs", "milk", "bread"]

Function

箭头函数(Arrow Functions =>)

普通函数可有 2 种书写方式:声明式表达式;而箭头函数只能是表达式

把常规函数转换成箭头函数的步骤如下:

  1. 删除函数标识符function
  2. 删除紧跟在后面的小括号()
  3. 删除后面的大括号{}
  4. 删除return
  5. 删除最后的分号;
  6. 在参数列表与函数体之间添加箭头=>
  7. (可选)若参数不是 1 个(0 个或多个),则需要把参数用小括号()括起来。当然,参数是 1 个的时候,也可以括起来。

举例,将一个数组所有元素都变成大写,返回这个新的数组:

1
2
3
4
5
6
7
8
9
// 常规函数写法
const upperizedNames = ['Farrin', 'Kagure', 'Asser'].map(function(name) {
  return name.toUpperCase();
});

// 箭头函数写法
const upperizedNames = ['Farrin', 'Kagure', 'Asser'].map(name =>
  name.toUpperCase()
);

箭头函数有 2 种语法:简洁函数体语法(concise body syntax)块状函数体语法(block body syntax)

简洁函数体语法:

  • 函数体没有被花括号{}包住;
  • 自动返回表达式的值。

块状函数体语法:

  • 函数体被花括号{}包住;
  • 需要用return手动返回需要的值。

this在常规函数与箭头函数中的不同:

常规函数:this取决于该函数是怎么被调用的; 箭头函数:this取决于这串代码在哪里被执行。

关于this在两种函数中的不同,我在另一篇文章中有详细说明。

函数中的参数默认值

常规函数写法

1
2
3
4
5
6
7
8
9
10
function greet(name, greeting) {
  name = typeof name !== 'undefined' ? name : 'Student';
  greeting = typeof greeting !== 'undefined' ? greeting : 'Welcome';

  return `${greeting} ${name}!`;
}

greet(); // Welcome Student!
greet('James'); // Welcome James!
greet('Richard', 'Howdy'); // Howdy Richard!

带默认值的函数写法

1
2
3
4
5
6
7
function greet(name = 'Student', greeting = 'Welcome') {
  return `${greeting} ${name}!`;
}

greet(); // Welcome Student!
greet('James'); // Welcome James!
greet('Richard', 'Howdy'); // Howdy Richard!

允许空值的写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function houseDescriptor({
  houseColor = 'green',
  shutterColors = ['red']
} = {}) {
  return `I have a ${houseColor} house with ${shutterColors.join(
    ' and '
  )} shutters`;
}

houseDescriptor({ houseColor: 'red', shutterColors: ['yellow', 'salmon'] });
// I have a red house with yellow and salmon shutters
houseDescriptor({ houseColor: 'cyan' });
// I have a cyan house with red shutters
houseDescriptor();
// I have a green house with red shutters
houseDescriptor({ shutterColors: ['blue'] });
// I have a green house with blue shutters
houseDescriptor({});
// I have a green house with red shutters

类(Class)

JavaScript 中的 Class 其实 仍然是一个函数。只是它有一些特别之处,如首字母要大写、专门用来构造对象等。从根本上说,它用了新的、更简练的写法,把本来需要用原型继承(prototypal inheritance)单独列出来的代码,封装到class内部,从而使代码更精炼易懂,且不易出错。其实底层逻辑是没有变的。

常规写法:

1
2
3
4
5
6
7
8
9
10
function Plane(numEngines) {
  this.numEngines = numEngines;
  this.enginesActive = false;
}

// 如下方法可被所有实例化的对象继承
Plane.prototype.startEngines = function() {
  console.log('starting engines...');
  this.enginesActive = true;
};

用 ES6 重写:

1
2
3
4
5
6
7
8
9
10
11
class Plane {
  constructor(numEngines) {
    this.numEngines = numEngines;
    this.enginesActive = false;
  }

  startEngines() {
    console.log('starting engines...');
    this.enginesActive = true;
  }
}

静态方法(Static method)

静态方法 static 可直接作为类的方法调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Plane {
  constructor(numEngines) {
    this.numEngines = numEngines;
    this.enginesActive = false;
  }

  static badWeather(planes) {
    for (const plane of planes) {
      plane.enginesActive = false;
      console.log(plane.enginesActive);
    }
  }

  startEngines() {
    console.log('starting engines...');
    this.enginesActive = true;
  }
}

const plane1 = new Plane(2);
const plane2 = new Plane(3);

Plane.badWeather([plane1, plane2]); // false x 2

子类(Subclass)

子类引入了 2 个关键词:superextends。举一个简单例子,对 ES5 和 ES6 的不同写法做个对比。定义父类Tree,再定义子类Maple继承父类的部分属性和方法,并加入自己独有的方法:

ES5 的写法

原型链、继承、多个代码块与容易混淆的构造函数,让 ES5 的代码很难阅读与维护:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
function Tree(size, leaves) {
  this.size = typeof size === 'undefined' ? 10 : size;
  var defaultLeaves = {
    spring: 'green',
    summer: 'green',
    fall: 'orange',
    winter: null
  };
  this.leaves = typeof leaves === 'undefined' ? defaultLeaves : leaves;
  this.leafColor;
}

Tree.prototype.changeSeason = function(season) {
  this.leafColor = this.leaves[season];
  if (season === 'spring') {
    this.size += 1;
  }
};

function Maple(syrupQty, size, leaves) {
  Tree.call(this, size, leaves);
  this.syrupQty = typeof syrupQty === 'undefined' ? 15 : syrupQty;
}

Maple.prototype = Object.create(Tree.prototype);
Maple.prototype.constructor = Maple;

Maple.prototype.changeSeason = function(season) {
  Tree.prototype.changeSeason.call(this, season);
  if (season === 'spring') {
    this.syrupQty += 1;
  }
};

Maple.prototype.gatherSyrup = function() {
  this.syrupQty -= 3;
};

var myMaple = new Maple(15, 5);
myMaple.changeSeason('fall');
myMaple.gatherSyrup();
myMaple.changeSeason('spring');

ES6 的写法

有了classsuperextends的帮助,我们可以写出更易懂的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class Tree {
  constructor(
    size = '10',
    leaves = { spring: 'green', summer: 'green', fall: 'orange', winter: null }
  ) {
    this.size = size;
    this.leaves = leaves;
    this.leafColor = null;
  }

  changeSeason(season) {
    this.leafColor = this.leaves[season];
    if (season === 'spring') {
      this.size += 1;
    }
  }
}

class Maple extends Tree {
  // extends 为子类(Maple)继承父类(Tree)
  constructor(syrupQty = 15, size, leaves) {
    super(size, leaves); // super 在 constructor 中,作为函数,继承父类的属性
    this.syrupQty = syrupQty;
  }

  changeSeason(season) {
    super.changeSeason(season); // super 在方法中,作为对象,继承父类的方法
    if (season === 'spring') {
      this.syrupQty += 1;
    }
  }

  gatherSyrup() {
    this.syrupQty -= 3;
  }
}

const myMaple = new Maple(15, 5); // 糖浆数量(syrupQty)为15,树的尺寸(size)为5
myMaple.changeSeason('fall'); // 通过父类改变了季节
myMaple.gatherSyrup(); // 通过子类改变了糖浆数量
myMaple.changeSeason('spring'); // 通过父类改变季节后,通过子类改变糖浆数量

ES6 内置方法

唯一标识符(Symbol)

Symbol是在现有的 6 种数据类型(Undefined, Null, Boolean, Number, String, Object)之外的第 7 种类型。它用于为一个对象/属性设置唯一标识符。

1
2
3
4
5
6
7
8
const bowl = {
  [Symbol('apple')]: { color: 'red', weight: 136.078 },
  [Symbol('banana')]: { color: 'yellow', weight: 183.15 },
  [Symbol('orange')]: { color: 'orange', weight: 170.097 },
  [Symbol('banana')]: { color: 'yellow', weight: 176.845 }
};
console.log(bowl);
// 结果:Object {Symbol(apple): Object, Symbol(banana): Object, Symbol(orange): Object, Symbol(banana): Object}

上文的代码中,即使bowl中有 2 个banana,由于使用了symbol,即使描述一样,但 JavaScript 仍然允许它们并存,因为 symbol 是 唯一标识符

Set

Set是一个构造函数,它可以构造 可迭代、不重复、无索引 的数据结构。它是 数组 的变种。

1
2
const games = new Set();
console.log(games); // Set {}

Set相关方法:

  • .add() 增加一个元素
  • .delete() 删除一个元素,返回一个布尔类型的值(成功删除则为 true ,失败则为false )
  • .clear() 清空该 Set
  • .size() 包含的元素数量
  • .has() 传入一个元素,若 Set 包含该元素,则返回 true , 否则返回 false
  • .values() 或 .keys() 返回 Set 中所有元素,返回类型为 SetIterator
  • .next() (配合 .values() 使用) 返回一个对象,该对象包含下一个元素的值,以及一个布尔值,判断是否检索到最后一个元素:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const months = new Set([
  'January',
  'February',
  'March',
  'April',
  'May',
  'June',
  'July',
  'August',
  'September',
  'October',
  'November',
  'December'
]);
const iterator = months.values();
iterator.next(); // Object {value: 'January', done: false}
iterator.next(); // Object {value: 'February', done: false}

WeakSetSet类似,但只可包含对象,不能迭代,且没有clear()方法:

1
2
3
4
5
6
7
let student1 = { name: 'James', age: 26, gender: 'male' };
let student2 = { name: 'Julia', age: 27, gender: 'female' };
let student3 = { name: 'Richard', age: 31, gender: 'male' };

const roster = new WeakSet([student1, student2, student3]);
roster.add('Amanda'); // Uncaught TypeError: Invalid value used in weak set(…)
roster.clear(); // Uncaught TypeError: roster.clear is not a function

Map

Map是一个构造函数,它可以构造 可迭代、不重复、无索引 的数据结构。它是 对象 的变种。

1
2
const employees = new Map();
console.log(employees); // Map {}

Map相关方法:

  • .set() 添加一个对象:
1
2
3
4
5
6
7
8
9
const employees = new Map();

employees.set('james.parkes@udacity.com', {
  firstName: 'James',
  lastName: 'Parkes',
  role: 'Content Developer'
});

console.log(employees); // Map {'james.parkes@udacity.com' => Object {...}}
  • .delete() 删除一个对象
  • .clear() 清空该 Map
  • .has() 传入一个元素名称,若 Map 包含该元素,则返回 true , 否则返回 false
  • .get() 传入一个元素名称,得到该名称对应的键值
  • .values() 或 .keys() 返回 Map 中所有元素,返回类型为 MapIterator
  • .next()
    • (配合 .keys() 使用) 返回一个对象,该对象包含下一个对象名称,以及一个布尔值,判断是否检索到最后一个元素;
    • (配合 .values() 使用)返回一个对象,该对象包含下一个对象对应的键值,以及一个布尔值,判断是否检索到最后一个元素:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const members = new Map();

members.set('Evelyn', 75.68);
members.set('Liam', 20.16);
members.set('Sophia', 0);
members.set('Marcus', 10.25);

let iteratorObjForKeys = members.keys();
iteratorObjForKeys.next(); // Object {value: 'Evelyn', done: false}
iteratorObjForKeys.next(); // Object {value: 'Liam', done: false}

let iteratorObjForValues = members.values();
iteratorObjForValues.next(); // Object {value: 75.68, done: false}
iteratorObjForValues.next(); // Object {value: 20.16, done: false}

使用 for...of 循环,返回的是以对象名称、键值组成的数组:

1
2
3
4
5
6
7
8
9
for (const member of members) {
  console.log(member);
}
/**
['Evelyn', 75.68]
['Liam', 20.16]
['Sophia', 0]
['Marcus', 10.25]
/*

WeakMapweakSet 类似,只可包含对象,不能迭代,且没有clear()方法。

Promise

详见 这篇文章

代理(Proxy)

Proxy 对象介于真正的对象和调用代码之间。调用代码与 Proxy 交互,而不是真正的对象。要创建 Proxy 的话:

  • 使用 new Proxy() 构造函数
    • 将被代理的对象传入为第一个参数
    • 第二个参数是 handler(处理器)对象
  • handler 对象由 13 种不同的 trap 之一构成
  • trap 是一种函数,将截获对属性的调用,让你运行代码
  • 如果未定义 trap,默认行为会被发送给目标对象

Proxy 是一种强大的创建和管理对象之间的交互的新方式。

生成器(Generator)

用一个 * 放在 function 和函数名之间,就创建了一个生成器。生成器的目的是生成一个可迭代操作的函数,使一个本来自上而下运行、直到无路可走的函数中的代码变得更加可控,运行和暂停随时听我们指挥。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function* getEmployee() {
  console.log('the function has started');

  const names = [
    'Amanda',
    'Diego',
    'Farrin',
    'James',
    'Kagure',
    'Kavita',
    'Orit',
    'Richard'
  ];

  for (const name of names) {
    console.log(name);
    yield; // 生成器设置的暂停锚点,不经我们允许,它不会继续运行。
  }

  console.log('the function has ended');
}

const generatorIterator = getEmployee(); // 将生成器赋值给 generatorIterator
generatorIterator.next();
/** 输出结果
the function has started
Amanda
*/
generatorIterator.next(); // 输出结果:Diego

yield 同样可以返回值。我们把上面代码的 for...of 循环修改一下:

1
2
3
for (const name of names) {
    yield name;
}

此时,我们可以在每次函数暂停的时候,取到暂停时 name 的值:

1
2
3
4
const generatorIterator = getEmployee();
let result = generatorIterator.next();
result.value; // Amanda
generatorIterator.next().value; // Diego

不仅如此,我们还可以向生成器发送数据:

1
2
3
4
5
6
7
8
function* displayResponse() {
  const response = yield;
  console.log(`Your response is "${response}"!`);
}

const iterator = displayResponse();
iterator.next(); // 开始运行生成器
iterator.next('es6'); // 发送数据给生成器,打印结果:Your response is es6!

参考资料