1. 程式人生 > >ES6專案實戰-解析彩票專案-ES6基礎語法(3)

ES6專案實戰-解析彩票專案-ES6基礎語法(3)

1.類與物件

基本語法、類的繼承、靜態方法、靜態屬性、getter、setter

class Parent{
	//建構函式
	constructor(name='mukewang'){
		this.name = name;
	}
}

//生成例項
let v_parent = new Parent('v');
console.log(v_parent);   // Parent{name:"v"}

//繼承
class Parent{
	//建構函式
	constructor(name='mukewang'){
		this.name = name;
	}
}
class Child extends Parent{
	//繼承傳遞引數
	constructor(name='child'){
		super(name);
		this.type='child';
	}
}
new Child('hello');   //Child {name:"hello",type:"child"}

// getter、setter
class Parent{
	//建構函式
	constructor(name='mukewang'){
		this.name = name;
	}
	get longName(){
		return 'mk'+this.name;
	}
	set longName(value){
		this.name = value;
	}
}
let v= new Parent();
console.log('getter',v.longName); //v.longName就是get操作
v.longName = 'hello';   //賦值就是set操作,set和時是屬性
console.log('setter',v.longName);

//靜態方法(通過類去呼叫,不是通過例項去呼叫)
class Parent{
	//建構函式
	constructor(name='mukewang'){
		this.name = name;
	}

	static tell(){
		console.log(''tell'');
	}
}
Parent.tell();

//靜態屬性
class Parent{
	//建構函式
	constructor(name='mukewang'){
		this.name = name;
	}

	static tell(){
		console.log(''tell'');
	}
}
//靜態屬性(直接定義)
Parent.type = 'test';
Parent.type;   //呼叫

JavaScript 語言中,生成例項物件的傳統方法是通過建構函式。下面是一個例子。

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);

上面這種寫法跟傳統的面嚮物件語言(比如 C++ 和 Java)差異很大,很容易讓新學習這門語言的程式設計師感到困惑。

ES6 提供了更接近傳統語言的寫法,引入了(類)這個概念,作為物件的模板。通過class關鍵字,可以定義類。

基本上,ES6 的class可以看作只是一個語法糖,它的絕大部分功能,ES5 都可以做到,新的class寫法只是讓物件原型的寫法更加清晰、更像面向物件程式設計的語法而已。上面的程式碼用 ES6 的class改寫,就是下面這樣。

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

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

上面程式碼定義了一個“類”,可以看到裡面有一個constructor方法,這就是構造方法,而this關鍵字則代表例項物件。也就是說,ES5 的建構函式Point,對應 ES6 的Point類的構造方法。

Point類除了構造方法,還定義了一個toString方法。注意,定義“類”的方法的時候,前面不需要加上function這個關鍵字,直接把函式定義放進去了就可以了。另外,方法之間不需要逗號分隔,加了會報錯。

ES6 的類,完全可以看作建構函式的另一種寫法。

class Point {
  // ...
}

typeof Point // "function"
Point === Point.prototype.constructor // true

上面程式碼表明,類的資料型別就是函式,類本身就指向建構函式。

使用的時候,也是直接對類使用new命令,跟建構函式的用法完全一致。

class Bar {
  doStuff() {
    console.log('stuff');
  }
}

var b = new Bar();
b.doStuff() // "stuff"

建構函式的prototype屬性,在 ES6 的“類”上面繼續存在。事實上,類的所有方法都定義在類的prototype屬性上面。

class Point {
  constructor() {
    // ...
  }

  toString() {
    // ...
  }

  toValue() {
    // ...
  }
}

// 等同於

Point.prototype = {
  constructor() {},
  toString() {},
  toValue() {},
};

在類的例項上面呼叫方法,其實就是呼叫原型上的方法。

class B {}
let b = new B();

b.constructor === B.prototype.constructor // true

上面程式碼中,b是B類的例項,它的constructor方法就是B類原型的constructor方法。

由於類的方法都定義在prototype物件上面,所以類的新方法可以新增在prototype物件上面。Object.assign方法可以很方便地一次向類新增多個方法。

class Point {
  constructor(){
    // ...
  }
}

Object.assign(Point.prototype, {
  toString(){},
  toValue(){}
});

prototype物件的constructor屬性,直接指向“類”的本身,這與 ES5 的行為是一致的。

