ES6-你不得不知道的知識點

寫在前面

距離ES6正式版本的發布也快兩年了,在這兩年時間里,前端世界時刻都在發生著巨大的變化。今天,當我們去查看一些近期發布的開源項目,一些更時髦的框架或庫時,都會發現ES6的使用已經非常的頻繁和普遍了。針對這樣的情況,筆者從實際代碼閱讀中經常會碰到的ES6語法出發,從中提煉了一些重要的知識點,一方面是作為筆記備查,同時也希望能對大家有所幫助。
let

塊級作用域
不存在變量提升
暫時性死區:ES6明確規定,如果區塊中存在let和const命令,這個區塊對這些命令聲明的變量,從一開始就形成了封閉作用域。凡是在聲明之前就使用這些變量,就會報錯。
不允許重復聲明

var a = [];
for (let i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6](); //6

for循環還有一個特別之處,就是循環語句部分是一個父作用域,而循環體內部是一個單獨的子作用域。

for (let i = 0; i < 3; i++) {
  let i = 'abc';
  console.log(i);
}
// abc
// abc
// abc

const

const聲明一個只讀的常量。一旦聲明,常量的值就不能改變。
這意味著,const一旦聲明變量,就必須立即初始化,不能留到以后賦值。
對于復合類型的變量,變量名不指向數據,而是指向數據所在的地址。const命令只是保證變量名指向的地址不變,并不保證該地址的數據不變,所以將一個對象聲明為常量必須非常小心。

關于全局變量

var命令和function命令聲明的全局變量,依舊是頂層對象的屬性;另一方面規定,let命令、const命令、class命令聲明的全局變量,不屬于頂層對象的屬性。也就是說,從ES6開始,全局變量將逐步與頂層對象的屬性脫鉤。

var a = 1;
// 如果在Node的REPL環境,可以寫成global.a
// 或者采用通用方法,寫成this.a
window.a // 1
let b = 1;
window.b // undefined

解構

本質上,這種寫法屬于“模式匹配”,只要等號兩邊的模式相同,左邊的變量就會被賦予對應的值。
如果解構不成功,變量的值就等于undefined。

let [foo, [[bar], baz]] = [1, [[2], 3]];
foo // 1
bar // 2
baz // 3

let [ , , third] = ["foo", "bar", "baz"];
third // "baz"

let [x, , y] = [1, 2, 3];
x // 1
y // 3

let [head, ...tail] = [1, 2, 3, 4];
head // 1
tail // [2, 3, 4]

let [x, y, ...z] = ['a'];
x // "a"
y // undefined
z // []

對于 Set 結構,也可以使用數組的解構賦值

let [x, y, z] = new Set(['a', 'b', 'c']);
x // "a"

事實上,只要某種數據結構具有 Iterator 接口,都可以采用數組形式的解構賦值。

function* fibs() {
  let a = 0;
  let b = 1;
  while (true) {
    yield a;
    [a, b] = [b, a + b];
  }
}

let [first, second, third, fourth, fifth, sixth] = fibs();
sixth // 5

解構不僅可以用于數組,還可以用于對象。
對象的解構與數組有一個重要的不同。數組的元素是按次序排列的,變量的取值由它的位置決定;而對象的屬性沒有次序,變量必須與屬性同名,才能取到正確的值。

let { bar, foo } = { foo: "aaa", bar: "bbb" };
foo // "aaa"
bar // "bbb"

let { baz } = { foo: "aaa", bar: "bbb" };
baz // undefined

字符串也可以解構賦值。這是因為此時,字符串被轉換成了一個類似數組的對象。

const [a, b, c, d, e] = 'hello';
a // "h"
b // "e"
c // "l"
d // "l"
e // "o"

類似數組的對象都有一個length屬性,因此還可以對這個屬性解構賦值。

let {length : len} = 'hello';
len // 5

解構用途

//1.交換變量的值
let x = 1;
let y = 2;

[x, y] = [y, x];

//2.從函數返回多個值
// 返回一個數組

function example() {
  return [1, 2, 3];
}
let [a, b, c] = example();

// 返回一個對象

