在 Javascript 中,讀取、賦值、呼叫方法等等,幾乎一切操作都是圍繞“物件”展開的;長久以來,如何更好的瞭解和控制這些操作,就成了該語言發展中的重要問題。
I. JS物件的訪問控制
[1.1] 熟悉的 getter/setter
所謂 getter/setter,其定義一般為:
- 一個 getter 方法不接受任何引數,且總是返回一個值
- 一個 setter 總是接受一個引數,且並不會返回值
一些 getter/setter 的常識:
- 也被稱為存取方法,是訪問方法(access methods)中最常用的兩個
- 用來封裝私有成員方法,以隔離外界對其的直接訪問
- 也可以在存取過程中新增其他的邏輯,保證了外部呼叫的簡潔性
- 實現了物件或類內部邏輯的靈活性,保留了改變的可能
- 在很多 IDE 中可以自動生成
首先看看其他語言中一般的實現方式:
一種是傳統的顯式 getXXX()/setXXX(v)
方法呼叫
//JAVA
public class People {
private Integer _age;
public Integer getAge() {
return this._age;
}
public void setAge(Integer age) {
this._age = age;
}
public static void main(String[] args) {
People p = new People();
p.setAge(18);
System.out.println(p.getAge().toString()); //18
}
}複製程式碼
毫無疑問,顯式呼叫命名其實是隨意的,而且各種語言都能實現
另一種是隱式(implicit)的 getter/setter
//AS2
class Login2 {
private var _username:String;
public function get userName():String {
return this._username;
}
public function set userName(value:String):Void {
this._username = value;
}
}
var lg = new Login2;
lg.userName = "tom";
trace(lg.userName); //"tom"複製程式碼
//C#
class People
{
private string _name;
public string name
{
get {
return _name;
}
set {
_name = value;
}
}
}
People p = new People();
p.name = "tom";
Console.WriteLine(p.name)複製程式碼
//PHP
class MyClass {
private $firstField;
private $secondField;
public function __get($property) {
if (property_exists($this, $property)) {
return $this->$property;
}
}
public function __set($property, $value) {
if (property_exists($this, $property)) {
$this->$property = $value." world";
}
}
}
$mc = new MyClass;
$mc->firstField = "hello";
echo $mc->firstField; //"hello world"複製程式碼
隱式存取方法需要特定語言的支援,使用起來感覺就是讀取屬性(var x = obj.x
)或給屬性賦值(obj.x = "foo"
)
[1.2] ES5 中的 getter 和 setter
從 2011 年的 ECMAScript 5.1 (ECMA-262) 規範開始,JavaScript 也開始支援 getter/setter;形式上,自然是和同為 ECMAScript 實現的 AS2/AS3 相同
getter 的語法:
// prop 指的是要繫結到給定函式的屬性名
{get prop() { ... } }
// 還可以使用一個計算屬性名的 expression 繫結到給定的函式, 注意瀏覽器相容性
{get [expression]() { ... } }複製程式碼
? 例子:
var obj = {
log: ['example','test'],
get latest() {
if (this.log.length == 0) return undefined;
return this.log[this.log.length - 1];
}
}
console.log(obj.latest); // "test"
var expr = 'foo';
var obj2 = {
get [expr]() { return 'bar'; }
};
console.log(obj2.foo); // "bar"複製程式碼
使用 get 語法時應注意以下問題:
- 可以使用數值或字串作為標識
- 必須不帶引數
- 不能與另一個get或具有相同屬性的資料條目的物件字面量中出現
通過 delete 操作符刪除 getter:
delete obj.latest;複製程式碼
以下展示了一種進階的用法,即首次呼叫時才取值(lazy getter),並且將 getter 轉為普通資料屬性:
get notifier() {
delete this.notifier;
return this.notifier = document.getElementById('myId');
},複製程式碼
setter 的語法:
//prop 指的是要繫結到給定函式的屬性名
//val 指的是分配給prop的值
{set prop(val) { . . . }}
// 還可以使用一個計算屬性名的 expression 繫結到給定的函式, 注意瀏覽器相容性
{set [expression](val) { . . . }}複製程式碼
使用 set 語法時應注意以下問題:
- 識別符號可以是數字或字串
- 必須有一個明確的引數
- 在同一個物件中,不能為一個已有真實值的變數使用 set ,也不能為一個屬性設定多個 set
? 例子:
var language = {
set current(name) {
this.log.push(name);
},
log: []
}
language.current = 'EN';
console.log(language.log); // ['EN']
language.current = 'FA';
console.log(language.log); // ['EN', 'FA']
var expr = "foo";
var obj = {
baz: "bar",
set [expr](v) { this.baz = v; }
};
console.log(obj.baz); // "bar"
obj.foo = "baz"; // run the setter
console.log(obj.baz); // "baz"複製程式碼
setter 可以用delete操作來移除:
delete o.current;複製程式碼
[1.4] 用 Object.defineProperty() 精確定義物件成員
回顧前面提到過的,物件裡存在的屬性描述符有兩種主要形式:資料屬性和存取方法。描述符必須是兩種形式之一,不能同時是兩者。
並且在一般情況下,通過賦值來為物件新增的屬性,可以由 for...in 或 Object.keys 方法遍歷列舉出來;且通過這種方式新增的屬性值可以被改變,也可以被刪除。
var obj = {
_c: 99,
get c() {
return this._c;
}
};
obj.a = 'foo';
obj.b = function() {
alert("hello world!");
};
console.log( Object.keys(obj) ); //["_c", "c", "a", "b"]
for (var k in obj) console.log(k); //"_c", "c", "a", "b"
delete obj.b;
delete obj.c;
console.log(obj.b, obj.c); //undefined, undefined複製程式碼
對於這樣定義的資料屬性或存取方法,無法控制其是否可被 delete,也無法限制其是否能被列舉
而使用 Object.defineProperty() 則允許改變這些預設設定
同樣從 ECMAScript 5.1 規範開始,定義了 Object.defineProperty() 方法。用於直接在一個物件上定義一個新屬性,或者修改一個物件的現有屬性, 並返回這個物件
其語法為:
//obj 需要被操作的目標物件
//prop 目標物件需要定義或修改的屬性的名稱
//descriptor 將被定義或修改的屬性的描述符
Object.defineProperty(obj, prop, descriptor)複製程式碼
其中 descriptor
可以設定的屬性為:
屬性 | 描述 | 應用於 |
---|---|---|
configurable | 是否能被修改及刪除 | 資料屬性、存取方法 |
enumerable | 是否可被列舉 | 資料屬性、存取方法 |
value | 屬性值 | 資料屬性 |
writable | 是否能被賦值運算子改變 | 資料屬性 |
get | getter 方法 | 存取方法 |
set | setter 方法 | 存取方法 |
需要了解的是,從 IE8 開始有限支援這個方法(非 DOM 物件不可用)
? 例子:
var o = {};
o.a = 1;
// 等同於 :
Object.defineProperty(o, "a", {
value : 1,
writable : true,
configurable : true,
enumerable : true
});複製程式碼
var o = {};
var bValue;
Object.defineProperty(o, "b", {
get : function(){ //新增存取方法
return bValue;
},
set : function(newValue){
bValue = newValue;
},
enumerable : true,
configurable : true
});複製程式碼
var o = {};
Object.defineProperty(o, "a", {
value : 37,
writable : false //定義了一個“只讀”的屬性
});
console.log(o.a); // 37
o.a = 25; // 在嚴格模式下會丟擲錯誤,非嚴格模式只是不起作用
console.log(o.a); // 37複製程式碼
var o = {};
Object.defineProperty(o, "a", {
get : function(){return 1;},
configurable : false //不可編輯、不可刪除
});
// throws a TypeError
Object.defineProperty(o, "a", {configurable : true});
// throws a TypeError
Object.defineProperty(o, "a", {enumerable : true});
// throws a TypeError
Object.defineProperty(o, "a", {set : function(){}});
// throws a TypeError
Object.defineProperty(o, "a", {get : function(){return 1;}});
// throws a TypeError
Object.defineProperty(o, "a", {value : 12});
console.log(o.a); //1
delete o.a; // 在嚴格模式下會丟擲TypeError,非嚴格模式只是不起作用
console.log(o.a); //1複製程式碼
Object.defineProperty(o, "conflict", {
value: 0x9f91102,
get: function() {
return 0xdeadbeef;
}
}); //丟擲 TypeError,資料屬性和存取方法不能混合設定複製程式碼
相關方法:Object.getOwnPropertyDescriptor()
返回指定物件上一個自有屬性對應的屬性描述符。(自有屬性指的是直接賦予該物件的屬性,而非從原型鏈上進行查詢的屬性)
語法:
//其中 prop 對應於 Object.defineProperty() 中第三個引數 descriptor
Object.getOwnPropertyDescriptor(obj, prop)複製程式碼
? 例子:
var o = {
get foo() {
return 17;
}
};
Object.getOwnPropertyDescriptor(o, "foo");
// {
// configurable: true,
// enumerable: true,
// get: /*the getter function*/,
// set: undefined
// }複製程式碼
相關方法:Object.defineProperties()
直接在一個物件上定義多個新的屬性或修改現有屬性
語法:
//prop 和 descriptor 的定義對應於 Object.defineProperty()
Object.defineProperties(obj, {
prop1: descriptor1,
prop2: descriptor2,
...
})複製程式碼
? 例子:
var obj = {};
Object.defineProperties(obj, {
'property1': {
value: true,
writable: true
},
'property2': {
value: 'Hello',
writable: false
}
});複製程式碼
相關方法:Object.create()
使用指定的原型物件及其屬性去建立一個新的物件
語法:
//proto 為新建立物件的原型物件
//props 對應於 Object.defineProperties() 中的第二個引數
Object.create(proto[, props])複製程式碼
? 例子:
// 建立一個原型為null的空物件
var o = Object.create(null);
var o2 = {};
// 以字面量方式建立的空物件就相當於:
var o2 = Object.create(Object.prototype);複製程式碼
var foo = {a:1, b:2};
var o = Object.create(foo, {
// foo會成為所建立物件的資料屬性
foo: {
writable:true,
configurable:true,
value: "hello"
},
// bar會成為所建立物件的訪問器屬性
bar: {
configurable: false,
get: function() { return 10 },
set: function(value) {
console.log("Setting `o.bar` to", value);
}
}
});複製程式碼
[1.5] __define[G,S]etter__()
作為非標準和已廢棄的方法,defineGetter() 和 defineSetter() 有時會出現在一些歷史程式碼中,並仍能執行在 Firefox/Safari/Chrome 等瀏覽器中
? 直接看例子:
var o = {
word: null
};
o.__defineGetter__('gimmeFive', function() {
return 5;
});
console.log(o.gimmeFive); // 5
o.__defineSetter__('say', function(vlu) {
this.word = vlu;
});
o.say = "hello";
console.log(o.word); //"hello"複製程式碼
[1.6] __lookup[G,S]etter__()
同樣,還有 lookupGetter() 和 lookupSetter() 兩個非標準和已廢棄的方法
- lookupGetter() 會返回物件上某個屬性的 getter 函式
? 例子:
var obj = {
get foo() {
return Math.random() > 0.5 ? "foo" : "bar";
}
};
obj.__lookupGetter__("foo")
// (function (){return Math.random() > 0.5 ? "foo" : "bar"})複製程式碼
如果換成標準的方法,則是:
Object.getOwnPropertyDescriptor(obj, "foo").get
// (function (){return Math.random() > 0.5 ? "foo" : "bar"})複製程式碼
而如果那個訪問器屬性是繼承來的:
Object.getOwnPropertyDescriptor(Object.getPrototypeOf(obj), "foo").get
// function __proto__() {[native code]}複製程式碼
- lookupSetter() 會返回物件的某個屬性的 setter 函式
? 例子:
var obj = {
set foo(value) {
this.bar = value;
}
};
obj.__lookupSetter__('foo')
// (function(value) { this.bar = value; })
// 標準且推薦使用的方式。
Object.getOwnPropertyDescriptor(obj, 'foo').set;
// (function(value) { this.bar = value; })複製程式碼
[1.7] 用 onpropertychange 相容古早瀏覽器
在某些要求相容 IE6/IE7 等瀏覽器的極端情況下,利用 IE 支援的
onpropertychange
事件,也是可以模擬 getter/setter 的
要注意這種方法僅限於已載入到文件中的 DOM 物件
function addProperty(obj, name, onGet, onSet) {
var
oldValue = obj[name],
getter = function () {
return onGet.apply(obj, [oldValue]);
},
setter = function (newValue) {
return oldValue = onSet.apply(obj, [newValue]);
},
onPropertyChange = function (event) {
if (event.propertyName == name) {
// 暫時移除事件監聽以免迴圈呼叫
obj.detachEvent("onpropertychange", onPropertyChange);
// 把改變後的值傳遞給 setter
var newValue = setter(obj[name]);
// 設定 getter
obj[name] = getter;
obj[name].toString = getter;
// 恢復事件監聽
obj.attachEvent("onpropertychange", onPropertyChange);
}
};
// 設定 getter
obj[name] = getter;
obj[name].toString = getter;
obj.attachEvent("onpropertychange", onPropertyChange);
}複製程式碼
II. JS中的代理和反射
在物件本身上,一個個屬性的定義訪問控制,有時會帶來程式碼臃腫,甚至難以維護;瞭解代理和反射的概念和用法,可以有效改善這些狀況。
[2.1] 傳統的代理模式
在經典的設計模式(Design Pattern)中,代理模式(Proxy Pattern)被廣泛應用;其定義為:
在代理模式中,一個代理物件(Proxy)充當著另一個目標物件(Real Subject)的介面。代理物件居於目標物件的使用者(Client)和目標物件本身的中間,並負責保護對目標物件的訪問。
典型的應用場景為:
- 對目標物件的訪問控制和快取
- 延遲目標物件的初始化
- 訪問遠端物件
? 舉個例子:
function Book(id, name) {
this.id = id;
this.name = name;
}
function BookShop() {
this.books = {};
}
BookShop.prototype = {
addBook: function(book) {
this.books[book.id] = book;
},
findBook: function(id) {
return this.books[id];
}
}
function BookShopProxy() {
}
BookShopProxy.prototype = {
_init: function() {
if (this.bookshop)
return;
else
this.bookshop = new BookShop;
},
addBook: function(book) {
this._init();
if (book.id in this.bookshop.books) {
console.log('existed book!', book.id);
return;
} else {
this.bookshop.addBook(book);
}
},
findBook: function(id) {
this._init();
if (id in this.bookshop.books)
return this.bookshop.findBook(id);
else
return null;
}
}
var proxy = new BookShopProxy;
proxy.addBook({id:1, name:"head first design pattern"});
proxy.addBook({id:2, name:"thinking in java"});
proxy.addBook({id:3, name:"lua programming"});
proxy.addBook({id:2, name:"thinking in java"}); //existed book! 2
console.log(proxy.findBook(1)); //{ id: 1, name: 'head first design pattern' }
console.log(proxy.findBook(3)); //{ id: 3, name: 'lua programming' }複製程式碼
顯然,以上示例程式碼中展示了使用代理來實現延遲初始化和訪問控制。
值得一提的是,代理模式與設計模式中另一種裝飾者模式(Decorator Pattern)容易被混淆,兩者的相同之處在於都是對原始的目標物件的包裝;不同之處在於,前者著眼於提供與原始物件相同的API,並將對其的訪問控制保護起來,而後者則側重於在原有API的基礎上新增新的功能。
[2.2] ES6 中的 Proxy
在 ECMAScript 2015 (6th Edition, ECMA-262) 標準中,提出了原生的 Proxy 物件。用於定義基本操作的自定義行為(如屬性查詢,賦值,列舉,函式呼叫等)
語法:
let p = new Proxy(target, handler);複製程式碼
proxy 物件的目標物件 target
,可以是任何型別的物件,如 Object、Array、Function,甚至另一個 Proxy 物件;在進行let proxy=new Proxy(target,handle)
的操作後,proxy、target兩個物件會相互影響。即:
let target = {
_prop: 'foo',
prop: 'foo'
};
let proxy = new Proxy(target, handler);
proxy._prop = 'bar';
target._attr = 'new'
console.log(target._prop) //'bar'
console.log(proxy._attr) //'new'複製程式碼
而 handler
也是一個物件,其若干規定好的屬性是定義好一個個函式,表示了當執行目標物件的對應訪問時所執行的操作;最常見的操作是定義 getter/setter 的 get 和 set 屬性:
let handler = {
get (target, key){
return key in target
? target[key]
: -1; //預設值
},
set (target, key, value) {
if (key === 'age') { //校驗
target[key] = value > 0 && value < 100 ? value : 0
}
return true;
}
};
let target = {};
let proxy = new Proxy(target, handler);
proxy.age = 22 //22複製程式碼
可以注意到,和 ES5 中物件本身的 setter 不同的是, proxy 中的 setter 必須有返回值;
並且應該也很容易理解,不光是名字相同,Proxy 物件也的確符合經典的代理模式 -- 由代理物件對目標物件的 API 進行封裝和保護,隱藏目標物件,控制對其的訪問行為。
除了可以定義 getter/setter,較完整的 handler 屬性如下:
- "get": function (oTarget, sKey)
- "set": function (oTarget, sKey, vValue)
- "enumerate": function (oTarget, sKey)
- "ownKeys": function (oTarget, sKey)
- "has": function (oTarget, sKey)
- "defineProperty": function (oTarget, sKey, oDesc)
- "deleteProperty": function (oTarget, sKey)
- "getOwnPropertyDescriptor": function (oTarget, sKey)
- "getPrototypeOf(oTarget)"
- "setPrototypeOf(oTarget, oPrototype)"
- "apply(oTarget, thisArg, argumentsList)":
- "construct(oTarget, argumentsList, newTarget)"
- "isExtensible(oTarget)"
- "preventExtensions(oTarget)"
[2.3] 反射
物件的反射(reflection)是一種在執行時(runtime)探查和操作物件屬性的語言能力。
在 JAVA/AS3 等語言中,反射一般被用於在執行時獲取某個物件的類名、屬性列表,然後再動態構造等;比如通過 XML 配置檔案中的值動態建立物件,或者根據名稱提取 swf 檔案中的 MovieClip 等。
JS 本來也具有相關的反射API,比如 Object.getOwnPropertyDescriptor()
、Function.prototype.apply()
、in
、delete
等,但這些 API 分佈在不同的名稱空間甚至全域性保留字中,並且執行失敗時是以丟擲異常的方式進行的。這些因素使得涉及到物件反射的程式碼難以書寫和維護。
[2.4] ES6 中的 Reflect
和 Proxy 同時,在 ECMAScript 2015 (6th Edition, ECMA-262) 中,引入了 Reflect 物件,用來囊括物件反射的若干方法。
反射方法 | 相似操作 |
---|---|
Reflect.apply() | Function.prototype.apply() |
Reflect.construct() | 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() | Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target)) |
Reflect.preventExtensions() | Object.preventExtensions() |
Reflect.set() | target[name] = val |
Reflect.setPrototypeOf() | Object.setPrototypeOf() |
- Reflect 與 ES5 的 Object 有點類似,包含了物件語言內部的方法,Reflect 也有和 Proxy 互相一一對應的若干種方法。
- Proxy 相當於去修改設定物件的屬性行為,而Reflect則是獲取物件的這些行為(的原始版本)。兩者經常搭配使用。
- Reflect 沒有建構函式,可被呼叫的都是其靜態方法。
var target = {
a: 1
};
var proxy = new Proxy(target, {
get: function(tgt, key) {
console.log("Get %s", key);
return tgt[key] + 100;
},
set: function(tgt, key, val) {
console.log("Set %s = %s", key, val);
return tgt[key] = "VAL_" + val;
}
});
proxy.a = 2;
//Set a = 2
console.log(proxy.a);
//Get a
//VAL_2100
console.log(Reflect.get(target, "a"));
//VAL_2
Reflect.set(target, "a", 3);
console.log(Reflect.get(target, "a"));
//3複製程式碼
可以看到,如果直接在 Proxy 中存取目標物件的值,很可能呼叫多餘的 getter/setter;而搭配 Reflect 中對應的方法使用則可有效避免此情況
同時應注意到,在執行失敗時,這些方法並不丟擲錯誤,而是返回 false;這極大的簡化了處理:
//In ES5
var o = {};
Object.defineProperty(o, 'a', {
get: function() { return 1; },
configurable: false
});
try {
Object.defineProperty(o, 'a', { configurable: true });
} catch(e) {
console.log("Exception");
}
//In ES2015
var o = {};
Reflect.defineProperty(o, 'a', {
get: function() { return 1; },
configurable: false
});
if( !Reflect.defineProperty(o, 'a', { configurable: true }) ) {
console.log("Operation Failed");
}複製程式碼
[2.5] 配合使用 Proxy/Reflect
? 例子1:為物件的每個屬性設定 getter/setter
//in ES5
var obj = {
x: 1,
y: 2,
z: 3
};
function trace1() {
var cache = {};
Object.keys(obj).forEach(function(key) {
cache[key] = obj[key]; //避免迴圈 setter
Object.defineProperty(obj, key, {
get: function() {
console.log('Get ', key);
return cache[key];
},
set: function(vlu) {
console.log('Set ', key, vlu);
cache[key] = vlu;
}
})
});
}
trace1();
obj.x = 5;
console.log(obj.z);
// Set x 5
// Get z
// 3複製程式碼
//in ES6
var obj2 = {
x: 6,
y: 7,
z: 8
};
function trace2() {
return new Proxy(obj2, {
get(target, key) {
if (Reflect.has(target, key)) {
console.log('Get ', key);
}
return Reflect.get(target, key);
},
set(target, key, vlu) {
if (Reflect.has(target, key)) {
console.log('Set ', key, vlu);
}
return Reflect.set(target, key, vlu);
}
});
}
const proxy2 = trace2();
proxy2.x = 99;
console.log(proxy2.z);
// Set x 99
// Get z
// 8複製程式碼
? 例子2:跟蹤方法呼叫
var obj = {
x: 1,
y: 2,
say: function(word) {
console.log("hello ", word)
}
};
var proxy = new Proxy(obj, {
get(target, key) {
const targetValue = Reflect.get(target, key);
if (typeof targetValue === 'function') {
return function (...args) {
console.log('CALL', key, args);
return targetValue.apply(this, args);
}
} else {
console.log('Get ', key);
return targetValue;
}
}
});
proxy.x;
proxy.y;
proxy.say('excel!');
// Get x
// Get y
// CALL say [ 'excel!' ]
// hello excel!複製程式碼
總結
- getter/setter 也被稱為存取方法,是訪問方法中最常用的兩個
- 可以用訪問方法封裝保護原物件,並保留邏輯的靈活性
- ES5 中開始支援了隱式的 get 和 set 訪問方法,可以通過 delete 刪除
- 使用 使用 Object.defineProperty() 也可以設定 getter/setter 等
- 歷史上利用 Object.prototype.define[G,S]etter() 和 onpropertychange 實現存取方法的相容
- 可以利用代理和反射改善傳統的訪問控制
- 代理物件居於目標物件的使用者和目標物件本身的中間,並負責保護對目標物件的訪問
- ES6 原生的 Proxy 物件。用於定義基本操作的自定義行為(如屬性查詢,賦值,列舉,函式呼叫等)
- 物件的反射是一種在執行時探查和操作物件屬性的語言能力
- ES6 引入了 Reflect 物件,用來囊括物件反射的若干方法
- Reflect 有和 Proxy 一一對應的若干種方法,經常搭配使用
參考資料:
- dealwithjs.io/es6-feature…
- segmentfault.com/a/119000000…
- 2ality.com/2017/11/pro…
- www.zhihu.com/question/21…
- blogs.msdn.microsoft.com/ie/2010/09/…
- johndyer.name/native-brow…
- developer.mozilla.org/zh-CN/docs/…
- www.cnblogs.com/onlywujun/a…
- www.joezimjs.com/javascript/…
- stackoverflow.com/questions/7…
- www.importnew.com/9716.html
- help.adobe.com/en_US/AS2LC…
- blog.csdn.net/chy555chy/a…
- www.runoob.com/csharp/csha…
- php.net/manual/en/l…
- antimatter15.com/wp/2010/02/…
- www.52jbj.com/jbdq/512047…
- ponyfoo.com/articles/es…
- www.keithcirkel.co.uk/metaprogram…
- 2ality.com/2014/12/es6…
- qnimate.com/es6-reflect…