Point.prototype.constructor === Point // true

2.Promise

Promise 是非同步程式設計的一種解決方案,比傳統的解決方案——回撥函式和事件——更合理和更強大。它由社群最早提出和實現,ES6 將其寫進了語言標準,統一了用法,原生提供了Promise物件。

所謂Promise,簡單說就是一個容器,裡面儲存著某個未來才會結束的事件(通常是一個非同步操作)的結果。從語法上說,Promise 是一個物件,從它可以獲取非同步操作的訊息。Promise 提供統一的 API,各種非同步操作都可以用同樣的方法進行處理。

在這裡插入圖片描述

在這裡插入圖片描述

3.Iterator

JavaScript 原有的表示“集合”的資料結構,主要是陣列(Array)和物件(Object),ES6 又添加了Map和Set。這樣就有了四種資料集合,使用者還可以組合使用它們,定義自己的資料結構,比如陣列的成員是Map,Map的成員是物件。這樣就需要一種統一的介面機制,來處理所有不同的資料結構。

遍歷器(Iterator)就是這樣一種機制。它是一種介面,為各種不同的資料結構提供統一的訪問機制。任何資料結構只要部署 Iterator 介面,就可以完成遍歷操作(即依次處理該資料結構的所有成員)。

Iterator 的作用有三個:一是為各種資料結構,提供一個統一的、簡便的訪問介面;二是使得資料結構的成員能夠按某種次序排列;三是 ES6 創造了一種新的遍歷命令for…of迴圈,Iterator 介面主要供for…of消費。

Iterator 的遍歷過程是這樣的。
(1)建立一個指標物件,指向當前資料結構的起始位置。也就是說,遍歷器物件本質上,就是一個指標物件。
(2)第一次呼叫指標物件的next方法,可以將指標指向資料結構的第一個成員。
(3)第二次呼叫指標物件的next方法,指標就指向資料結構的第二個成員。
(4)不斷呼叫指標物件的next方法,直到它指向資料結構的結束位置。

每一次呼叫next方法,都會返回資料結構的當前成員的資訊。具體來說,就是返回一個包含value和done兩個屬性的物件。其中,value屬性是當前成員的值,done屬性是一個布林值,表示遍歷是否結束。

let arr = ['hello','world'];
let map = arr[Symbol.iterator]();
map.next();
map.next();
map.next():

在這裡插入圖片描述

let obj = {
	strat:[1,3,2],
	end:[7,9,8],
	[Symbol.iterator](){
		let self = this;
		let index =0;
		let arr = self.start.concat(self.end);
		let len = arr.length;
		return {
			next(){
				if(index<len){
					return {
						value:arr[index++],
						done:false
					}
				}else{
					return{
						value:arr[index++],
						done:true
					}
				}
			}
		}
	}
}
for(let key of obj){
	console.log(key);
}

4.Generator

Generator 函式是 ES6 提供的一種非同步程式設計解決方案,語法行為與傳統函式完全不同。

Generator就是一個Iterator 遍歷器生成函式。

Generator 函式有多種理解角度。語法上,首先可以把它理解成,Generator 函式是一個狀態機,封裝了多個內部狀態。

執行 Generator 函式會返回一個遍歷器物件,也就是說,Generator 函式除了狀態機,還是一個遍歷器物件生成函式。返回的遍歷器物件,可以依次遍歷 Generator 函式內部的每一個狀態。

形式上,Generator 函式是一個普通函式,但是有兩個特徵。一是,function關鍵字與函式名之間有一個星號;二是,函式體內部使用yield表示式,定義不同的內部狀態(yield在英語裡的意思就是“產出”)。

function* helloWorldGenerator() {
  yield 'hello';
  yield 'world';
  return 'ending';
}

var hw = helloWorldGenerator();

上面程式碼定義了一個 Generator 函式helloWorldGenerator,它內部有兩個yield表示式(hello和world),即該函式有三個狀態:hello,world 和 return 語句(結束執行)。

然後,Generator 函式的呼叫方法與普通函式一樣,也是在函式名後面加上一對圓括號。不同的是,呼叫 Generator 函式後,該函式並不執行,返回的也不是函式執行結果,而是一個指向內部狀態的指標物件,也就是上一章介紹的遍歷器物件(Iterator Object)。

