ES6,你不得不學!

在沒有學習 ES6 以前,學習 React,真的是一件很是痛苦的事情。即便以前你對 ES5 有着很好的基礎,包括閉包、函數、原型鏈和繼承,可是 React 中已經廣泛使用 ES6 的語法,包括 modules、class、箭頭函數等,還有 JSX 語法。因此,在學習 React 以前必定要先學習 ES6。javascript

關於 ES6 你必需要知道的一個教程,ECMAScript 6入門。這本書對於 ES6 的講解很是詳細,一步一步跟着來,絕對會對 ES6 的語法都瞭解到。html

學習 ES6,還要知道一個 ES6 的語法編譯器,Babel。ES6 出來好久了,並非全部瀏覽器都支持,Babel 就能夠把 ES6 代碼轉換成 ES5,讓全部瀏覽器都支持你寫的代碼。Babel 內嵌了對 JSX 的支持,學習 React 必備。在線實驗是一個 Babel 的在線編譯器,能夠用來練習 ES6 語法,並實時觀測轉換成 ES5 的代碼效果。前端

準備工做作完了,接下來開始今天的主題,你不得不學的 ES6!java

箭頭函數

講真,自從出了箭頭函數以後,不再用擔憂 this 問題了,並且就簡化代碼這一方面來講,箭頭函數可謂是裝逼神器。node

箭頭函數有幾點須要注意,若是 return 值就只有一行表達式,能夠省去 return,默認表示該行是返回值,不然須要加一個大括號和 return。若是參數只有一個,也能夠省去括號,兩個則須要加上括號。好比下面的例子:python

var f = v => v*2;
// 等價於
var f = function(v){
  return v*2;
}

// 判斷偶數
var isEven = n => n % 2 == 0;

// 須要加 return
var = (a, b) => {
  if(a >= b)
    return a;
  return b;
}

還有 this 的問題,我以爲這篇文章說的很是好。普通函數的 this 是可變的,咱們把函數歸爲兩種狀態,一種是定義時,一種是執行時,若是仔細研究會發現,函數中的 this 始終是指向函數執行時所在的對象。好比全局函數執行時,this 執行 window,對象的方法執行時,this 執行該對象,這就是函數 this 的可變。而箭頭函數中的 this 是固定的,看下面的例子:react

function obj(){
  setTimeout(()=>console.log(this.id), 20);
}
var id = 1;
obj.call({id: 2}); // 2

執行的結果是 2 而不是全局的 1,表示 setTimeout 函數執行的時候,this 指向的不是 window,這和普通函數是有區別的。es6

實際上,箭頭函數並無 this 對象,將箭頭函數轉成 ES5 會發現:面試

// ES6
function obj() {
  setTimeout(()=>console.log(this.id), 20);
}

// ES5
function foo() {
  var _this = this;
  setTimeout(function () {
    console.log(_this.id);
  }, 20);
}

經過 call aply 等方法是沒法綁定 箭頭函數中的 this:正則表達式

var f = () => this.x;
var x = 1;
f.call({x: 2}); // 1

對 this 的一個總結就是 在對象的方法中直接使用箭頭函數,會指向 window,其餘箭頭函數 this 會指向上一層的 this,箭頭函數並無存儲 this:

var obj = {
  id: 1,
  foo: ()=>{
    return this.id;
  }
}
var id = 2;
obj.foo(); // 2

除了 this 以外,箭頭函數的 arguments 也是不存在,不能使用 new 來構造,也不能使用 yield 命令。

class

盼星星盼月亮,終於盼來了 JS 的繼承。可是 ES6 中的繼承和已經很完善的 ES5 中流行的繼承庫,到底有多少差別?

先來看一個例子:

//定義類
class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
  // 注意函數構造的方式
  toString() {
    return '(' + this.x + ', ' + this.y + ')';
  }
}
var p1 = new Point(5, 5);
p1.toString(); //"(5, 5)"

