類
在ES5中,由於沒有類的概念,所以如果要使用物件導向程式設計,就需要利用原型繼承的方式。通常是建立一個構造器,然後將方法指派到該構造器的原型上。
就像這樣:
function Cat(name) {
this.name = name;
}
Cat.prototype.speak = function() {
console.log(`Mew!`);
}複製程式碼
ES6引入了class
關鍵字後就不再需要這樣做了。不過需要明白的是ES6中的類僅僅是以上面這種方式作為基礎的一個語法糖而已。
ES6中類宣告已class
關鍵字開始,其後是類的名稱;剩餘部分的語法部分看起來就像物件字面量中的方法簡寫,並且在方法之間不需要使用逗號。同時允許你在其中使用特殊的 constructor 方法名稱直接定義一個構造器,而不需要先定義一個函式再把它當作構造器使用。
class Cat {
constructor(name) {
this.name = name;
}
speak() {
console.log(`Mew!`);
}
}複製程式碼
類宣告與ES5仿類的區別
雖然ES6的類宣告是ES5方式的一個語法糖,但是與之相比,還是存在一些區別的。
- 類宣告不會被提升,這與函式定義不同。類宣告的行為與 let 相似,因此在程式的執行到達宣告處之前,類會存在於暫時性死區內。
- 類宣告中的所有程式碼會自動執行在嚴格模式下,並且也無法退出嚴格模式。
- 類的所有方法都是不可列舉的,這是對於自定義型別的顯著變化,後者必須用 Object.defineProperty() 才能將方法改變為不可列舉。
- 類的所有方法內部都沒有 [[Construct]] ,因此使用 new 來呼叫它們會丟擲錯誤。
- 呼叫類構造器時不使用 new ,會丟擲錯誤。
- 試圖在類的方法內部重寫類名,會丟擲錯誤。
訪問器屬性
自有屬性需要在類構造器中建立,而類還允許你在原型上定義訪問器屬性。
class Person {
constructor(name, age) {
this.age = age;
this.name = name;
}
get firstName() {
return this.name.split(` `)[0];
}
set firstName(value) {
let lastName = this.name.split(` `)[1];
this.name = value + ` ` + lastName;
}
}
let person = new Person(`Michael Jackson`, 35);
console.log(person.firstName); //`Michael`
person.firstName = `Marry`;
console.log(person.name); // `Marry Jackson`複製程式碼
在讀取訪問器屬性的時候,會呼叫getter方法,而寫入值的時候,會呼叫setter方法。這類似於ES5中使用Object.definePropery
的方法。
靜態成員
靜態成員在ES5中一般是直接定義在構造器上的,如:
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.createAdult = function(name) {
return new Person(name, 18);
};複製程式碼
而在ES6中提供了static
關鍵字簡化了宣告靜態成員的方式:
class Person {
constructor(name, age) {
this.age = age;
this.name = name;
}
static createAdult(name) {
return new Person(name, 18);
}
}複製程式碼
繼承
ES5中實現繼承的方式有很多種,但是如果要實現嚴格的繼承,步驟較為繁瑣。為了簡化繼承的關係,ES6中使用類讓這項工作變得更簡單。如果你熟悉面嚮物件語言,如java等,那麼extends這個關鍵你一定不會陌生。同樣的,在ES6中使用extends
關鍵字來指定當前類所需要繼承的函式即可。生成的類的原型會被自動調整,而你還能呼叫 super() 方法來訪問基類的構造器。
class Person {
constructor(country) {
this.country = country;
}
}
class Chinese extends Person{
constructor() {
super(`China`);
}
speak() {
console.log(`I come from ` + this.country);
}
}複製程式碼
派生類中的方法總是會遮蔽基類中的同名方法,因此,如果你需要使用父類中定義的方法的話,可以使用super
關鍵字來進行訪問。如:
class Person {
constructor(country) {
this.country = country;
}
speak() {
console.log(`I come from ` + this.country);
}
}
class Chinese extends Person{
constructor() {
super(`China`);
}
speak() {
super.speak();
console.log(`I am a Chinese`);
}
}
const chinese = new Chinese();
chinese.speak();
//I come from China.
//I am a Chinese.複製程式碼
從表示式中派生類
另一個在ES6中比較高階的地方是,可以從表示式中派生出類來:
let SerializableMixin = {
serialize() {
return JSON.stringify(this);
}
};
let AreaMixin = {
getArea() {
return this.length * this.width;
}
};
//混入
function mixin(...mixins) {
var base = function() {};
Object.assign(base.prototype, ...mixins);
return base;
}
class Square extends mixin(AreaMixin, SerializableMixin) {
constructor(length) {
super();
this.length = length;
this.width = length;
}
}
var x = new Square(3);
console.log(x.getArea()); // 9
console.log(x.serialize()); // "{"length":3,"width":3}"複製程式碼
繼承內建物件
利用extends
繼承內建物件的時候,容易出現的一個問題是會返回內建物件例項的方式,在繼承後會返回子類的例項。如:
class SubArray extends Array {
}
const subArr = new SubArray(1,2,3);
const filteredArr = subArr.filter(value => value > 1);
console.assert(filteredArr instanceof SubArray); //true複製程式碼
如果需要想讓其返回例項型別是Array
可以利用Symbol.species
這個符號來處理:
class SubArray extends Array {
//這裡使用static,表明是靜態訪問器屬性
static get [Symbol.species]() {
return Array;
}
}複製程式碼
定義抽象類
利用之前介紹的new.target
可以實現一個抽象類,原理就是當使用者呼叫new
直接建立例項的時候,丟擲錯誤。:
class BaseClass {
constructor() {
if(new.target === BaseClass) {
throw new Error(`該類不能直接例項化`)
}
}
}複製程式碼
模組
隨著專案的規模越來越大,現在模組化已經成為開發過程中必備的流程。之前,我們可能借助RequireJS等工具進行模組化管理,而現在ES6已經提供了模組系統。
先來了解一下基本語法:
基本的匯出匯入
模組( Modules )本質上就是 包含JS 程式碼的檔案。在一個js檔案中,你可以使用export
關鍵字,將程式碼公開給其他模組。
// sayHello.js
export function sayHello() {
console.log(`hello`);
}
// funcs.js
export function fun1() { .... }
export function func2() { .... }
export const value1 = `value1`;複製程式碼
如上面的例子中所示,你可以在檔案中匯出所有的最外層函式
、類
以及var
、let
或const
宣告的變數。而這些匯出的變數或公開部分則可以被其他檔案利用import
語法進行匯入後引用。
//單個匯入
import {sayHello} from `./sayHello.js`;
//多個匯入
import {func1, func2} from `./funs.js`;
sayHello(); // hello複製程式碼
為了確保瀏覽器與Node.js之間保持良好的相容性,建議使用相對路徑的寫法。
如果需要將整個模組當做單一的物件進行匯入,可以使用*
萬用字元:
//使用as關鍵字為匯出物件設定別名,模組中所有匯出都將作為屬性存在
import * as funcs from `./funcs.js`;
funcs.func1();
funsc.func2();複製程式碼
重新命名匯出與匯入
如果不想用原來模組中的命名,可以通過as
關鍵字來指定別名。
//as前面為模組原先的名稱,後面是別名,使用別名後sayHello為undefined
import { sayHello as say } from `./sayHello.js`;
say();複製程式碼
預設值
你可以使用export
關鍵字來匯出預設模組:
// sayHello.js
export default function() {
console.log(`hello`);
}
// main.js
import sayHello from `./sayHello.js`;
sayHello();複製程式碼
可以注意到,這裡預設匯出的時候,不需要使用花括號,而直接為其命名即可。這種寫法也較為簡潔。當一個檔案中,同時存在預設匯出模組和非預設匯出模組的時候,匯出的時候,預設匯出模組需要寫在前面,例如:
import sayHello,{ func1 } from `./sayHello.js`; //此處略去匯出過程
//或者使用如下方式
import {default as sayHello, func1} from `./sayHello.js`;複製程式碼
無繫結匯出
當一個檔案中沒有使用export
語句進行匯出的時候,其實我們還是可以import
進行匯入的。通常是被用於建立polyfill與shim的時候。
//sayHello.js
const name = `scq000`;
function sayHello() {
console.log(`hello`);
}
// main.js
import `./sayHello.js`;
sayHello();
console.log(name);複製程式碼
載入模組
雖然說現在在專案中通常都使用webpack來處理模組程式碼,但也需要知道其他載入模組的方式。
你可以使用<script type="module">
的方式進行模組的載入,預設瀏覽器會採用defer
屬性,一旦頁面文件完全被解析後,模組就會按次序執行。如果需要非同步載入的話,可以加上async
關鍵字。
另外,如果是使用Web Worker或Server Worker之類的worker的話,可以通過下面這種方式載入模組:
let worker = new Worker(`module.js`, { type: `module` });複製程式碼
迭代器與生成器
迭代器和生成器通常是一起來使用的。迭代器的目的是為了更加方便地遍歷物件,而生成器用來生成可迭代的物件。使用迭代器的過程中,你可以結合for...of
語句以及...
擴充套件符來遍歷物件的值。
迭代器
在ES6中,迭代器是專門用來設計迭代的物件,帶有特殊的介面。所有的迭代器都帶有next
方法,用來返回一個結果。這裡我們來手工實現一個迭代器:
function createIterator() {
var i = 0;
return {
next() {
var done = false;
var value;
if (i < 3) {
value = i * 2;
i++;
} else {
done = true;
value = undefined;
}
return { value: value, done: done }
}
}
}
let iterator = new createIterator();
iterator.next(); // {value: 0, done: false}
iterator.next(); // {value: 0, done: false}
iterator.next(); // {value: 4, done: false}
iterator.next(); // {value: undefined, done: true}複製程式碼
集合物件(Set、Map、Array)提供了三種內建的迭代器:entries,keys,values,這三個方法都會返回一個迭代器,用來方便地獲取鍵值對等資訊。ES6中定義了可迭代物件(iterable object),如Set、Map、Array以及字串等都可以利用for...of
語法來進行遍歷操作。原理其實就是呼叫它們內建的預設迭代器。對於使用者自定義的物件,如果也要讓它們支援for...of
語法,則需要去定義Symbol.iterator
屬性。具體例子,可以檢視符號那一部分的內容。
生成器
生成器(generator)是能夠返回迭代器的函式。通常定義的時候,我們會利用function
關鍵字之後的(*)號表示,使用yield
語句輸出每一次的資料。
function *getNum() {
yield 1;
yield 2;
yield 3;
}
const nums = getNum();
for(let num of nums) {
console.log(num);
}
//1,2,3複製程式碼
Promise與非同步程式設計
這部分的內容我在前端的非同步解決方案之Promise和Await/Async中有詳細的闡述,如果感興趣的可以看一下。
代理與反射介面
為了讓開發者能夠建立內建物件,ES6通過代理( proxy )的方式暴露了物件上的內部工作。使用代理能夠攔截並改變 JS 引擎的底層操作,如日誌、物件虛擬化等。而反射( reflect )則是反映了對底層的預設行為操作。
接下來這個例子,將演示如何利用代理和反射的方式對物件的內建行為做修改:
//要修改的預設物件
let target = {
name: `scq000`,
age: 23
};
//代理物件
let proxy = new Proxy(target, {
has(trapTarget, key) {
if(key === `age`) {
return false;
}else {
//呼叫預設的行為
return Reflect.has(trapTarget, key);
}
}
});
console.log(`value` in proxy); //true
console.log(`age` in proxy); //false複製程式碼
可以看到,上面這個例子使用代理物件攔截了in
操作符的預設行為並作出了修改。has
這個方法稱作陷阱函式,它能夠響應對in
操作的訪問操作。trapTarget
則是這個函式的目標物件,has
方法接受一個額外的引數key
是對應著需要檢查的屬性。一旦檢查到屬性名為age
,則返回false
,這樣就能隱藏這個屬性。
以下是一些常用的代理陷阱以及反射所對應的預設行為:
代理陷阱 | 被重寫的行為 | 預設行為 |
---|---|---|
get/set | 讀取/寫入一個屬性值 | Reflect.get/Reflect.set |
has | in運算子 | Reflect.has |
deleteProperty | delete運算子 | Reflect.deleteProperty |
getPropertyOf/setPropertyOf | Object.getPropertyOf/setPropertyOf | Reflefct.getPropertyOf/setPropertyOf |
目前,反射和代理在瀏覽器上還不支援,主要還是用在NodeJS程式設計上。這一部分的功能在實際開發中並不是特別常用,因此,這裡不做過多介紹。如果感興趣的話,可以自行查詢相關文件。