下一步,必須呼叫遍歷器物件的next方法,使得指標移向下一個狀態。也就是說,每次呼叫next方法,內部指標就從函式頭部或上一次停下來的地方開始執行,直到遇到下一個yield表示式(或return語句)為止。換言之,Generator 函式是分段執行的,yield表示式是暫停執行的標記,而next方法可以恢復執行。

hw.next()
// { value: 'hello', done: false }

hw.next()
// { value: 'world', done: false }

hw.next()
// { value: 'ending', done: true }

hw.next()
// { value: undefined, done: true }

上面程式碼一共呼叫了四次next方法。

第一次呼叫,Generator 函式開始執行,直到遇到第一個yield表示式為止。next方法返回一個物件,它的value屬性就是當前yield表示式的值hello,done屬性的值false,表示遍歷還沒有結束。

第二次呼叫,Generator 函式從上次yield表示式停下的地方,一直執行到下一個yield表示式。next方法返回的物件的value屬性就是當前yield表示式的值world,done屬性的值false,表示遍歷還沒有結束。

第三次呼叫,Generator 函式從上次yield表示式停下的地方,一直執行到return語句(如果沒有return語句,就執行到函式結束)。next方法返回的物件的value屬性,就是緊跟在return語句後面的表示式的值(如果沒有return語句,則value屬性的值為undefined),done屬性的值true,表示遍歷已經結束。

第四次呼叫,此時 Generator 函式已經執行完畢,next方法返回物件的value屬性為undefined,done屬性為true。以後再呼叫next方法,返回的都是這個值。

總結一下,呼叫 Generator 函式,返回一個遍歷器物件,代表 Generator 函式的內部指標。以後,每次呼叫遍歷器物件的next方法,就會返回一個有著value和done兩個屬性的物件。value屬性表示當前的內部狀態的值,是yield表示式後面那個表示式的值;done屬性是一個布林值,表示是否遍歷結束。

5.Decorators

修飾器是一個函式。

修改行為。

修改類的行為。

許多面向物件的語言都有修飾器(Decorator)函式,用來修改類的行為。目前,有一個提案將這項功能,引入了 ECMAScript。

@testable
class MyTestableClass {
  // ...
}

function testable(target) {
  target.isTestable = true;
}

MyTestableClass.isTestable // true

上面程式碼中,@testable就是一個修飾器。它修改了MyTestableClass這個類的行為,為它加上了靜態屬性isTestable。testable函式的引數target是MyTestableClass類本身。

基本上,修飾器的行為就是下面這樣。

@decorator
class A {}

// 等同於

class A {}
A = decorator(A) || A;

也就是說,修飾器是一個對類進行處理的函式。修飾器函式的第一個引數,就是所要修飾的目標類。

function testable(target) {
  // ...
}

上面程式碼中,testable函式的引數target,就是會被修飾的類。

如果覺得一個引數不夠用,可以在修飾器外面再封裝一層函式。

function testable(isTestable) {
  return function(target) {
    target.isTestable = isTestable;
  }
}

@testable(true)
class MyTestableClass {}
MyTestableClass.isTestable // true

@testable(false)
class MyClass {}
MyClass.isTestable // false

上面程式碼中,修飾器testable可以接受引數,這就等於可以修改修飾器的行為。

注意,修飾器對類的行為的改變,是程式碼編譯時發生的,而不是在執行時。這意味著,修飾器能在編譯階段執行程式碼。也就是說,修飾器本質就是編譯時執行的函式。

6.Module模組化

//匯出變數、函式和類
export let A = 123;

export function test(){

}

export class Hello{
	test(){

	}
}

//匯出變數、函式和類的另一種方法
let A = 123;
let test = function(){

}
class Hello{

}

export default {
	A,
	test,
	Hello
}

//引入匯出的變數、函式和類
import {A,test,Hello} from 'lesson17';   // from 路徑
console.log(A,test,Hello);

import * as lesson from 'lesson17';   // from 路徑
console.log(lesson.A)

歷史上,JavaScript 一直沒有模組(module)體系,無法將一個大程式拆分成互相依賴的小檔案,再用簡單的方法拼裝起來。其他語言都有這項功能,比如 Ruby 的require、Python 的import,甚至就連 CSS 都有@import,但是 JavaScript 任何這方面的支援都沒有,這對開發大型的、複雜的專案形成了巨大障礙。

