Iterator和for...of
是什么:
Iterator(迭代器)是專門用來控制如何遍歷的對象,具有特殊的接口。
Iterator接口是一種數據遍歷的協議,只要調用迭代器對象對象的next方法,就會得到一個對象,表示當前遍歷指針所在的那個位置的信息,這個包含done和value兩個屬性。
迭代器對象創建后,可以反復調用 next()使用。
怎么用:
Iterator對象帶有next方法,每一次調用next方法,都會返回數據結構的當前成員的信息。具體來說,就是返回一個包含value和done兩個屬性的對象。其中,value屬性是當前成員的值,done屬性是一個布爾值,表示遍歷是否結束。
ES6規定,默認的Iterator接口部署在數據結構的Symbol.iterator屬性,或者說,一個數據結構只要具有Symbol.iterator屬性,就可以認為是“可遍歷的”(iterable)。Symbol.iterator屬性本身是一個函數,就是當前數據結構默認的遍歷器生成函數。執行這個函數,就會返回一個遍歷器。
let obj = {
data: [ 'hello', 'world' ],
[Symbol.iterator]() {
const self = this
let index = 0
return {
next() {
if (index < self.data.length) {
return {
value: self.data[index++],
done: false
}
} else {
return { value: undefined, done: true }
}
}
}
}
}
for(let item of obj){
console.log(item)
}
// hello
// world
如上,for-of循環首先調用obj對象的Symbol.iterator方法,緊接着返回一個新的迭代器對象。迭代器對象可以是任意具有.next()方法的對象,for-of循環將重復調用這個方法,每次循環調用一次。return的對象中value表示當前的值,done表示是否完成迭代。
Iterator的作用有三個:
-
為各種數據結構,提供一個統一的、簡便的訪問接口;
-
使得數據結構的成員能夠按某種次序排列;
-
ES6創造了一種新的遍歷命令for...of循環,Iterator接口主要供for...of消費。
一個數據結構只要部署了Symbol.iterator屬性,就被視為具有iterator接口,就可以用for...of循環遍歷它的成員。也就是說,for...of循環內部調用的是數據結構的Symbol.iterator方法。
for...of循環可以使用的范圍包括數組、Set和Map結構、某些類似數組的對象(比如arguments對象、DOM NodeList對象)、后文的Generator對象,以及字符串。
Symbol
是什么
ES6引入了一種第六種基本類型的數據:Symbol。Symbol是一種特殊的、不可變的數據類型,可以作為對象屬性的標識符使用。
怎么用
調用Symbol()創建一個新的symbol,它的值與其它任何值皆不相等。
var sym = new Symbol() // TypeError,阻止創建一個顯式的Symbol包裝器對象而不是一個Symbol值
var s1 = Symbol('foo')
var s2 = Symbol('foo')
s1 === s2 // false
常用使用場景:
由於每一個Symbol值都是不相等的,因此常作為對象的屬性名來防止某一個鍵被不小心改寫或覆蓋,這個以symbol為鍵的屬性可以保證不與任何其它屬性產生沖突。
作為對象屬性名時的遍歷:參見對象的遍歷那節
內置的Symbol值:
除了定義自己使用的Symbol值以外,ES6還提供了11個內置的Symbol值,指向語言內部使用的方法。其中一個很重要的就是Iterator中提到的Symbol.iterator
Reflect(反射)
是什么
Reflect是一個內置的對象,它提供可攔截JavaScript操作的方法。
為什么要增加Reflect對象
1)更有用的返回值
比如,Object.defineProperty(obj, name, desc)在無法定義屬性時,會拋出一個錯誤,而Reflect.defineProperty(obj, name, desc)則會返回false。
// 老寫法
try {
Object.defineProperty(target, property, attributes);
// success
} catch (e) {
// failure
}
// 新寫法
if (Reflect.defineProperty(target, property, attributes)) {
// success
} else {
// failure
}
2)函數操作。某些Object操作是命令式,比如name in obj和delete obj[name],而Reflect.has(obj, name)和Reflect.deleteProperty(obj, name)讓它們變成了函數行為
3)更加可靠的函數調用方式
在ES5中,當我們想傳一個參數數組args來調用函數f,並且將this綁定為this,可以這樣寫:
f.apply(obj, args)
但是,f可能是一個故意或者不小心定義了它自己的apply方法的對象。當你想確保你調用的是內置的apply方法時,一種典型的方法是這樣寫的:
Function.prototype.apply.call(f, obj, args)
但是這種方法不僅冗長而且難以理解。通過使用Reflect,你可以以一種更簡單、容易的方式來可靠地進行函數調用
Reflect.apply(f, obj, args)
4)可變參數的構造函數
假設你想調用一個參數是可變的構造函數。在ES6中,由於新的擴展運算符,你可能可以這樣寫:
var obj = new F(...args)
在ES5中,這更加難寫,因為只有通過F.apply或者F.call傳遞可變參數來調用函數,但是沒有F.contruct來傳遞可變參數實例化一個構造函數。通過Reflect,在ES5中可以這樣寫(內容翻譯自參考鏈接,鏈接的項目是ES6 Reflect和Proxy的一個ES5 shim,所以會這么說):
var obj = Reflect.construct(F, args)
5)為Proxy(代理,見下一章)的traps提供默認行為
當使用Proxy對象去包裹存在的對象時,攔截一個操作是很常見的。執行一些行為,然后去“做默認的事情”,這是對包裹的對象進行攔截操作的典型形式。例如,我只是想在獲取對象obj的屬性時log出所有的屬性:
var loggedObj = new Proxy(obj, {
get: function(target, name) {
console.log("get", target, name);
// now do the default thing
}
});
Reflect和Proxy的API被設計為互相聯系、協同的,因此每個Proxy trap都有一個對應的Reflect去“做默認的事情”。因此當你發現你想在Proxy的handler中“做默認的事情”是,正確的事情永遠都是去調用Reflect對象對應的方法:
var loggedObj = new Proxy(obj, {
get: function(target, name) {
console.log("get", target, name);
return Reflect.get(target, name);
}
});
Reflect方法的返回類型已經被確保了能和Proxy traps的返回類型兼容。
6)控制訪問或者讀取時的this
var name = ... // get property name as a string
Reflect.get(obj, name, wrapper) // if obj[name] is an accessor, it gets run with `this === wrapper`
Reflect.set(obj, name, value, wrapper)
靜態方法
Reflect對象一共有14個靜態方法(其中Reflect.enumerate被廢棄)
與大多數全局對象不同,Reflect沒有構造函數。不能將其與一個new運算符一起使用,或者將Reflect對象作為一個函數來調用。
Reflect對象提供以下靜態函數,它們與代理處理程序方法(Proxy的handler)有相同的名稱。這些方法中的一些與Object上的對應方法基本相同,有些遍歷操作稍有不同,見對象擴展遍歷那節。
Reflect.apply()
對一個函數進行調用操作,同時可以傳入一個數組作為調用參數。和Function.prototype.apply()功能類似。
Reflect.construct()
對構造函數進行new操作,相當於執行new target(...args)。
Reflect.defineProperty()
和Object.defineProperty()類似。
Reflect.deleteProperty()
刪除對象的某個屬性,相當於執行delete target[name]。
Reflect.enumerate()
該方法會返回一個包含有目標對象身上所有可枚舉的自身字符串屬性以及繼承字符串屬性的迭代器,for...in 操作遍歷到的正是這些屬性。
Reflect.get()
獲取對象身上某個屬性的值,類似於target[name]。
Reflect.getOwnPropertyDescriptor()
類似於Object.getOwnPropertyDescriptor()。
Reflect.getPrototypeOf()
類似於Object.getPrototypeOf()。
Reflect.has()
判斷一個對象是否存在某個屬性,和in運算符的功能完全相同。
Reflect.isExtensible()
類似於Object.isExtensible().
Reflect.ownKeys()
返回一個包含所有自身屬性(不包含繼承屬性)的數組。
Reflect.preventExtensions()
類似於Object.preventExtensions()。
Reflect.set()
設置對象身上某個屬性的值,類似於target[name] = val。
Reflect.setPrototypeOf()
類似於Object.setPrototypeOf()。
Proxy(代理)
是什么
Proxy對象用於定義基本操作的自定義行為 (例如屬性查找,賦值,枚舉,函數調用等)。
一些術語:
- handler:包含traps的對象。
- traps:提供訪問屬性的方法,與操作系統中的traps定義相似。
- target:被代理虛擬化的對象,這個對象常常用作代理的存儲后端。
用法
ES6原生提供Proxy構造函數,用來生成Proxy實例。
var proxy = new Proxy(target, handler);
Proxy對象的所有用法,都是上面這種形式,不同的只是handler參數的寫法。其中,new Proxy()表示生成一個Proxy實例,target參數表示所要代理的目標對象,handler參數也是一個對象,用來定制代理行為。
下面代碼對一個空對象進行了代理,重定義了屬性的讀取(get)和設置(set)行為。
var obj = new Proxy({}, {
get: function (target, key, receiver) {
console.log(`getting ${key}!`);
return Reflect.get(target, key, receiver);
},
set: function (target, key, value, receiver) {
console.log(`setting ${key}!`);
return Reflect.set(target, key, value, receiver);
}
});
obj.count = 1
// setting count!
++obj.count
// getting count!
// setting count!
// 2
handler對象的方法
handler是一個包含了Proxy的traps的占位符對象。
所有的trap都是可選的,如果某個trap沒有定義,將會對target進行默認操作。這些trap和Reflect的靜態方法是對應的,可以使用Reflect對應的靜態方法提供默認行為。上面的例子中,handler定義了get和set兩個trap,每個trap都是一個方法,接收一些參數。返回了對應的Reflect方法來執行默認方法。
handler的每個方法可以理解為對相應的某個方法進行代理攔截。
handler.getPrototypeOf(target):Object.getPrototypeOf的一個trap
handler.setPrototypeOf(target, proto):Object.setPrototypeOf的一個trap
handler.isExtensible(target):Object.isExtensible的一個trap
handler.preventExtensions(target):Object.preventExtensions的一個trap
handler.getOwnPropertyDescriptor(target, propKey):Object.getOwnPropertyDescriptor的一個trap
handler.defineProperty(target, propKey, propDesc):Object.defineProperty的一個trap
handler.has(target, propKey):in操作的一個trap
handler.get(target, propKey, receiver):獲取屬性值的一個trap
handler.set(target, propKey, value, receiver):設置屬性值的一個trap
handler.deleteProperty(target, propKey):delete操作的一個trap
handler.ownKeys(target):Object.getOwnPropertyNames和Object.getOwnPropertySymbols的一個trap
handler.apply(target, object, args):函數調用的一個trap
handler.construct(target, args):new操作的一個trap
Proxy.revocable()
Proxy.revocable方法返回一個可取消的Proxy實例。
let target = {};
let handler = {};
let {proxy, revoke} = Proxy.revocable(target, handler);
proxy.foo = 123;
proxy.foo // 123
revoke();
proxy.foo // TypeError: Revoked
Proxy.revocable方法返回一個對象,該對象的proxy屬性是Proxy實例,revoke屬性是一個函數,可以取消Proxy實例。上面代碼中,當執行revoke函數之后,再訪問Proxy實例,就會拋出一個錯誤。
Proxy.revocable的一個使用場景是,目標對象不允許直接訪問,必須通過代理訪問,一旦訪問結束,就收回代理權,不允許再次訪問。
使用場景
上面說的那些可能都比較虛,去看一下w3cplus上翻譯的實例解析ES6 Proxy使用場景,可能就會更清楚地明白該怎么用。
如實例解析ES6 Proxy使用場景中所說,Proxy其功能非常類似於設計模式中的代理模式,該模式常用於三個方面:
- 攔截和監視外部對對象的訪問
- 降低函數或類的復雜度
- 在復雜操作前對操作進行校驗或對所需資源進行管理
有以下5個常見使用場景:
-
抽離校驗模塊
-
私有屬性
-
訪問日志
-
預警和攔截
-
過濾操作
類與繼承
類:
將原先JavaScript中傳統的通過構造函數生成新對象的方式變為類的方式,contructor內是構造函數執行的代碼,外面的方法為原型上的方法
// ES5
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)
//定義類
class Point {
constructor(x, y) {
this.x = x
this.y = y
}
// 靜態方法,static關鍵字,就表示該方法不會被實例繼承(但是會被子類繼承),而是直接通過類來調用
static classMethod() {
return 'hello'
}
toString() {
return '(' + this.x + ', ' + this.y + ')'
}
}
繼承:
通過extends關鍵字來實現。super關鍵字則是用來調用父類
ES5的繼承,實質是先創造子類的實例對象this,然后再將父類的方法添加到this上面(Parent.apply(this))。ES6的繼承機制完全不同,實質是先創造父類的實例對象this(所以必須先調用super方法),然后再用子類的構造函數修改this。理解了這句話,下面1,2兩點也就順其自然了:
1)子類必須在constructor方法中調用super方法,否則新建實例時會報錯。這是因為子類沒有自己的this對象,而是繼承父類的this對象,然后對其進行加工。如果不調用super方法,子類就得不到this對象。
2)在子類的構造函數中,只有調用super之后,才可以使用this關鍵字,否則會報錯。
class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y); // 調用父類的constructor(x, y)
this.color = color;
}
toString() {
return this.color + ' ' + super.toString() // 調用父類的toString()
}
}
Object.getPrototypeOf(ColorPoint) === Point // true
3)mixin: 繼承多個類
function mix(...mixins) {
class Mix {}
for (let mixin of mixins) {
copyProperties(Mix, mixin);
copyProperties(Mix.prototype, mixin.prototype);
}
return Mix
}
function copyProperties(target, source) {
for (let key of Reflect.ownKeys(source)) {
if ( key !== "constructor"
&& key !== "prototype"
&& key !== "name"
) {
let desc = Object.getOwnPropertyDescriptor(source, key);
Object.defineProperty(target, key, desc)
}
}
}
class DistributedEdit extends mix(Loggable, Serializable) {
// ...
}
4)new.target屬性:通過檢查new.target對象是否是undefined,可以判斷函數是否通過new進行調用。
function Person(name) {
if (new.target !== undefined) {
this.name = name
} else {
throw new Error('必須使用new生成實例')
}
}
// 另一種寫法
function Person(name) {
if (new.target === Person) {
this.name = name
} else {
throw new Error('必須使用new生成實例')
}
}
var person = new Person('張三') // 正確
var notAPerson = Person.call(person, '張三') // 報錯
Decorator(裝飾器)
是什么
Decorator是用來修改類(包括類和類的屬性)的一個函數。
這是ES的一個提案,其實是ES7的特性,目前Babel轉碼器已經支持。
怎么用
1)修飾類:在類之前使用@加函數名,裝飾器函數的第一個參數,就是所要修飾的目標類
function testable(target) {
target.prototype.isTestable = true;
}
@testable
class MyTestableClass {}
let obj = new MyTestableClass();
obj.isTestable // true
裝飾器函數也可以是一個工廠方法
function testable(isTestable) {
return function(target) {
target.isTestable = isTestable;
}
}
@testable(true)
class MyTestableClass {}
MyTestableClass.isTestable // true
@testable(false)
class MyClass {}
MyClass.isTestable // false
2)修飾類的屬性:修飾器函數一共可以接受三個參數,第一個參數是所要修飾的目標對象,第二個參數是所要修飾的屬性名,第三個參數是該屬性的描述對象。裝飾器在作用於屬性的時候,實際上是通過Object.defineProperty來進行擴展和封裝的。
下面是一個例子,修改屬性描述對象的enumerable屬性,使得該屬性不可遍歷。
class Person {
@nonenumerable
get kidCount() { return this.children.length; }
}
function nonenumerable(target, name, descriptor) {
descriptor.enumerable = false;
return descriptor;
}
實踐
core-decorators.js這個第三方模塊提供了幾個常見的修飾器。
在修飾器的基礎上,可以實現Mixin模式等。
Module(模塊)
在ES6之前,前端和nodejs實踐中已經有一些模塊加載方案,如CommonJS、AMD、CMD等。ES6在語言標准的層面上,實現了模塊功能。
模塊功能主要由兩個命令構成:export和import。export命令用於規定模塊的對外接口,import命令用於輸入其他模塊提供的功能。
export
一個模塊就是一個獨立的文件。該文件內部的所有變量,外部無法獲取。必須使用export關鍵字輸出該變量。有以下兩種不同的導出方式:
命名導出
命名導出規定的是對外的接口,必須與模塊內部的變量建立一一對應關系。
export { myFunction }; // 導出一個函數聲明
export const foo = Math.sqrt(2); // 導出一個常量
默認導出 (每個腳本只能有一個),使用export default命令:
export default myFunctionOrClass
本質上,export default就是輸出一個叫做default的變量或方法,然后系統允許你為它取任意名字
對於只導出一部分值來說,命名導出的方式很有用。在導入時候,可以使用相同的名稱來引用對應導出的值。
關於默認導出方式,每個模塊只有一個默認導出。一個默認導出可以是一個函數,一個類,一個對象等。當最簡單導入的時候,這個值是將被認為是”入口”導出值。
import
使用export命令定義了模塊的對外接口以后,其他JS文件就可以通過import命令加載這個模塊。
import { foo, bar } from 'my_module' // 指定加載某個輸出值
import 'lodash'; // 僅執行
import { lastName as surname } from './profile'; // 為輸入的模塊重命名
import * as circle from './circle'; // 整體加載
/*export和import復合寫法*/
export { foo, bar } from 'my_module';
// 等同於
import { foo, bar } from 'my_module';
export { foo, bar };
ES6模塊與CommonJS模塊的差異
它們有兩個重大差異。
- CommonJS模塊輸出的是一個值的拷貝,ES6模塊輸出的是值的引用。
- CommonJS模塊是運行時加載,ES6模塊是編譯時輸出接口。
CommonJS是運行時加載,ES6是編譯時加載,使得靜態分析成為可能
注意事項
-
ES6的模塊自動采用嚴格模式。因此ES6模塊中,頂層的this指向undefined。
-
export一般放在兩頭即開始或者結尾這樣更能清晰地明白暴露了什么變量
-
注意,import命令具有提升效果,會提升到整個模塊的頭部,首先執行。因為不是運行時加載,不支持條件加載、按需加載等