最全 ECMAScript 攻略之 ES2015-ES6

1,986 阅读12分钟

推荐阮一峰大佬的ES 入门教程,中文文档没有比他更详细的了

像是解構賦值、箭頭函數、class 語法等等都是在 ES6 版本當中規範。因為 ES6 在 2015 年發布所以又叫做 ES2015,目前看起來似乎是用 ES + 年份居多,如 ES2016/ES2017。

let/const

为什么要引入 let/const

var 的缺陷-变量提升,变量提升带来的问题

  1. 变量容易在不被察觉的情况下呗覆盖掉
  2. 本应销毁的变量没有被销毁

JavaScript 是如何支持块级作用域的

let/const 的典型使用场景

如何使用 ES5 模拟 const(面试题)

todo。。。

如何使用 ES5 模拟块级作用域(面试题)

箭头函数(arrows)

箭头函数是使用=>语法的函数简写。与一般函数不同的是

  1. 函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。
  • this 对象的指向是可变的,但是在箭头函数中,它是固定的。
  1. 不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。
  2. 不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
  3. 不可以使用yield命令,因此箭头函数不能用作 Generator 函数。
var f = (v) => v;

// 等同于
var f = function (v) {
  return v;
};

function foo() {
  setTimeout(() => {
    console.log("id:", this.id);
  }, 100);
}

var id = 21;
// 箭头函数导致this总是指向函数定义生效时所在的对象({id: 42}),所以打印出来的是42
foo.call({ id: 42 });
// id: 42

// 对象不构成单独的作用域,使得this指向全局对象
globalThis.s = 21;
const obj = {
  s: 42,
  m: () => console.log(this.s),
};

obj.m(); // 21

更多详细内容参考ES 入门教程-箭头函数

类(Class)

// ES5
function Point(x, y) {
  this.x = x;
  this.y = y;
}

Point.prototype.toString = function () {
  return "(" + this.x + ", " + this.y + ")";
};

var p = new Point(1, 2);

// ES6
class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  toString() {
    return "(" + this.x + ", " + this.y + ")";
  }
}

更多详细内容参考ES 入门教程-Class

对象的扩展(enhanced object literals)

对象的属性的简洁表示法

let test1 = "a";
let test2 = "b";

// bad 👴
let obj = { test1: test1, test2: test2 };

// better 👶
let obj = { test1, test2 };
const foo = "bar";
const method = function () {
  return "Hello!";
};

const filed = "name";

const baz = {
  foo,
  method,
  [filed]: "小王",
};

// 等同于
const baz = {
  foo: foo,
  method: function () {
    return "Hello!";
  },
  name: "小王",
};

更多详细内容参考ES 入门教程-对象扩展

模板字符串

如果你厌倦了使用 + 将多个变量连接成一个字符串,那么这个简化技巧将让你不再头痛。

// 字符串中嵌入变量

// bad 👴
const welcome = "Hi " + test1 + " " + test2 + ".";
// better 👶
const welcome = `Hi ${test1} ${test2}`;
// 跨行字符串

// bad 👴
const data =
  "abc abc abc abc abc abc\n\t" + "test test,test test test test\n\t";
// better 👶
const data = `abc abc abc abc abc abc
         test test,test test test test`;

更多详细内容参考ES 入门教程-字符串模板

数组解构+扩展运算符

var [a] = [];

a === undefined; // true

var [a = 1] = [];
a === 1; // true

场景: 交换数组元素的位置

// bad 👴
const swapWay = (arr, i, j) => {
  const newArr = [...arr];

  let temp = newArr[i];

  newArr[i] = list[j];
  newArr[j] = temp;

  return newArr;
};

ES6 开始,从数组中的不同位置交换值变得容易多了

// better 👶
const swapWay = (arr, i, j) => {
  const newArr = [...arr];

 const [newArr[j],newArr[i]] = [newArr[i],newArr[j]];

  return newArr;
};

更多详细内容参考ES 入门教程-数组的扩展运算符

函数默认参数+剩余参数+扩展运算符