在 ES6 之前,社群制定了一些模組載入方案,最主要的有 CommonJS 和 AMD 兩種。前者用於伺服器,後者用於瀏覽器。ES6 在語言標準的層面上,實現了模組功能,而且實現得相當簡單,完全可以取代 CommonJS 和 AMD 規範,成為瀏覽器和伺服器通用的模組解決方案。

ES6 模組的設計思想是儘量的靜態化,使得編譯時就能確定模組的依賴關係,以及輸入和輸出的變數。CommonJS 和 AMD 模組,都只能在執行時確定這些東西。比如,CommonJS 模組就是物件,輸入時必須查詢物件屬性。

// CommonJS模組
let { stat, exists, readFile } = require('fs');

// 等同於
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;

上面程式碼的實質是整體載入fs模組(即載入fs的所有方法),生成一個物件(_fs),然後再從這個物件上面讀取 3 個方法。這種載入稱為“執行時載入”,因為只有執行時才能得到這個物件,導致完全沒辦法在編譯時做“靜態優化”。

ES6 模組不是物件,而是通過export命令顯式指定輸出的程式碼,再通過import命令輸入。

// ES6模組
import { stat, exists, readFile } from 'fs';

上面程式碼的實質是從fs模組載入 3 個方法,其他方法不載入。這種載入稱為“編譯時載入”或者靜態載入,即 ES6 可以在編譯時就完成模組載入,效率要比 CommonJS 模組的載入方式高。當然,這也導致了沒法引用 ES6 模組本身,因為它不是物件。

由於 ES6 模組是編譯時載入,使得靜態分析成為可能。有了它,就能進一步拓寬 JavaScript 的語法,比如引入巨集(macro)和型別檢驗(type system)這些只能靠靜態分析實現的功能。

除了靜態載入帶來的各種好處,ES6 模組還有以下好處。

  • 不再需要UMD模組格式了,將來伺服器和瀏覽器都會支援 ES6 模組格式。目前,通過各種工具庫,其實已經做到了這一點。
  • 將來瀏覽器的新 API 就能用模組格式提供,不再必須做成全域性變數或者navigator物件的屬性。
  • 不再需要物件作為名稱空間(比如Math物件),未來這些功能可以通過模組提供。

async 函式

ES2017 標準引入了 async 函式,使得非同步操作變得更加方便。

async 函式是什麼?一句話,它就是 Generator 函式的語法糖。

前文有一個 Generator 函式,依次讀取兩個檔案。

const fs = require('fs');

const readFile = function (fileName) {
  return new Promise(function (resolve, reject) {
    fs.readFile(fileName, function(error, data) {
      if (error) return reject(error);
      resolve(data);
    });
  });
};

const gen = function* () {
  const f1 = yield readFile('/etc/fstab');
  const f2 = yield readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};

上面程式碼的函式gen可以寫成async函式,就是下面這樣。

const asyncReadFile = async function () {
  const f1 = await readFile('/etc/fstab');
  const f2 = await readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};

一比較就會發現,async函式就是將 Generator 函式的星號(*)替換成async,將yield替換成await,僅此而已。

async函式對 Generator 函式的改進,體現在以下四點。

(1)內建執行器。

Generator 函式的執行必須靠執行器,所以才有了co模組,而async函式自帶執行器。也就是說,async函式的執行,與普通函式一模一樣,只要一行。

asyncReadFile();
上面的程式碼呼叫了asyncReadFile函式,然後它就會自動執行,輸出最後結果。這完全不像 Generator 函式,需要呼叫next方法,或者用co模組,才能真正執行,得到最後結果。

(2)更好的語義。

async和await,比起星號和yield,語義更清楚了。async表示函式裡有非同步操作,await表示緊跟在後面的表示式需要等待結果。

(3)更廣的適用性。

co模組約定,yield命令後面只能是 Thunk 函式或 Promise 物件,而async函式的await命令後面,可以是 Promise 物件和原始型別的值(數值、字串和布林值,但這時等同於同步操作)。

(4)返回值是 Promise。

async函式的返回值是 Promise 物件,這比 Generator 函式的返回值是 Iterator 物件方便多了。你可以用then方法指定下一步的操作。

進一步說,async函式完全可以看作多個非同步操作,包裝成的一個 Promise 物件,而await命令就是內部then命令的語法糖。