ES6 部分總結

tiu5發表於2017-12-14

var、let、const 比較

var 會有宣告提升,並初始化為 undefined

console.log(a)  //  undefined
var a = 1
console.log(a)  //  1

console.log(b)  //  Uncaught ReferenceError: b is not defined
b = 3
console.log(b)  //  3
console.log(window.b)  //  3
複製程式碼

疑問:

  • 為什麼列印 b 會報錯?
  • 為什麼 window.bb 結果是一樣的?

因為 var 宣告有會一個提升。因為變數 b 沒有帶宣告的關鍵字,在非嚴格模式下 b 就會被當作是全域性變數,全域性變數又會自動加到全域性物件屬性裡,瀏覽器裡的全域性物件就是 window,所以才可以用 window.b 呼叫。關於 var 宣告提升可以看下面程式碼:

var a = 'undefined'
console.log(a)  //  undefined
a = 1
console.log(a)  //  1
複製程式碼

let 有宣告提升,但是不會初始化為 undefined ,而是儲存在暫存區(TDZ)裡。

let a = 'global'
{
  console.log(a)  //  Uncaught ReferenceError: a is not defined
  let a = 1
}
複製程式碼

為什麼不是列印出 'global',而是報錯了?如果把 let a = 1 這個語句去掉,是可以正常列印出 'global' 的,所以程式碼應該是這樣的:

let a = 'global'
{
  let a  //  這時候,變數 a 進了暫存區,除非 `let a = 1` 執行完,它才會出暫存區,才能被呼叫。
  console.log(a)  //  Uncaught ReferenceError: a is not defined
  a = 1
}
複製程式碼

let 是不允許重複宣告的,是有塊級作用域的。

//  不允許重複宣告
let a = 1
let a = 2  //  Uncaught SyntaxError: Identifier 'a' has already been declared

//  沒有塊級作用域
var x = 1
{
  var x = 2
}
console.log(x)  //  2

//  有塊級作用域
let x = 1
{
  let x = 2
}
console.log(x)  //  1
複製程式碼

const 是常量宣告,宣告時要指定初始值,也有塊級作用域。

const c  //  Uncaught SyntaxError: Missing initializer in const declaration

const c = 1
{
  const c = 2
}
console.log(c)  //  輸出1,而且不會報錯

c = 2  //  Uncaught TypeError: Assignment to constant variable.
複製程式碼

最佳實踐

  • 常量用 const,能用 const 就不要用 let
  • 能用 let 就不要用 var

箭頭函式

一種語法糖: 語法糖(Syntactic sugar),也譯為糖衣語法,是由英國電腦科學家彼得·蘭丁發明的一個術語,指計算機語言中新增的某種語法,這種語法對語言的功能沒有影響,但是更方便程式設計師使用。語法糖讓程式更加簡潔,有更高的可讀性。(概念來自維基百科)

ES6 允許使用“箭頭”(=>)定義函式。

let f = v => v
//  等同於
let f = function(v) {
  return v
}

let f = () => 5
//  等同於
let f = function () { return 5 }

let sum = (num1, num2) => num1 + num2
//  等同於
let sum = function(num1, num2) {
  return num1 + num2
}

//  這個涉及到與變數解構的使用,後面講。
const full = ({ first, last }) => first + ' ' + last;

//  等同於
function full(person) {
  return person.first + ' ' + person.last;
}
複製程式碼

部分語法:

  • 引數列表右括號要和箭頭在同一行上。
  • 單行箭頭函式,函式體只能有一個語句。
  • 若箭頭函式返回一個物件字面量,要用括號括起來。

特性: this 指向是固定的,不可改的,將函式內部的 this 延伸到上一層作用域。

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

//  使用 Bable 轉為 ES5 的程式碼如下:
'use strict';