//如果没有传递y 或者y===undefined ,则y=12
function f(x, y = 12) {
  return x + y;
}
f(3) == 15;
function f(x, ...y) {
  // y 是一个数组
  return x * y.length;
}
f(3, "hello", true) == 6;
function f(x, y, z) {
  return x + y + z;
}
// Pass each elem of array as argument
f(...[1, 2, 3]) == 6;
// bad 👴
function add(test1, test2) {
  if (test1 === undefined) test1 = 1;
  if (test2 === undefined) test2 = 2;
  return test1 + test2;
}

// better 👶
add = (test1 = 1, test2 = 2) => test1 + test2;
add(); //output: 3

更多详细内容参考ES 入门教程-函数默认参数

块级作用域变量

随着 ES6 中引入 let/const 关键字,JS 才具有函数作用域和全局作用域,现在 JS 也可以有块级作用域了。

function f() {
  {
    let x;
    {
      // 正常,因为在一个新的块级作用域中
      const x = "sneaky";
      // const 定义的是常量无法被修改,因此会报错
      x = "foo";
    }
    // 在块级作用域中已声明x,因此会报错
    let x = "inner";
  }
}

更多详细内容参考ES 入门教程-unicode

遍历/迭代器+for..of(iterators + for..of)

一个数据结构只要部署了 Symbol.iterator 属性,就被视为具有 iterator 接口,就可以用 for...of 循环遍历它的成员。也就是说,for...of 循环内部调用的是数据结构的 Symbol.iterator 方法。

for ... offor ... inforEach()的替代方法,它循环访问可迭代的数据结构,如数组,映射,集合和字符串。

JavaScript 原有的 for...in 循环,只能获得对象的键名,不能直接获取键值。ES6 提供 for...of 循环,允许遍历获得键值。

var arr = ["a", "b", "c", "d"];

for (let a in arr) {
  console.log(a); // 0 1 2 3
}

for (let a of arr) {
  console.log(a); // a b c d
}

const str = "helloworld";
for (let a of str) {
  console.log(a); // h e l l o w o r l d
}

更多详细内容参考ES 入门教程-iterators

生成器(generators)

Generators 使用function *yield简化了迭代器的创建。 声明为function *的函数一个遍历器对象,也就是说,Generator 函数是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。

生成器是迭代器的子类型,因此具有nextthrow方法。

yield表达式是暂停执行的标记,而next方法可以恢复执行

注意:ES7 出现后,推荐使用await

function* foo() {
  yield 1;
  yield 2;
  yield 3;
  yield 4;
  yield 5;
  return 6;
}

for (let v of foo()) {
  console.log(v);
}
// 1 2 3 4 5

下面是一个利用 Generator 函数和for...of循环,实现斐波那契数列的例子。

var fibonacci = {
  [Symbol.iterator]: function* () {
    let [prev, curr] = [0, 1];
    for (;;) {
      yield curr;
      [prev, curr] = [curr, prev + curr];
    }
  },
};

for (var n of fibonacci) {
  //
  if (n > 1000) break;
  console.log(n);
}

从上面代码可见,使用for...of语句时不需要使用next方法。

利用for...of循环,可以写出遍历任意对象(object)的方法。原生的 JavaScript 对象没有迭代器接口,无法使用for...of循环,通过 Generator 函数为它加上这个接口,就可以用了。

生成器(Generator) 实质上继承了迭代器(Iterator)

interface Generator extends Iterator {
  next(value?: any): IteratorResult;
  throw(exception: any);
}

更多详细内容参考ES 入门教程-iterators

Unicode

ES6 增强了 Unicode 的功能,包括

  • 支持字符的 Unicode 表示法

举例来说,“中”的 Unicode 码点是 U+4e2d,你可以直接在字符串里面输入这个汉字,也可以输入它的转义形式\u4e2d,两者是等价的。

"中" === "\u4e2d"; // true
  • 使用/u匹配码点的正则表达式
// new RegExp behaviour, opt-in ‘u’
"𠮷".match(/./u)[0].length == 2;
  • 获取 32 位的 UTF-16 字符的码点-codePointAt
"𠮷".codePointAt(0) == 0x20bb7;

let s = "𠮷a";
for (let ch of s) {
  console.log(ch.codePointAt(0).toString(16));
}
// 20bb7
// 61