typeof Point // function
p1.constructor == Point //true

直接使用 class 關鍵字,constructor 做爲構造方法,函數能夠直接 toString(){} 的方式。

可是,class 的本質仍然是函數,是構造函數的另一種寫法。既然 class 的本質是函數,那麼必不可少的一些 proto,prototype 方法也是存在的。

關於 class 的繼承

經過關鍵字 extends 能夠實現 class 的繼承,

class Square extends Point{
  constructor(x){
    super(x, x);
  }
  toString(){
    return super.toString() + 'Square!';
  }
}
var s1 = new Square(4);
s1.toString(); //"(4, 4)Square!"
s1 instanceof Point // true
s1 instanceof Square // true

既然說到了繼承,對 es5 中繼承瞭解到小夥伴,確定會疑惑關於 class 中的 proto 和 prototype 是一個什麼樣的關係。

子類的 proto 指向父類,子類的 prototype 的 proto 指向父類的 prototype,這和 ES5 並無區別。

Square.__proto__ === Point
// true
Square.prototype.__proto__ === Point.prototype
// true

super 關鍵字

在 Java 等語言中,是有 super 繼承父類函數,JS 中更加靈活,能夠用做父類的構造函數,又能夠用做對象。

子類的 constructor 必需要調用 super 方法,且只能在 constructor 方法中調用,其餘地方調用會報錯。

class A {
  constructor(a){
    this.x = a;
  }
}
A.prototype.y = 2;
class B extends A{
  constructor(a){
    super();
  }
  getY(){
    super() // 報錯
    return super.y
  }
}

原生構造函數的繼承

對於一些原生的構造函數,好比 Array,Error,Object,String 等,在 ES5 是沒法經過 Object.create 方法實現原生函數的內部屬性,原生函數內部的 this 沒法綁定,內部屬性得到不了。原生構造函數的繼承

ES6 的 class 能夠解決這個問題。

class MyArray extends Array {
  constructor(...args) {
    super(...args);
  }
}

var arr = new MyArray();
arr[0] = 12;
arr.length // 1

arr.length = 0;
arr[0] // undefined

extends 關鍵字不只能夠用來繼承類,還能用來繼承原生的構造函數,在原生函數的基礎上,自定義本身的函數。

靜態方法

ES6 支持 static 關鍵字,該關鍵字定義的方法,不會被實例繼承,但能夠被子類繼承:

class A{
  static add(x, y){
    return x + y;
 }
}
A.add(1, 2);
var a = new A();
a.add()// error
class B extends A{}
B.add(2, 2)// 4

Module

ES6 以前,JS 一直沒有 modules 體系,解決外部包的問題經過 CommonJS 和 AMD 模塊加載方案,一個用於服務器,一個用於瀏覽器。ES6 提出的 modules (import/export)方案徹底能夠取代 CommonJS 和 AMD 成爲瀏覽器和服務器通用的模塊解決方案。

關於模塊,就只有兩個命令,import 用於導入其餘模塊,export 用於輸出模塊。

// profile.js
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;

export {firstName, lastName, year};

// main.js
import {firstName, lastName, year} from './profile';
console.log(firstName, lastName) // Michael Jackson

import 加載的模塊能夠只加載用到的,可是必須使用同名的原則,能夠用 as 來解決名字問題,一樣,as 也能夠解決 export 問題:

//main.js
import { lastName as surname } from './profile';
console.log(surname); // Jackson

//profile.js
export {firstName as name}

export 能夠輸出的內容不少,包括變量、函數、類,貌似均可以輸出,還能夠藉助 export default 來加載默認輸出。

//default.js
function add(a, b){
  return a + b;
}
export default add;
// 實際上
export {add as default};

// main.js
import add from './default'
//實際上 add 名字能夠隨便起
import {default as add} from './default'

模塊加載的實質