function example() {
  return {
    foo: 1,
    bar: 2
  };
}
let { foo, bar } = example();

//3.函數參數的定義
// 參數是一組有次序的值
function f([x, y, z]) { ... }
f([1, 2, 3]);

// 參數是一組無次序的值
function f({x, y, z}) { ... }
f({z: 3, y: 2, x: 1});

//4.提取JSON數據
let jsonData = {
  id: 42,
  status: "OK",
  data: [867, 5309]
};

let { id, status, data: number } = jsonData;

console.log(id, status, number);
// 42, "OK", [867, 5309]

//5.函數參數的默認值
jQuery.ajax = function (url, {
  async = true,
  beforeSend = function () {},
  cache = true,
  complete = function () {},
  crossDomain = false,
  global = true,
  // ... more config
}) {
  // ... do stuff
};

//6.遍歷Map結構
var map = new Map();
map.set('first', 'hello');
map.set('second', 'world');

for (let [key, value] of map) {
  console.log(key + " is " + value);
}
// first is hello
// second is world

// 獲取鍵名
for (let [key] of map) {
  // ...
}

// 獲取鍵值
for (let [,value] of map) {
  // ...
}

//7.輸入模塊的指定方法
const { SourceMapConsumer, SourceNode } = require("source-map");

模板字符串