更多详细内容参考ES 入门教程-unicode

模块化(modules)

ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。

使用 export defaultexport 进行导出

// math.js
export const pi = 3.141593;

export default function sum(x, y) {
  return x + y;
}

使用 import 进行导入

// app.js
import sum, { pi } from "./math";

alert("2π = " + sum(pi, pi));

更多详细内容参考ES 入门教程-module

模块加载器规则(module loaders)

模块加载器支持:

  • 异步加载
  • 代码是在模块作用域之中运行,而不是在全局作用域运行。模块内部的顶层变量,外部不可见。
  • 模块之中,顶层的 this 关键字返回 undefined,而不是指向 window。也就是说,在模块顶层使用 this 关键字,是无意义的
//index.js
const x = 1;

console.log(x === window.x); //false
console.log(this === undefined); // true

利用顶层的 this 等于 undefined 这个语法点,可以侦测当前代码是否在 ES6 模块之中。

const isNotModuleScript = this !== undefined;

更多详细内容参考ES 入门教程-module-loader

import and export

Map + Set + Weakmap + Weakset

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

// Sets
var s = new Set();
s.add("hello").add("goodbye").add("hello");
s.size === 2;
s.has("hello") === true;

ES6 提供了 Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应,是一种更完善的 Hash 结构实现。如果你需要“键值对”的数据结构,Map 比 Object 更合适。

// Maps
var m = new Map();
m.set("hello", 42);
m.set(s, 34);
m.get(s) == 34;

WeakMap 结构与 Map 结构类似,也是用于生成键值对的集合。

WeakMapMap 的区别有两点。

  1. WeakMap 只接受对象作为键名(null 除外),不接受其他类型的值作为键名。
  2. WeakMap 的键名所指向的对象,不计入垃圾回收机制。
// Weak Maps
var wm = new WeakMap();
wm.set(s, { extra: 42 });
wm.size === undefined;

WeakSet 结构与 Set 类似,也是不重复的值的集合。但是,它与 Set 有两个区别。

  1. WeakSet 的成员只能是对象,而不能是其他类型的值。
  2. WeakSet 中的对象都是弱引用
// Weak Sets
var ws = new WeakSet();
ws.add({ data: 42 });
// Because the added object has no other references, it will not be held in the set

更多详细内容参考ES 入门教程-Set 和 Map

代理(proxies)

Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改。 可以用于操作拦截,日志记录/分析等。

// 代理一个普通对象
var target = {};
var handler = {
  get: function (receiver, name) {
    return `Hello, ${name}!`;
  },
};

var p = new Proxy(target, handler);

// true
p.world === "Hello, world!";

下面是 Proxy 所有可以代理的"元操作"

var handler =
{
  get:...,
  set:...,
  has:...,
  deleteProperty:...,
  apply:...,
  construct:...,
  getOwnPropertyDescriptor:...,
  defineProperty:...,
  getPrototypeOf:...,
  setPrototypeOf:...,
  enumerate:...,
  ownKeys:...,
  preventExtensions:...,
  isExtensible:...
}

MDN-handler.get()

// 代理一个函数对象
var target = function () {
  return "I am the target";
};
var handler = {
  apply: function (receiver, ...args) {
    return "I am the proxy";
  },
};

var p = new Proxy(target, handler);
//true
p() === "I am the proxy";

更多详细内容参考ES 入门教程-proxy

symbols

ES6 引入了一种新的原始数据类型 Symbol,表示独一无二的值

Symbol 值通过 Symbol 函数生成。这就是说,对象的属性名现在可以有两种类型,一种是原来就有的字符串,另一种就是新增的 Symbol 类型。凡是属性名属于 Symbol 类型,就都是独一无二的,可以保证不会与其他属性名产生冲突。

var MyClass = (function () {
  //
  var key = Symbol("key");

  function MyClass(privateData) {
    this[key] = privateData;
  }

  MyClass.prototype = {
    doStuff: function () {
      this[key];
    },
  };

  return MyClass;
})();

var c = new MyClass("hello");
// true
console.log(c["key"] === undefined);