這部分 ES6模塊加載的實質 徹底只能參考了,由於對模塊加載用的很少,沒有一點經驗,可是看到做者提到了拷貝和引用,感受逼格很高的樣子。

ES6模塊加載的機制,與CommonJS模塊徹底不一樣。CommonJS模塊輸出的是一個值的拷貝,而ES6模塊輸出的是值的引用。

好比一個 CommonJS 加載的例子:

// lib.js
var counter = 3;
function incCounter() {
  counter++;
}
module.exports = {
  counter: counter,
  incCounter: incCounter,
};

// main.js
var mod = require('./lib');

console.log(mod.counter);  // 3
mod.incCounter();
console.log(mod.counter); // 3

這個值會被 mod 緩存,而取不到原始的值。

ES6 中不同,它只是生成一個引用,當真正須要的時候,纔會到模塊裏去取值,

// lib.js
export let counter = 3;
export function incCounter() {
  counter++;
}

// main.js
import { counter, incCounter } from './lib';
console.log(counter); // 3
incCounter();
console.log(counter); // 4

循環加載

循環加載也比較有意思,常常能看到 nodejs 中出現加載同一個模塊,而循環加載卻不常見,nodejs 使用 CommonJS 模塊機制,CommonJS 的循環加載採用的是加載多少,輸出多少,就像是咱們平時打了斷點同樣,會跳到另一個文件,執行完在跳回來。

//a.js
exports.done = '1';
var a = require('./b.js');
console.log('half a=%s', a);
exports.done = '3';
console.log('done a');

//b.js
exports.done = '2';
var b = require('./a.js');
console.log('half b=%s', b);
exports.done = '4';
console.log('done b');

//main.js
var a = require('./a.js');
var b = require('./b.js');
console.log('all done! a=%s,b=%s',a,b)

node main.js 的結果:

half a=2
done a
half b=3
done b
all done! a=3,b=4

這就是 CommonJS 所謂的循環加載。

而 ES6 採用的加載模式也不同,由於使用動態引用,必需要開發者保證能 import 到值:

// a.js以下
import {bar} from './b.js';
console.log('a.js');
console.log(bar);
export let foo = 'foo';

// b.js
import {foo} from './a.js';
console.log('b.js');
console.log(foo);
export let bar = 'bar';

結果:

$ babel-node a.js
b.js
undefined
a.js
bar

循環加載稍有不慎,就會 underfined。

字符串模版

ES6 在字符串上面但是下了很多功夫,先是解決了字符 unicode 的 bug,增長了一些處理多字節字符串 codePointAt 函數,還多了字符串的遍歷接口 for...of,這個遍歷藉口有點仿造 python 的感受。只要有迭代器功能的對象,均可以用 for...of 來遍歷。

ES6 添加了一些有意思的函數,好比 repeat(),前幾天比較火的文章‘五道經典的前端面試題’,就有提到一個在字符串上實現原生的重複方法,這裏的 repeat 能夠直接解決。

關於字符串上的新內容,很是有幫助的仍是模版字符串。以前在 js 中跨行的字符串實現起來很彆扭,而 python 能夠用三個反引號來實現。

ES6 中的模版字符串使用須要注意如下內容:

// ` 能夠跨行
var html = `
  <ul>
    <li>first</li>
    <li>second</li>
  </ul>`

//${} 調用變量和函數
var name = 'window';
var str = `my name is ${name};`;
// my name is window;

var add = (a, b)=> a+b;
var str = `2 + 3 = ${add(2,3)}`;
// "2 + 3 = 5"

用過 ejs 、swig 或 hbs 等模版,它們能夠嵌入 js 代碼,ES6 的模版字符串也能夠。使用 <%...%> 放置 JavaScript 代碼,使用 <%= ... %> 輸出 JavaScript 表達式。

var template = `
  <ul>
    <% data.forEach(function(item){ %>
      <li><%= item %></li>
    <% }) %>
  </ul>
`