模板字符串(template string)是增強版的字符串,用反引號(`)標識。它可以當作普通字符串使用,也可以用來定義多行字符串,或者在字符串中嵌入變量。

// 普通字符串
`In JavaScript '\n' is a line-feed.`

// 多行字符串
`In JavaScript this is
 not legal.`

console.log(`string text line 1
string text line 2`);

// 字符串中嵌入變量
var name = "Bob", time = "today";
`Hello ${name}, how are you ${time}?`
var x = 1;
var y = 2;

`${x} + ${y} = ${x + y}`
// "1 + 2 = 3"

`${x} + ${y * 2} = ${x + y * 2}`
// "1 + 4 = 5"

var obj = {x: 1, y: 2};
`${obj.x + obj.y}`
// 3

箭頭函數

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

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

var sum = (num1, num2) => num1 + num2;
// 等同于
var sum = function(num1, num2) {
  return num1 + num2;
};

//箭頭函數可以與變量解構結合使用
const full = ({ first, last }) => first + ' ' + last;

// 等同于
function full(person) {
  return person.first + ' ' + person.last;
}

// 正常函數寫法
[1,2,3].map(function (x) {
  return x * x;
});

// 箭頭函數寫法
[1,2,3].map(x => x * x);

// 正常函數寫法
var result = values.sort(function (a, b) {
  return a - b;
});

// 箭頭函數寫法
var result = values.sort((a, b) => a - b);

const numbers = (...nums) => nums;

numbers(1, 2, 3, 4, 5)
// [1,2,3,4,5]

//with rest arguments
const headAndTail = (head, ...tail) => [head, tail];

headAndTail(1, 2, 3, 4, 5)
// [1,[2,3,4,5]]

==使用注意點==

箭頭函數有幾個使用注意點。
(1)函數體內的this對象,就是定義時所在的對象,而不是使用時所在的對象。
(2)不可以當作構造函數,也就是說,不可以使用new命令,否則會拋出一個錯誤。
(3)不可以使用arguments對象,該對象在函數體內不存在。如果要用,可以用Rest參數代替。
(4)不可以使用yield命令,因此箭頭函數不能用作Generator函數。
上面四點中,第一點尤其值得注意。this對象的指向是可變的,但是在箭頭函數中,它是固定的。

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

var id = 21;

foo.call({ id: 42 });
// id: 42

上面代碼中,setTimeout的參數是一個箭頭函數,這個箭頭函數的定義生效是在foo函數生成時,而它的真正執行要等到100毫秒后。如果是普通函數,執行時this應該指向全局對象window,這時應該輸出21。但是,箭頭函數導致this總是指向函數定義生效時所在的對象(本例是{id: 42}),所以輸出的是42。
箭頭函數可以讓setTimeout里面的this,綁定定義時所在的作用域,而不是指向運行時所在的作用域。下面是另一個例子。

function Timer() {
  this.s1 = 0;
  this.s2 = 0;
  // 箭頭函數
  setInterval(() => this.s1++, 1000);
  // 普通函數
  setInterval(function () {
    this.s2++;
  }, 1000);
}

var timer = new Timer();

setTimeout(() => console.log('s1: ', timer.s1), 3100);
setTimeout(() => console.log('s2: ', timer.s2), 3100);
// s1: 3
// s2: 0

上面代碼中,Timer函數內部設置了兩個定時器,分別使用了箭頭函數和普通函數。前者的this綁定定義時所在的作用域(即Timer函數),后者的this指向運行時所在的作用域(即全局對象)。所以,3100毫秒之后,timer.s1被更新了3次,而timer.s2一次都沒更新。
箭頭函數可以讓this指向固定化,這種特性很有利于封裝回調函數。下面是一個例子,DOM事件的回調函數封裝在一個對象里面。

var handler = {
  id: '123456',

  init: function() {
    document.addEventListener('click',
      event => this.doSomething(event.type), false);
  },

  doSomething: function(type) {
    console.log('Handling ' + type  + ' for ' + this.id);
  }
};

上面代碼的init方法中,使用了箭頭函數,這導致這個箭頭函數里面的this,總是指向handler對象。否則,回調函數運行時,this.doSomething這一行會報錯,因為此時this指向document對象。
this指向的固定化,并不是因為箭頭函數內部有綁定this的機制,實際原因是箭頭函數根本沒有自己的this,導致內部的this就是外層代碼塊的this。正是因為它沒有this,所以也就不能用作構造函數。
所以,箭頭函數轉成ES5的代碼如下。

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

// ES5
function foo() {
  var _this = this;

  setTimeout(function () {
    console.log('id:', _this.id);
  }, 100);
}

除了this,以下三個變量在箭頭函數之中也是不存在的,指向外層函數的對應變量:arguments、super、new.target。
另外,由于箭頭函數沒有自己的this,所以當然也就不能用call()、apply()、bind()這些方法去改變this的指向

class

ES6提供了更接近傳統語言的寫法,引入了Class(類)這個概念,作為對象的模板。通過class關鍵字,可以定義類。基本上,ES6的class可以看作只是一個語法糖,它的絕大部分功能,ES5都可以做到,新的class寫法只是讓對象原型的寫法更加清晰、更像面向對象編程的語法而已。上面的代碼用ES6的“類”改寫,就是下面這樣。

//定義類
class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

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

//繼承
class ColorPoint extends Point {
  constructor(x, y, color) {
    super(x, y); // 調用父類的constructor(x, y)
    this.color = color;
  }

  toString() {
    return this.color + ' ' + super.toString(); // 調用父類的toString()
  }
}

上面代碼中,constructor方法和toString方法之中,都出現了super關鍵字,它在這里表示父類的構造函數,用來新建父類的this對象。
子類必須在constructor方法中調用super方法,否則新建實例時會報錯。這是因為子類沒有自己的this對象,而是繼承父類的this對象,然后對其進行加工。如果不調用super方法,子類就得不到this對象。
在子類的構造函數中,只有調用super之后,才可以使用this關鍵字,否則會報錯。這是因為子類實例的構建,是基于對父類實例加工,只有super方法才能返回父類實例。

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

class ColorPoint extends Point {
  constructor(x, y, color) {
    this.color = color; // ReferenceError
    super(x, y);
    this.color = color; // 正確
  }
}

super 關鍵字

super這個關鍵字,既可以當作函數使用,也可以當作對象使用。在這兩種情況下,它的用法完全不同。
第一種情況,super作為函數調用時,代表父類的構造函數。ES6 要求,子類的構造函數必須執行一次super函數。
作為函數時,super()只能用在子類的構造函數之中,用在其他地方就會報錯。
第二種情況,super作為對象時,指向父類的原型對象。

class A {
  p() {
    return 2;
  }
}

class B extends A {
  constructor() {
    super();
    console.log(super.p()); // 2
  }
}

let b = new B();

這里需要注意,由于super指向父類的原型對象,所以定義在父類實例上的方法或屬性,是無法通過super調用的。

class A {
  constructor() {
    this.p = 2;
  }
}

class B extends A {
  get m() {
    return super.p;
  }
}

let b = new B();
b.m // undefined
//上面代碼中,p是父類A實例的屬性,super.p就引用不到它。

Class 的靜態方法

類相當于實例的原型,所有在類中定義的方法,都會被實例繼承。如果在一個方法前,加上static關鍵字,就表示該方法不會被實例繼承,而是直接通過類來調用,這就稱為“靜態方法”。

class Foo {
  static classMethod() {
    return 'hello';
  }
}

Foo.classMethod() // 'hello'

var foo = new Foo();
foo.classMethod()
// TypeError: foo.classMethod is not a function

上面代碼中,Foo類的classMethod方法前有static關鍵字,表明該方法是一個靜態方法,可以直接在Foo類上調用(Foo.classMethod()),而不是在Foo類的實例上調用。如果在實例上調用靜態方法,會拋出一個錯誤,表示不存在該方法。
父類的靜態方法,可以被子類繼承。

class Foo {
  static classMethod() {
    return 'hello';
  }
}

class Bar extends Foo {
}

Bar.classMethod(); // 'hello'

//靜態方法也是可以從super對象上調用的。
class Foo {
  static classMethod() {
    return 'hello';
  }
}

class Bar extends Foo {
  static classMethod() {
    return super.classMethod() + ', too';
  }
}

Bar.classMethod();

Module

模塊功能主要由兩個命令構成:export和import。export命令用于規定模塊的對外接口,import命令用于輸入其他模塊提供的功能。

// 第一組
export default function crc32() { // 輸出
  // ...
}

import crc32 from 'crc32'; // 輸入

// 第二組
export function crc32() { // 輸出
  // ...
};

import {crc32} from 'crc32'; // 輸入

上面代碼的兩組寫法,第一組是使用export default時,對應的import語句不需要使用大括號;第二組是不使用export default時,對應的import語句需要使用大括號。
export default命令用于指定模塊的默認輸出。顯然,一個模塊只能有一個默認輸出,因此export default命令只能使用一次。所以,import命令后面才不用加大括號,因為只可能對應一個方法。
本質上,export default就是輸出一個叫做default的變量或方法,然后系統允許你為它取任意名字。

import _, { each } from 'lodash';
//對應上面代碼的export語句如下。
export default function (obj) {
  // ···
}

export function each(obj, iterator, context) {
  // ···
}

export { each as forEach };

ES6 模塊與 CommonJS 模塊的差異

討論 Node 加載 ES6 模塊之前,必須了解 ES6 模塊與 CommonJS 模塊完全不同。
它們有兩個重大差異。
1.CommonJS 模塊輸出的是一個值的拷貝,ES6 模塊輸出的是值的引用。
2.CommonJS 模塊是運行時加載,ES6 模塊是編譯時輸出接口
第二個差異是因為 CommonJS 加載的是一個對象(即module.exports屬性),該對象只有在腳本運行完才會生成。而 ES6 模塊不是對象,它的對外接口只是一種靜態定義,在代碼靜態解析階段就會生成。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 以下內容是我在學習和研究ES6時,對ES6的特性、重點和注意事項的提取、精練和總結,可以做為ES6特性的字典;在本...
    科研者閱讀 3,150評論 2 9
  • 函數參數的默認值 基本用法 在ES6之前,不能直接為函數的參數指定默認值,只能采用變通的方法。 上面代碼檢查函數l...
    呼呼哥閱讀 3,459評論 0 1
  • class的基本用法 概述 JavaScript語言的傳統方法是通過構造函數,定義并生成新對象。下面是一個例子: ...
    呼呼哥閱讀 4,128評論 3 11
  • 一、ES6簡介 ? 歷時將近6年的時間來制定的新 ECMAScript 標準 ECMAScript 6(亦稱 ...
    一歲一枯榮_閱讀 6,113評論 8 25
  • 三,字符串擴展 3.1 Unicode表示法 ES6 做出了改進,只要將碼點放入大括號,就能正確解讀該字符。有了這...
    eastbaby閱讀 1,566評論 0 8