创建 Symbol 的时候,可以添加一个描述。

const sym = Symbol("foo");

上面代码中,sym 的描述就是字符串 foo

Symbol 作为属性名,遍历对象的时候,该属性不会出现在 for...infor...of 循环中,也不会被Object.keys()Object.getOwnPropertyNames()JSON.stringify()返回。

但是,它也不是私有属性,有一个 Object.getOwnPropertySymbols()方法,可以获取指定对象的所有 Symbol 属性名。该方法返回一个数组,成员是当前对象的所有用作属性名的 Symbol 值。

const obj = {};
let a = Symbol("a");
let b = Symbol("b");

obj[a] = "Hello";
obj[b] = "World";

const objectSymbols = Object.getOwnPropertySymbols(obj);

objectSymbols;
// [Symbol(a), Symbol(b)]

更多详细内容参考ES 入门教程-symbol

期约(promises)

Promise 是一个用于异步编程的库,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。 许多现有的 JavaScript 库已经使用了 Promise

function timeout(duration = 0) {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, duration);
  });
}

var p = timeout(1000)
  .then(() => {
    return timeout(2000);
  })
  .then(() => {
    throw new Error("hmm");
  })
  .catch((err) => {
    return Promise.all([timeout(100), timeout(200)]);
  });

更多详细内容参考ES 入门教程-promise

math + number + string + array + object APIs

添加了许多类型的扩展方法,包括:Math ,Array ,String ,Object

Number.EPSILON;
Number.isInteger(Infinity); // false
Number.isNaN("NaN"); // false

Math.acosh(3); // 1.762747174039086
Math.hypot(3, 4); // 5
Math.imul(Math.pow(2, 32) - 1, Math.pow(2, 32) - 2); // 2

"abcde".includes("cd"); // true
"abc".repeat(3); // "abcabcabc"

Array.from(document.querySelectorAll("*")); // Returns a real Array
Array.of(1, 2, 3) // Similar to new Array(...), but without special one-arg behavior
  [(0, 0, 0)].fill(7, 1) // [0,7,7]
  [(1, 2, 3)].find((x) => x == 3) // 3
  [(1, 2, 3)].findIndex((x) => x == 2) // 1
  [(1, 2, 3, 4, 5)].copyWithin(3, 0) // [1, 2, 3, 1, 2]
  [("a", "b", "c")].entries() // iterator [0, "a"], [1,"b"], [2,"c"]
  [("a", "b", "c")].keys() // iterator 0, 1, 2
  [("a", "b", "c")].values(); // iterator "a", "b", "c"

Object.assign(Point, { origin: new Point(0, 0) });

更多详细内容参考 ES 入门教程:

二进制和八进制(binary and octal literals)

两种新的数字表示形式。

  • 二进制: 0b 开头
  • 八进制: 0o 开头
0b111110111 === 503; // true
0o767 === 503; // true

reflect api

reflect API 公开对象上的运行时级别的元操作

最重要的目的是配合 Proxy 使用,执行原生行为

Object操作都变成函数行为。某些Object操作是命令式,比如name in objdelete obj[name],而Reflect.has(obj, name)Reflect.deleteProperty(obj, name)让它们变成了函数行为。

// 老写法
"assign" in Object; // true

// 新写法
Reflect.has(Object, "assign"); // true

更多详细内容参考ES 入门教程-reflect

尾调用(tail calls)

  • 尾调用:某个函数的最后一步是返回并调用另一个函数
  • 尾递归:函数调用自身,称为递归。如果尾调用自身,就称为尾递归。
  • 尾调用优化

注意,目前只有 Safari 浏览器支持尾调用优化,Chrome 和 Firefox 都不支持。这里就不深入研究了 😁

function factorial(n, acc = 1) {
  if (n <= 1) return acc;
  return factorial(n - 1, n * acc);
}

// 大多数浏览器中都会出现 堆栈溢出 的错误,
// 但是在 ES6的Safari中是安全的
factorial(100000);

更多详细内容参考ES 入门教程-尾调用


最后

文章浅陋,欢迎各位看官评论区留下的你的见解!

觉得有收获的同学欢迎点赞,关注一波!

good

往期文章