下面就能夠寫正則表達式替換掉自定義字符並執行函數:

function compile(str){
  var evalExpr = /<%=(.+?)%>/g;
  var expr = /<%([\s\S]+?)%>/g;
  str = str.replace(evalExpr, '`); \n  join( $1 ); \n  join(`')
    .replace(expr, '`); \n $1 \n  join(`');
  str = 'join(`' + str + '`);';
  var script = `
    (function parse(data){
      var output = "";

      function join(html){
        output += html;
      }

      ${ str }

      return output;
    })
  `
  return script;
}
var strParse = eval(compile(template));
// 使用
var html = strParse(['shanghai', 'beijing', 'nanjing']);
//  <ul>    
//    <li>shanghai</li>
//    <li>beijing</li>
//    <li>nanjing</li>
//  </ul>

經過兩次使用字符串模版,並使用 eval 函數,一個 ES6 簡易模版就這樣完成了。

一些其餘核心功能

let const

ES5 經過 var 來申明變量,ES6 新添 let 和 const,且做用域是 塊級做用域

let 使用和 var 很是相似,let 不存在變量提高,也不容許重複申明,let 的聲明只能在它所在的代碼塊有效,好比 for 循環,很是適合使用 let:

for(let i = 0; i < data.length; i++){
  console.log(data[i]);
}
console.log(i); // error

若是用 var 來申明 i,最後不會報錯。以前學閉包的時候,有一個利用閉包解決循環的問題,用 let 能夠解決:

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

const 就是申明常量用的,一旦申明即被鎖定,後面沒法更改。

const PI = 3.14;
PI = 3; //error

let 和 const 都是塊級做用域,塊級做用域能夠任意嵌套,且 {} 內定義的變量,外層做用域是沒法得到的,且內外層的做用域能夠同名。

function fn() {
  let n = 1;
  if (true) {
    let n = 2;
  }
  console.log(n); // 1
}

解構賦值

解構賦值真的很好用,可是我每次都忘記使用。ES6 解構賦值基本語法 var [a, b, c] = [1, 2, 3];,從數組中取值,並按照前後次序來賦值。若是解構賦值不成功,就會返回 underfined,解構賦值也容許指定默認值:

var [a, b] = [1];
b // undefined

// 指定默認值
var [a, b = 2] = [1];
b // 2

除了數組,對象也能夠解構賦值,可是數組是有順序的,而對象沒有順序,若是想要成功賦值,必須與對象屬性同名,才能成功賦值,不然返回 underfined:

var {a, b} = {a: 1, b: 2};
a // 1
b // 2

var {a, c} = {a: 1, b: 2};
c // undefined

字符串的解構賦值比較有意思,既能夠把字符串看成能夠迭代的數組,又能夠看成對象,好比:

var [a1,a2,a3,a4,a5] = 'hello';
a2 // e

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

函數參數的解構賦值,看一個 forEach 的例子:

var data = [[1, 2], [3, 4]];
data.forEach(([a, b]) => console.log(a+b));
// 3
// 7

Promise 解決回掉

一直以來,回掉問題都是一件使人頭疼的事,調試的時候感受代碼跳來跳去,玩着玩着就暈了。ES6 提供 Promise 對象(函數),專門用來處理回掉。

var promise = new Promise(function(resolve, reject) {
  // ... some code
  if (/* 異步操做成功 */){
    resolve(value);
  } else {
    reject(error);
  }
});

promise.then(function(value) {
  // success
}, function(error) {
  // failure
});

resolve 和 reject 是兩個異步操做調用函數,當異步操做完成時,調用 resolve,error 則調用 reject,這兩個函數的功能就是把參數傳遞給回掉函數。then 函數用來處理成功或失敗狀態。

function loadImageAsync(url) {
  var p = new Promise(function(resolve, reject) {
    var image = new Image();

    image.onload = function() {
      resolve(image);
    };

    image.onerror = function() {
      reject(url);
    };

    image.src = url;
  });
  p.then(function(image){
    document.body.appendChild(image);
  }, function(url){
    throw new Error('Could not load '+ url);
  })
}