function foo() {
  var _this = this;

  setTimeout(function () {
    console.log('id:', _this.id);
  }, 100);
  //  也就是說箭頭函式的 `this` 指向的是 foo() 函式體裡的 `this`,而不是匿名函式的 `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);  //  s1: 3
setTimeout(() => console.log('s2: ', timer.s2), 3100);  //  s2: 0
setTimeout(() => console.log(s2), 3300);  //  NaN
複製程式碼

為什麼 console.log(s2) 沒有報錯,而是 NaN ??這是因為在第二個定時器裡頭,回撥函式是一個匿名函式嘛,所以它的 this 指向的是全域性物件(windows),所以 this.s2++; 就等同於 window.s2++; 也等同於全域性變數 s2++s2 初始值應該是 undefined,所以 s2++ 就變成 NaN

模板字串

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

以前如果寫 html 可能要用到很多字串拼接。

var str =
  'There are <b>' + basket.count + '</b> ' +
  'items in your basket, ' +
  '<em>' + basket.onSale +
  '</em> are on sale!'
複製程式碼

現在利用模板字串就可以大大簡化工作了。

var str =`
  There are <b> ${basket.count} </b> items in your basket,  
  <em> ${basket.onSale} </em> are on sale!
  `
複製程式碼

變數的解構賦值

實際場景:我請求介面,然後返回的是一個物件。

//  假設返回的就是這個 data
data = {
    name: 'tao',
    age: 20,
    sex: '男'
}

//  我要拿 data 裡頭的屬性,以前我可能這樣寫:
let str = `
    <span>姓名:${data.name}</span>
    <span>年齡:${data.age}</span>
    <span>性別:${data.sex}</span>
`
//  這時候,寫介面的很坑,假設他給我那個 age 是個空值,沒有初始值的。這時候我沒做檢測的話頁面就崩了。
//  利用物件解構可以這樣寫:
let {name, age, sex} = data
let str = `
    <span>姓名:${name}</span>
    <span>年齡:${age}</span>
    <span>性別:${sex}</span>
`
//  這時候是不是簡化了,還有就是沒有值,頁面也不會崩了,因為都給初始值為 undefined

//  還有一個場景就是,楚育把 name 用爛了,用了很多次,那如果我還用 name 的話,就衝突了。所以可以下面這樣

let { name: className } = { name: '15軟2', bar: "bbb" }
console.log(className) // '15軟2'

複製程式碼

... 操作符

展開運算子(用三個連續的點 ( ... ) 表示)是 ES6 中的新概念,使你能夠將 字面量物件 展開為多個元素。

const fruits = ["apples", "bananas", "pears"];
const vegetables = ["corn", "potatoes", "carrots"];

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

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

//  在之前的話,結合陣列可能要用到 concat 方法
複製程式碼

剩餘引數也用三個連續的點 ( ... ) 表示,使你能夠將不定數量的元素表示為陣列。

function sum(...nums) {
  let total = 0;  
  for(const num of nums) {
    total += num;
  }
  return total;
}

sum(10, 36, 7, 84, 90, 110);  //  337

//  其中 for...of 是一個新的迴圈形式,該迴圈將只迴圈訪問物件中的值。
const numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
for (const number of numbers) {
  console.log(number);
}
//  依次列印出 0 到 9
複製程式碼

預設函式引數

function say(message = 'hello') {
  console.log(message)
}

say()  //  hello
say('你好')  //  你好
//  這種用法我記得 PHP 也有,挺方便的。
複製程式碼

把預設引數和解構結合起來

function quadrature([sum1 = 2, sum2 = 4]){
  console.log(sum1 * sum2)
}
quadrature([])  //  8
quadrature([4])  //  16
quadrature([, 5])  //  10
quadrature([3, 4])  // 12

//  這種寫法有個不好的地方就是如果不傳引數就會報錯
quadrature()  //  Uncaught TypeError: Cannot read property 'Symbol(Symbol.iterator)' of undefined

//  改成這樣就不會了
function quadrature([sum1 = 2, sum2 = 2] = []){
  console.log(sum1 * sum2)
}
複製程式碼
function quadrature({sum1 = 2, sum2 = 2, arrs = ['hello', '你好']} = {}){
  
  console.log(sum1 * sum2)

  for (const arr of arrs){
    console.log(arr)
  }
  
}
quadrature()  //  4  hello  你好
複製程式碼

物件解構與陣列解構的區別

使用物件解構,它是根據鍵值對去配對,而陣列解構,它是基於位置的。個人建議用物件解構比較好。

Class 類

關鍵字 class constructor() static extends super

es6-class.png

通過對比,我們可以看出: Event 類裡頭的建構函式 constructor() 好像就是右邊的 function Event(){}

prototype.png

其實我們在 new Event() 的時候,它就是呼叫了 constructor() 然後它會返回一個物件,而這個物件它的 __proto__ 會指向 Event.prototpye

然後我們在呼叫 event.on(),因為它本身是沒有這個方法的,所以會去 event.__proto__ 指向的 Event.prototpye 去找,如果 Event.prototpye 也沒有,會繼續根據 Event.__proto__ 指向的 Function.prototype 裡頭找,會根據原型鏈一直到找下去,直到 __proto__ 指向 null 這時會報錯說不存在該方法。

繼承

//  stream.js

const Writable = require('stream').Writable; 
const util = require('util');

module.exports = class MyStream extends Writable {
    constructor(matchText, options) {
        super();
        this.count = 0;
        this.matcher = new RegExp(matchText, 'ig');
    }

    _write(chunk, encoding, cb) {
        let matches = chunk.toString().match(this.matcher);
        if (matches) {
            this.count += matches.length;
        }
        if (cb) {
            cb();
        }
    }

    end() {
        this.emit('total', this.count);
    }
}
複製程式碼

我們可以看到,只是簡單的用關鍵字 extends,然後裡面有一點要注意的是,你用子類的構造器時,要先呼叫 super(),不然會報錯。然後還有其他用法,我自己也沒怎麼試,我就不細寫,其實我是想早點回宿舍。。。

這只是一部分程式碼,整份程式碼可以看這個 node.js-demo

ES5 的繼承可以看我之前寫的 JavaScript類的繼承

ES6 Module

不打算細講,因為瀏覽器支援不是很好,如果我們團隊以後用 Webpack 等打包工具了,那沒問題,直接通過 Babel 打包成相容 ES5 的檔案,那就比較方便。但是現在我們還在用。。

在此之前市面上的模組實現的規範有這麼幾種,CommonJS、AMD、CMD、UMD。像 Node.js 就是用 CommonJS 那一套,RequireJS 就是 AMD,CMD 的話有 Sea.js 等等,你們可以去了解這些關於模組的規範。現在 ES6 也有了自己的模組。

好,接下來。講我們的主角

關於模組有兩個關鍵字:importexport

接觸過程式設計的人,一看就大概知道這兩個幹嘛用的吧。import 肯定就是匯入東西,就好像 Java 的匯入包也是用這個關鍵字嘛。那 export 應該就是匯出,輸出的意思吧。

//  sum.js
export default (a, b) => console.log(a + b)

//  寫成這樣也沒問題
function sum (a, b) {
  console.log(a + b)
}

export {sum as default}

//  為什麼會多一個 default ,不這樣的話,我匯入的時候變數名要跟匯出的變數對應得上。
複製程式碼
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Module Demo</title>
</head>
<body>
  <h1> ES6 </h1>
</body>
<script type="module">
  import fn from './sum.js'
  fn(5, 6)  //  11
</script>
</html>
複製程式碼

注意

  • 匯入的時候要寫檔案字尾名,不像 Node.js 的 require 方法一樣,js 檔案可以不加字尾。
  • 要以'/''./''../'開頭,反正現在還不支援直接用 sum.js
  • 會有 CORS 檢查,也就是說會有跨域問題,比如你直接右鍵檔案用瀏覽器開啟肯定是報錯的,你要放伺服器裡頭,用本地主機去訪問。
  • 記得指令碼標籤要加上 type="module"

可以參考這些文件

題外話

如果對於作用域,宣告前置,this 指向不怎麼清楚的,可以看工作室書架裡的書 《你不知道的JavaScript 上卷》。然後更多關於 ES6 的新特性可以看下面參考資料的連結。

參考資料