loadImageAsync('http://yuren.space/images/bg.gif');

上面是一個用 Promise 實現的異步加載圖片的函數。

for of 與 ...

Python 中有 for in 運算符,ES6 就搞了個 for...of。當使用 for...of 循環遍歷某種數據結構時,該循環會自動去尋找 Iterator 接口。一種數據結構只要部署了 Iterator 接口,咱們就稱這種數據結構是可遍歷的,對象、數組、字符串都是可遍歷的。

var str = 'hello';
for(let i of str){
  console.log(i);
}
// 'h' 'e' 'l' 'l' 'o'

...也很是好用,能夠直接把可遍歷對象直接轉換成數組:

var str = 'hello';
[...str] //["h", "e", "l", "l", "o"]

let arr = ['b', 'c'];
['a', ...arr, 'd'] 
// ['a', 'b', 'c', 'd']

有了 ... 以後,方便對非數組可遍歷的對象進行轉換,好比 arguments 和 querySelectAll 的結果:

[...arguments] // Array

var selects = document.querySelectAll('a');
[...selects] // Array

set 集合和 Map 結構

ES6 新增 Set 集合對象,其實像其餘語言早都支持了,不過,吃瓜羣衆,不覺明厲,之後,再遇到數組去重算法題,就能夠:

[...(new Set([1, 2, 2, 3]))];
//[1, 2, 3]

Set 方法分爲操做和遍歷,操做方法有 add-添加成員, delete-刪除成員, has-擁有判斷返回布爾值, clear-清空集合。

遍歷操做有 keys(),values(),entries(),forEach(),...,for of,map 和 filter 函數也能夠用於 Set,不過要進行巧妙操做,先轉換成數組,在進行操做:

let set = new Set([1,2,3]);
set = new Set([...set].map(a => a*2));
// Set {2, 4, 6}

Map 用來解決對象只接受字符串做爲鍵名,Map 相似於對象,也是鍵值對集合,可是「鍵」的範圍不限於字符串,各類類型的值(包括對象)均可以看成鍵。

Map 能夠經過 [set、 get、 has、 delete] 方法來操做:

var m = new Map();
var arr = [1, 2];
m.set(arr, 'array');
m.get(arr); // 'array'

m.has(arr) // true
m.delete(arr) // true
m.has(arr) // false

參數默認

參數默認這個功能使用起來仍是比較方便的,之前參數都是經過 || 來實現默認,如今可使用默認參數。不過這個功能在 Python 等語言中已是支持的。

// 之前寫代碼
var sayHello = function(name){
  var name = name || 'world';
  console.log('hello ' + name);
}

//參數默認
var sayHello = function(name = 'world'){
  console.log('hello ' + name);
}

sayHello() // 'hello world'
sayHello('ES6') // 'hello ES6'

對於不定參數,之前都是對 arguments 對象處理,且 arguments 對象仍是個僞數組,如今方便了:

var add = function(...arr){
  console.log(arr.constructor.name) // Array
  return arr.reduce((a, b) => a+b, 0);
}
add(1,2,3) // 6

總結

總之,對於 ES6 的學習仍是要活用,當我看了一遍 ECMAScript 6入門時候,感受知識點仍是不少,有點亂。當接觸了 react 以後,發現不少語法都很是的熟悉,因而就從頭溫習了 ES6,並整理了這篇文章。可能,你還不知道,這篇文章,大部分都是參考阮一峯老師的。共勉!

參考

ECMAScript 6入門
30分鐘掌握ES6/ES2015核心內容(上)
30分鐘掌握ES6/ES2015核心內容(下)
ES6 學習筆記
ES6新特性概覽
ES6js.com 官網

歡迎來我博客交流。

相關文章
相關標籤/搜索