## 屬性的簡潔表示法
ES6允許直接寫入變量和函數,作為對象的屬性和方法。這樣的書寫更加簡潔。
~~~
function f( x, y ) {
return { x, y };
}
// 等同于
function f( x, y ) {
return { x: x, y: y };
}
~~~
上面是屬性簡寫的例子,方法也可以簡寫。
~~~
var o = {
method() {
return "Hello!";
}
};
// 等同于
var o = {
method: function() {
return "Hello!";
}
};
~~~
下面是一個更實際的例子。
~~~
var Person = {
name: '張三',
//等同于birth: birth
birth,
// 等同于hello: function ()...
hello() { console.log('我的名字是', this.name); }
};
~~~
這種寫法用于函數的返回值,將會非常方便。
~~~
function getPoint() {
var x = 1;
var y = 10;
return {x, y};
}
getPoint()
// {x:1, y:10}
~~~
## 屬性名表達式
JavaScript語言定義對象的屬性,有兩種方法。
~~~
// 方法一
obj.foo = true;
// 方法二
obj['a'+'bc'] = 123;
~~~
上面代碼的方法一是直接用標識符作為屬性名,方法二是用表達式作為屬性名,這時要將表達式放在方括號之內。
但是,如果使用字面量方式定義對象(使用大括號),在ES5中只能使用方法一(標識符)定義屬性。
~~~
var obj = {
foo: true,
abc: 123
};
~~~
ES6允許字面量定義對象時,用方法二(表達式)作為對象的屬性名,即把表達式放在方括號內。
~~~
let propKey = 'foo';
let obj = {
[propKey]: true,
['a'+'bc']: 123
};
~~~
下面是另一個例子。
~~~
var lastWord = "last word";
var a = {
"first word": "hello",
[lastWord]: "world"
};
a["first word"] // "hello"
a[lastWord] // "world"
a["last word"] // "world"
~~~
表達式還可以用于定義方法名。
~~~
let obj = {
['h'+'ello']() {
return 'hi';
}
};
console.log(obj.hello()); // hi
~~~
## 方法的name屬性
函數的name屬性,返回函數名。ES6為對象方法也添加了name屬性。
~~~
var person = {
sayName: function() {
console.log(this.name);
},
get firstName() {
return "Nicholas"
}
}
person.sayName.name // "sayName"
person.firstName.name // "get firstName"
~~~
上面代碼中,方法的name屬性返回函數名(即方法名)。如果使用了存值函數,則會在方法名前加上get。如果是存值函數,方法名的前面會加上set。
~~~
var doSomething = function() {
// ...
};
console.log(doSomething.bind().name); // "bound doSomething"
console.log((new Function()).name); // "anonymous"
~~~
有兩種特殊情況:bind方法創造的函數,name屬性返回“bound”加上原函數的名字;Function構造函數創造的函數,name屬性返回“anonymous”。
~~~
(new Function()).name // "anonymous"
var doSomething = function() {
// ...
};
doSomething.bind().name // "bound doSomething"
~~~
## Object.is()
Object.is()用來比較兩個值是否嚴格相等。它與嚴格比較運算符(===)的行為基本一致,不同之處只有兩個:一是+0不等于-0,二是NaN等于自身。
~~~
+0 === -0 //true
NaN === NaN // false
Object.is(+0, -0) // false
Object.is(NaN, NaN) // true
~~~
ES5可以通過下面的代碼,部署Object.is()。
~~~
Object.defineProperty(Object, 'is', {
value: function(x, y) {
if (x === y) {
// 針對+0 不等于 -0的情況
return x !== 0 || 1 / x === 1 / y;
}
// 針對NaN的情況
return x !== x && y !== y;
},
configurable: true,
enumerable: false,
writable: true
});
~~~
## Object.assign()
Object.assign方法用來將源對象(source)的所有可枚舉屬性,復制到目標對象(target)。它至少需要兩個對象作為參數,第一個參數是目標對象,后面的參數都是源對象。只要有一個參數不是對象,就會拋出TypeError錯誤。
~~~
var target = { a: 1 };
var source1 = { b: 2 };
var source2 = { c: 3 };
Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}
~~~
注意,如果目標對象與源對象有同名屬性,或多個源對象有同名屬性,則后面的屬性會覆蓋前面的屬性。
~~~
var target = { a: 1, b: 1 };
var source1 = { b: 2, c: 2 };
var source2 = { c: 3 };
Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}
~~~
assign方法有很多用處。
**(1)為對象添加屬性**
~~~
class Point {
constructor(x, y) {
Object.assign(this, {x, y});
}
}
~~~
上面方法通過assign方法,將x屬性和y屬性添加到Point類的對象實例。
**(2)為對象添加方法**
~~~
Object.assign(SomeClass.prototype, {
someMethod(arg1, arg2) {
···
},
anotherMethod() {
···
}
});
// 等同于下面的寫法
SomeClass.prototype.someMethod = function (arg1, arg2) {
···
};
SomeClass.prototype.anotherMethod = function () {
···
};
~~~
上面代碼使用了對象屬性的簡潔表示法,直接將兩個函數放在大括號中,再使用assign方法添加到SomeClass.prototype之中。
**(3)克隆對象**
~~~
function clone(origin) {
return Object.assign({}, origin);
}
~~~
上面代碼將原始對象拷貝到一個空對象,就得到了原始對象的克隆。
不過,采用這種方法克隆,只能克隆原始對象自身的值,不能克隆它繼承的值。如果想要保持繼承鏈,可以采用下面的代碼。
~~~
function clone(origin) {
let originProto = Object.getPrototypeOf(origin);
return Object.assign(Object.create(originProto), origin);
}
~~~
**(4)合并多個對象**
將多個對象合并到某個對象。
~~~
const merge =
(target, ...sources) => Object.assign(target, ...sources);
~~~
如果希望合并后返回一個新對象,可以改寫上面函數,對一個空對象合并。
~~~
const merge =
(...sources) => Object.assign({}, ...sources);
~~~
**(5)為屬性指定默認值**
~~~
const DEFAULTS = {
logLevel: 0,
outputFormat: 'html'
};
function processContent(options) {
let options = Object.assign({}, DEFAULTS, options);
}
~~~
上面代碼中,DEFAULTS對象是默認值,options對象是用戶提供的參數。assign方法將DEFAULTS和options合并成一個新對象,如果兩者有同名屬性,則option的屬性值會覆蓋DEFAULTS的屬性值。
## **proto**屬性,Object.setPrototypeOf(),Object.getPrototypeOf()
**(1)**proto**屬性**
**proto**屬性,用來讀取或設置當前對象的prototype對象。該屬性一度被正式寫入ES6草案,但后來又被移除。目前,所有瀏覽器(包括IE11)都部署了這個屬性。
~~~
// es6的寫法
var obj = {
__proto__: someOtherObj,
method: function() { ... }
}
// es5的寫法
var obj = Object.create(someOtherObj);
obj.method = function() { ... }
~~~
有了這個屬性,實際上已經不需要通過Object.create()來生成新對象了。
**(2)Object.setPrototypeOf()**
Object.setPrototypeOf方法的作用與**proto**相同,用來設置一個對象的prototype對象。它是ES6正式推薦的設置原型對象的方法。
~~~
// 格式
Object.setPrototypeOf(object, prototype)
// 用法
var o = Object.setPrototypeOf({}, null);
~~~
該方法等同于下面的函數。
~~~
function (obj, proto) {
obj.__proto__ = proto;
return obj;
}
~~~
**(3)Object.getPrototypeOf()**
該方法與setPrototypeOf方法配套,用于讀取一個對象的prototype對象。
~~~
Object.getPrototypeOf(obj)
~~~
## Symbol
### 概述
在ES5中,對象的屬性名都是字符串,這容易造成屬性名的沖突。比如,你使用了一個他人提供的對象,但又想為這個對象添加新的方法,新方法的名字有可能與現有方法產生沖突。如果有一種機制,保證每個屬性的名字都是獨一無二的就好了,這樣就從根本上防止屬性名的沖突。這就是ES6引入Symbol的原因。
ES6引入了一種新的原始數據類型Symbol,表示獨一無二的ID。它通過Symbol函數生成。這就是說,對象的屬性名現在可以有兩種類型,一種是原來就有的字符串,另一種就是新增的Symbol類型。凡是屬性名屬于Symbol類型,就都是獨一無二的,可以保證不會與其他屬性名產生沖突。
~~~
let s = Symbol();
typeof s
// "symbol"
~~~
上面代碼中,變量s就是一個獨一無二的ID。typeof運算符的結果,表明變量s是Symbol數據類型,而不是字符串之類的其他類型。
注意,Symbol函數前不能使用new命令,否則會報錯。這是因為生成的Symbol是一個原始類型的值,不是對象。也就是說,由于Symbol值不是對象,所以不能添加屬性。基本上,它是一種類似于字符串的數據類型。
Symbol函數可以接受一個字符串作為參數,表示對Symbol實例的描述,主要是為了在控制臺顯示,或者轉為字符串時,比較容易區分。
~~~
var s1 = Symbol('foo');
var s2 = Symbol('bar');
s1 // Symbol(foo)
s2 // Symbol(bar)
s1.toString() // "Symbol(foo)"
s2.toString() // "Symbol(bar)"
~~~
上面代碼中,s1和s2是兩個Symbol值。如果不加參數,它們在控制臺的輸出都是`Symbol()`,不利于區分。有了參數以后,就等于為它們加上了描述,輸出的時候就能夠分清,到底是哪一個值。
注意,Symbol函數的參數只是表示對當前Symbol類型的值的描述,因此相同參數的Symbol函數的返回值是不相等的。
~~~
// 沒有參數的情況
var s1 = Symbol();
var s2 = Symbol();
s1 === s2 // false
// 有參數的情況
var s1 = Symbol("foo");
var s2 = Symbol("foo");
s1 === s2 // false
~~~
上面代碼中,s1和s2都是Symbol函數的返回值,而且參數相同,但是它們是不相等的。
Symbol類型的值不能與其他類型的值進行運算,會報錯。
~~~
var sym = Symbol('My symbol');
"your symbol is " + sym
// TypeError: can't convert symbol to string
`your symbol is ${sym}`
// TypeError: can't convert symbol to string
~~~
但是,Symbol類型的值可以轉為字符串。
~~~
var sym = Symbol('My symbol');
String(sym) // 'Symbol(My symbol)'
sym.toString() // 'Symbol(My symbol)'
~~~
### 作為屬性名的Symbol
由于每一個Symbol值都是不相等的,這意味著Symbol值可以作為標識符,用于對象的屬性名,就能保證不會出現同名的屬性。這對于一個對象由多個模塊構成的情況非常有用,能防止某一個鍵被不小心改寫或覆蓋。
~~~
var mySymbol = Symbol();
// 第一種寫法
var a = {};
a[mySymbol] = 'Hello!';
// 第二種寫法
var a = {
[mySymbol]: 123
};
// 第三種寫法
var a = {};
Object.defineProperty(a, mySymbol, { value: 'Hello!' });
// 以上寫法都得到同樣結果
a[mySymbol] // "Hello!"
~~~
上面代碼通過方括號結構和Object.defineProperty,將對象的屬性名指定為一個Symbol值。
注意,Symbol值作為對象屬性名時,不能用點運算符。
~~~
var mySymbol = Symbol();
var a = {};
a.mySymbol = 'Hello!';
a[mySymbol] // undefined
a['mySymbol'] // "Hello!"
~~~
上面代碼中,因為點運算符后面總是字符串,所以不會讀取mySymbol作為標識名所指代的那個值,導致a的屬性名實際上是一個字符串,而不是一個Symbol值。
同理,在對象的內部,使用Symbol值定義屬性時,Symbol值必須放在方括號之中。
~~~
let s = Symbol();
let obj = {
[s]: function (arg) { ... }
};
obj[s](123);
~~~
上面代碼中,如果s不放在方括號中,該屬性的鍵名就是字符串s,而不是s所代表的那個Symbol值。
采用增強的對象寫法,上面代碼的obj對象可以寫得更簡潔一些。
~~~
let obj = {
[s](arg) { ... }
};
~~~
Symbol類型還可以用于定義一組常量,保證這組常量的值都是不相等的。
~~~
log.levels = {
DEBUG: Symbol('debug'),
INFO: Symbol('info'),
WARN: Symbol('warn'),
};
log(log.levels.DEBUG, 'debug message');
log(log.levels.INFO, 'info message');
~~~
還有一點需要注意,Symbol值作為屬性名時,該屬性還是公開屬性,不是私有屬性。
### 屬性名的遍歷
Symbol作為屬性名,該屬性不會出現在for...in、for...of循環中,也不會被`Object.keys()`、`Object.getOwnPropertyNames()`返回。但是,它也不是私有屬性,有一個Object.getOwnPropertySymbols方法,可以獲取指定對象的所有Symbol屬性名。
Object.getOwnPropertySymbols方法返回一個數組,成員是當前對象的所有用作屬性名的Symbol值。
~~~
var obj = {};
var a = Symbol('a');
var b = Symbol.for('b');
obj[a] = 'Hello';
obj[b] = 'World';
var objectSymbols = Object.getOwnPropertySymbols(obj);
objectSymbols
// [Symbol(a), Symbol(b)]
~~~
下面是另一個例子,Object.getOwnPropertySymbols方法與for...in循環、Object.getOwnPropertyNames方法進行對比的例子。
~~~
var obj = {};
var foo = Symbol("foo");
Object.defineProperty(obj, foo, {
value: "foobar",
});
for (var i in obj) {
console.log(i); // 無輸出
}
Object.getOwnPropertyNames(obj)
// []
Object.getOwnPropertySymbols(obj)
// [Symbol(foo)]
~~~
上面代碼中,使用Object.getOwnPropertyNames方法得不到Symbol屬性名,需要使用Object.getOwnPropertySymbols方法。
另一個新的API,Reflect.ownKeys方法可以返回所有類型的鍵名,包括常規鍵名和Symbol鍵名。
~~~
let obj = {
[Symbol('my_key')]: 1,
enum: 2,
nonEnum: 3
};
Reflect.ownKeys(obj)
// [Symbol(my_key), 'enum', 'nonEnum']
~~~
由于以Symbol值作為名稱的屬性,不會被常規方法遍歷得到。我們可以利用這個特性,為對象定義一些非私有的、但又希望只用于內部的方法。
~~~
var size = Symbol('size');
class Collection {
constructor() {
this[size] = 0;
}
add(item) {
this[this[size]] = item;
this[size]++;
}
static sizeOf(instance) {
return instance[size];
}
}
var x = new Collection();
Collection.sizeOf(x) // 0
x.add('foo');
Collection.sizeOf(x) // 1
Object.keys(x) // ['0']
Object.getOwnPropertyNames(x) // ['0']
Object.getOwnPropertySymbols(x) // [Symbol(size)]
~~~
上面代碼中,對象x的size屬性是一個Symbol值,所以`Object.keys(x)`、`Object.getOwnPropertyNames(x)`都無法獲取它。這就造成了一種非私有的內部方法的效果。
### Symbol.for(),Symbol.keyFor()
有時,我們希望重新使用同一個Symbol值,`Symbol.for`方法可以做到這一點。它接受一個字符串作為參數,然后搜索有沒有以該參數作為名稱的Symbol值。如果有,就返回這個Symbol值,否則就新建并返回一個以該字符串為名稱的Symbol值。
~~~
var s1 = Symbol.for('foo');
var s2 = Symbol.for('foo');
s1 === s2 // true
~~~
上面代碼中,s1和s2都是Symbol值,但是它們都是同樣參數的`Symbol.for`方法生成的,所以實際上是同一個值。
`Symbol.for()`與`Symbol()`這兩種寫法,都會生成新的Symbol。它們的區別是,前者會被登記在全局環境中供搜索,后者不會。`Symbol.for()`不會每次調用就返回一個新的Symbol類型的值,而是會先檢查給定的key是否已經存在,如果不存在才會新建一個值。比如,如果你調用`Symbol.for("cat")`30次,每次都會返回同一個Symbol值,但是調用`Symbol("cat")`30次,會返回30個不同的Symbol值。
~~~
Symbol.for("bar") === Symbol.for("bar")
// true
Symbol("bar") === Symbol("bar")
// false
~~~
上面代碼中,由于`Symbol()`寫法沒有登記機制,所以每次調用都會返回一個不同的值。
Symbol.keyFor方法返回一個已登記的Symbol類型值的key。
~~~
var s1 = Symbol.for("foo");
Symbol.keyFor(s1) // "foo"
var s2 = Symbol("foo");
Symbol.keyFor(s2) // undefined
~~~
上面代碼中,變量s2屬于未登記的Symbol值,所以返回undefined。
需要注意的是,`Symbol.for`為Symbol值登記的名字,是全局環境的,可以在不同的iframe或service worker中取到同一個值。
~~~
iframe = document.createElement('iframe');
iframe.src = String(window.location);
document.body.appendChild(iframe);
iframe.contentWindow.Symbol.for('foo') === Symbol.for('foo')
// true
~~~
上面代碼中,iframe窗口生成的Symbol值,可以在主頁面得到。
### 內置的Symbol值
除了定義自己使用的Symbol值以外,ES6還提供一些內置的Symbol值,指向語言內部使用的方法。
**(1)Symbol.hasInstance**
對象的Symbol.hasInstance屬性,指向一個內部方法。該對象使用instanceof運算符時,會調用這個方法,判斷該對象是否為某個構造函數的實例。比如,`foo instanceof Foo`在語言內部,實際調用的是`Foo[Symbol.hasInstance](foo)`。
**(2)Symbol.isConcatSpreadable**
對象的Symbol.isConcatSpreadable屬性,指向一個方法。該對象使用Array.prototype.concat()時,會調用這個方法,返回一個布爾值,表示該對象是否可以擴展成數組。
**(3)Symbol.isRegExp**
對象的Symbol.isRegExp屬性,指向一個方法。該對象被用作正則表達式時,會調用這個方法,返回一個布爾值,表示該對象是否為一個正則對象。
**(4)Symbol.match**
對象的Symbol.match屬性,指向一個函數。當執行`str.match(myObject)`時,如果該屬性存在,會調用它,返回該方法的返回值。
**(5)Symbol.iterator**
對象的Symbol.iterator屬性,指向該對象的默認遍歷器方法,即該對象進行for...of循環時,會調用這個方法,返回該對象的默認遍歷器,詳細介紹參見《Iterator和for...of循環》一章。
~~~
class Collection {
*[Symbol.iterator]() {
let i = 0;
while(this[i] !== undefined) {
yield this[i];
++i;
}
}
}
let myCollection = new Collection();
myCollection[0] = 1;
myCollection[1] = 2;
for(let value of myCollection) {
console.log(value);
}
// 1
// 2
~~~
**(6)Symbol.toPrimitive**
對象的Symbol.toPrimitive屬性,指向一個方法。該對象被轉為原始類型的值時,會調用這個方法,返回該對象對應的原始類型值。
**(7)Symbol.toStringTag**
對象的Symbol.toStringTag屬性,指向一個方法。在該對象上面調用`Object.prototype.toString`方法時,如果這個屬性存在,它的返回值會出現在toString方法返回的字符串之中,表示對象的類型。也就是說,這個屬性可以用來定制`[object Object]`或`[object Array]`中object后面的那個字符串。
~~~
class Collection {
get [Symbol.toStringTag]() {
return 'xxx';
}
}
var x = new Collection();
Object.prototype.toString.call(x) // "[object xxx]"
~~~
**(8)Symbol.unscopables**
對象的Symbol.unscopables屬性,指向一個對象。該對象指定了使用with關鍵字時,那些屬性會被with環境排除。
~~~
Array.prototype[Symbol.unscopables]
// {
// copyWithin: true,
// entries: true,
// fill: true,
// find: true,
// findIndex: true,
// keys: true
// }
Object.keys(Array.prototype[Symbol.unscopables])
// ['copyWithin', 'entries', 'fill', 'find', 'findIndex', 'keys']
~~~
上面代碼說明,數組有6個屬性,會被with命令排除。
~~~
// 沒有unscopables時
class MyClass {
foo() { return 1; }
}
var foo = function () { return 2; };
with (MyClass.prototype) {
foo(); // 1
}
// 有unscopables時
class MyClass {
foo() { return 1; }
get [Symbol.unscopables]() {
return { foo: true };
}
}
var foo = function () { return 2; };
with (MyClass.prototype) {
foo(); // 2
}
~~~
## Proxy
### 概述
Proxy用于修改某些操作的默認行為,等同于在語言層面做出修改,所以屬于一種“元編程”(meta programming),即對編程語言進行編程。
Proxy可以理解成,在目標對象之前架設一層“攔截”,外界對該對象的訪問,都必須先通過這層攔截,因此提供了一種機制,可以對外界的訪問進行過濾和改寫。Proxy這個詞的原意是代理,用在這里表示由它來“代理”某些操作,可以譯為“代理器”。
~~~
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);
}
});
~~~
上面代碼對一個空對象架設了一層攔截,重定義了屬性的讀取(get)和設置(set)行為。這里暫時不解釋具體的語法,只看運行結果。對設置了攔截行為的對象obj,去讀寫它的屬性,就會得到下面的結果。
~~~
obj.count = 1
// setting count!
++obj.count
// getting count!
// setting count!
// 2
~~~
上面代碼說明,Proxy實際上重載(overload)了點運算符,即用自己的定義覆蓋了語言的原始定義。
ES6原生提供Proxy構造函數,用來生成Proxy實例。
~~~
var proxy = new Proxy(target, handler)
~~~
Proxy對象的所用用法,都是上面這種形式,不同的只是handler參數的寫法。其中,`new Proxy()`表示生成一個Proxy實例,target參數表示所要攔截的目標對象,handler參數也是一個對象,用來定制攔截行為。
下面是另一個攔截讀取屬性行為的例子。
~~~
var proxy = new Proxy({}, {
get: function(target, property) {
return 35;
}
});
proxy.time // 35
proxy.name // 35
proxy.title // 35
~~~
上面代碼中,作為構造函數,Proxy接受兩個參數。第一個參數是所要代理的目標對象(上例是一個空對象),即如果沒有Proxy的介入,操作原來要訪問的就是這個對象;第二個參數是一個配置對象,對于每一個被代理的操作,需要提供一個對應的處理函數,該函數將攔截對應的操作。比如,上面代碼中,配置對象有一個get方法,用來攔截對目標對象屬性的訪問請求。get方法的兩個參數分別是目標對象和所要訪問的屬性。可以看到,由于攔截函數總是返回35,所以訪問任何屬性都得到35。
注意,要使得Proxy起作用,必須針對Proxy實例(上例是proxy對象)進行操作,而不是針對目標對象(上例是空對象)進行操作。
一個技巧是將Proxy對象,設置到`object.proxy`屬性,從而可以在object對象上調用。
~~~
var object = { proxy: new Proxy(target, handler) }
~~~
Proxy實例也可以作為其他對象的原型對象。
~~~
var proxy = new Proxy({}, {
get: function(target, property) {
return 35;
}
});
let obj = Object.create(proxy);
obj.time // 35
~~~
上面代碼中,proxy對象是obj對象的原型,obj對象本身并沒有time屬性,所有根據原型鏈,會在proxy對象上讀取該屬性,導致被攔截。
同一個攔截器函數,可以設置攔截多個操作。
~~~
var handler = {
get: function(target, name) {
if (name === 'prototype') return Object.prototype;
return 'Hello, '+ name;
},
apply: function(target, thisBinding, args) { return args[0]; },
construct: function(target, args) { return args[1]; }
};
var fproxy = new Proxy(function(x,y) {
return x+y;
}, handler);
fproxy(1,2); // 1
new fproxy(1,2); // 2
fproxy.prototype; // Object.prototype
fproxy.foo; // 'Hello, foo'
~~~
下面是Proxy支持的攔截操作一覽。
對于可以設置、但沒有設置攔截的操作,則直接落在目標對象上,按照原先的方式產生結果。
**(1)get(target, propKey, receiver)**
攔截對象屬性的讀取,比如`proxy.foo`和`proxy['foo']`,返回類型不限。最后一個參數receiver可選,當target對象設置了propKey屬性的get函數時,receiver對象會綁定get函數的this對象。
**(2)set(target, propKey, value, receiver)**
攔截對象屬性的設置,比如`proxy.foo = v`或`proxy['foo'] = v`,返回一個布爾值。
**(3)has(target, propKey)**
攔截`propKey in proxy`的操作,返回一個布爾值。
**(4)deleteProperty(target, propKey)**
攔截`delete proxy[propKey]`的操作,返回一個布爾值。
**(5)enumerate(target)**
攔截`for (var x in proxy)`,返回一個遍歷器。
**6)hasOwn(target, propKey)**
攔截`proxy.hasOwnProperty('foo')`,返回一個布爾值。
**(7)ownKeys(target)**
攔截`Object.getOwnPropertyNames(proxy)`、`Object.getOwnPropertySymbols(proxy)`、`Object.keys(proxy)`,返回一個數組。該方法返回對象所有自身的屬性,而`Object.keys()`僅返回對象可遍歷的屬性。
**(8)getOwnPropertyDescriptor(target, propKey)**
攔截`Object.getOwnPropertyDescriptor(proxy, propKey)`,返回屬性的描述對象。
**(9)defineProperty(target, propKey, propDesc)**
攔截`Object.defineProperty(proxy, propKey, propDesc)`、`Object.defineProperties(proxy, propDescs)`,返回一個布爾值。
**(10)preventExtensions(target)**
攔截`Object.preventExtensions(proxy)`,返回一個布爾值。
**(11)getPrototypeOf(target)**
攔截`Object.getPrototypeOf(proxy)`,返回一個對象。
**(12)isExtensible(target)**
攔截`Object.isExtensible(proxy)`,返回一個布爾值。
**(13)setPrototypeOf(target, proto)**
攔截`Object.setPrototypeOf(proxy, proto)`,返回一個布爾值。
如果目標對象是函數,那么還有兩種額外操作可以攔截。
**(14)apply(target, object, args)**
攔截Proxy實例作為函數調用的操作,比如`proxy(...args)`、`proxy.call(object, ...args)`、`proxy.apply(...)`。
**(15)construct(target, args, proxy)**
攔截Proxy實例作為構造函數調用的操作,比如new proxy(...args)。
下面是其中幾個重要攔截方法的詳細介紹。
### get()
get方法用于攔截某個屬性的讀取操作。上文已經有一個例子,下面是另一個攔截讀取操作的例子。
~~~
var person = {
name: "張三"
};
var proxy = new Proxy(person, {
get: function(target, property) {
if (property in target) {
return target[property];
} else {
throw new ReferenceError("Property \"" + property + "\" does not exist.");
}
}
});
proxy.name // "張三"
proxy.age // 拋出一個錯誤
~~~
上面代碼表示,如果訪問目標對象不存在的屬性,會拋出一個錯誤。如果沒有這個攔截函數,訪問不存在的屬性,只會返回undefined。
利用proxy,可以將讀取屬性的操作(get),轉變為執行某個函數。
~~~
var pipe = (function () {
var pipe;
return function (value) {
pipe = [];
return new Proxy({}, {
get: function (pipeObject, fnName) {
if (fnName == "get") {
return pipe.reduce(function (val, fn) {
return fn(val);
}, value);
}
pipe.push(window[fnName]);
return pipeObject;
}
});
}
}());
var double = function (n) { return n*2 };
var pow = function (n) { return n*n };
var reverseInt = function (n) { return n.toString().split('').reverse().join('')|0 };
pipe(3) . double . pow . reverseInt . get
// 63
~~~
上面代碼設置Proxy以后,達到了將函數名鏈式使用的效果。
### set()
set方法用來攔截某個屬性的賦值操作。假定Person對象有一個age屬性,該屬性應該是一個不大于200的整數,那么可以使用Proxy對象保證age的屬性值符合要求。
~~~
let validator = {
set: function(obj, prop, value) {
if (prop === 'age') {
if (!Number.isInteger(value)) {
throw new TypeError('The age is not an integer');
}
if (value > 200) {
throw new RangeError('The age seems invalid');
}
}
// 對于age以外的屬性,直接保存
obj[prop] = value;
}
};
let person = new Proxy({}, validator);
person.age = 100;
person.age // 100
person.age = 'young' // 報錯
person.age = 300 // 報錯
~~~
上面代碼中,由于設置了存值函數set,任何不符合要求的age屬性賦值,都會拋出一個錯誤。利用set方法,還可以數據綁定,即每當對象發生變化時,會自動更新DOM。
### apply()
apply方法攔截函數的調用、call和apply操作。
~~~
var target = function () { return 'I am the target'; };
var handler = {
apply: function (receiver, ...args) {
return 'I am the proxy';
}
};
var p = new Proxy(target, handler);
p() === 'I am the proxy';
// true
~~~
上面代碼中,變量p是Proxy的實例,當它作為函數調用時(p()),就會被apply方法攔截,返回一個字符串。
### ownKeys()
ownKeys方法用來攔截Object.keys()操作。
~~~
let target = {};
let handler = {
ownKeys(target) {
return ['hello', 'world'];
}
};
let proxy = new Proxy(target, handler);
Object.keys(proxy)
// [ 'hello', 'world' ]
~~~
上面代碼攔截了對于target對象的Object.keys()操作,返回預先設定的數組。
### 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實例,就會拋出一個錯誤。
## Reflect
### 概述
Reflect對象與Proxy對象一樣,也是ES6為了操作對象而提供的新API。Reflect對象的設計目的有這樣幾個。
(1) 將Object對象的一些明顯屬于語言層面的方法,放到Reflect對象上。現階段,某些方法同時在Object和Reflect對象上部署,未來的新方法將只部署在Reflect對象上。
(2) 修改某些Object方法的返回結果,讓其變得更合理。比如,`Object.defineProperty(obj, name, desc)`在無法定義屬性時,會拋出一個錯誤,而`Reflect.defineProperty(obj, name, desc)`則會返回false。
(3) 讓Object操作都變成函數行為。某些Object操作是命令式,比如`name in obj`和`delete obj[name]`,而`Reflect.has(obj, name)`和`Reflect.deleteProperty(obj, name)`讓它們變成了函數行為。
(4)Reflect對象的方法與Proxy對象的方法一一對應,只要是Proxy對象的方法,就能在Reflect對象上找到對應的方法。這就讓Proxy對象可以方便地調用對應的Reflect方法,完成默認行為,作為修改行為的基礎。
~~~
Proxy(target, {
set: function(target, name, value, receiver) {
var success = Reflect.set(target,name, value, receiver);
if (success) {
log('property '+name+' on '+target+' set to '+value);
}
return success;
}
});
~~~
上面代碼中,Proxy方法攔截target對象的屬性賦值行為。它采用Reflect.set方法將值賦值給對象的屬性,然后再部署額外的功能。
下面是get方法的例子。
~~~
var loggedObj = new Proxy(obj, {
get: function(target, name) {
console.log("get", target, name);
return Reflect.get(target, name);
}
});
~~~
### 方法
Reflect對象的方法清單如下。
* Reflect.getOwnPropertyDescriptor(target,name)
* Reflect.defineProperty(target,name,desc)
* Reflect.getOwnPropertyNames(target)
* Reflect.getPrototypeOf(target)
* Reflect.deleteProperty(target,name)
* Reflect.enumerate(target)
* Reflect.freeze(target)
* Reflect.seal(target)
* Reflect.preventExtensions(target)
* Reflect.isFrozen(target)
* Reflect.isSealed(target)
* Reflect.isExtensible(target)
* Reflect.has(target,name)
* Reflect.hasOwn(target,name)
* Reflect.keys(target)
* Reflect.get(target,name,receiver)
* Reflect.set(target,name,value,receiver)
* Reflect.apply(target,thisArg,args)
* Reflect.construct(target,args)
上面這些方法的作用,大部分與Object對象的同名方法的作用都是相同的。下面是對其中幾個方法的解釋。
(1)Reflect.get(target,name,receiver)
查找并返回target對象的name屬性,如果沒有該屬性,則返回undefined。
如果name屬性部署了讀取函數,則讀取函數的this綁定receiver。
~~~
var obj = {
get foo() { return this.bar(); },
bar: function() { ... }
}
// 下面語句會讓 this.bar()
// 變成調用 wrapper.bar()
Reflect.get(obj, "foo", wrapper);
~~~
(2)Reflect.set(target, name, value, receiver)
設置target對象的name屬性等于value。如果name屬性設置了賦值函數,則賦值函數的this綁定receiver。
(3)Reflect.has(obj, name)
等同于`name in obj`。
(4)Reflect.deleteProperty(obj, name)
等同于`delete obj[name]`。
(5)Reflect.construct(target, args)
等同于`new target(...args)`,這提供了一種不使用new,來調用構造函數的方法。
(6)Reflect.getPrototypeOf(obj)
讀取對象的__proto__屬性,等同于`Object.getPrototypeOf(obj)`。
(7)Reflect.setPrototypeOf(obj, newProto)
設置對象的__proto__屬性。注意,Object對象沒有對應這個方法的方法。
(8)Reflect.apply(fun,thisArg,args)
等同于`Function.prototype.apply.call(fun,thisArg,args)`。一般來說,如果要綁定一個函數的this對象,可以這樣寫`fn.apply(obj, args)`,但是如果函數定義了自己的apply方法,就只能寫成`Function.prototype.apply.call(fn, obj, args)`,采用Reflect對象可以簡化這種操作。
另外,需要注意的是,Reflect.set()、Reflect.defineProperty()、Reflect.freeze()、Reflect.seal()和Reflect.preventExtensions()返回一個布爾值,表示操作是否成功。它們對應的Object方法,失敗時都會拋出錯誤。
~~~
// 失敗時拋出錯誤
Object.defineProperty(obj, name, desc);
// 失敗時返回false
Reflect.defineProperty(obj, name, desc);
~~~
上面代碼中,Reflect.defineProperty方法的作用與Object.defineProperty是一樣的,都是為對象定義一個屬性。但是,Reflect.defineProperty方法失敗時,不會拋出錯誤,只會返回false。
## Object.observe(),Object.unobserve()
Object.observe方法用來監聽對象(以及數組)的變化。一旦監聽對象發生變化,就會觸發回調函數。
~~~
var user = {};
Object.observe(user, function(changes){
changes.forEach(function(change) {
user.fullName = user.firstName+" "+user.lastName;
});
});
user.firstName = 'Michael';
user.lastName = 'Jackson';
user.fullName // 'Michael Jackson'
~~~
上面代碼中,Object.observer方法監聽user對象。一旦該對象發生變化,就自動生成fullName屬性。
一般情況下,Object.observe方法接受兩個參數,第一個參數是監聽的對象,第二個函數是一個回調函數。一旦監聽對象發生變化(比如新增或刪除一個屬性),就會觸發這個回調函數。很明顯,利用這個方法可以做很多事情,比如自動更新DOM。
~~~
var div = $("#foo");
Object.observe(user, function(changes){
changes.forEach(function(change) {
var fullName = user.firstName+" "+user.lastName;
div.text(fullName);
});
});
~~~
上面代碼中,只要user對象發生變化,就會自動更新DOM。如果配合jQuery的change方法,就可以實現數據對象與DOM對象的雙向自動綁定。
回調函數的changes參數是一個數組,代表對象發生的變化。下面是一個更完整的例子。
~~~
var o = {};
function observer(changes){
changes.forEach(function(change) {
console.log('發生變動的屬性:' + change.name);
console.log('變動前的值:' + change.oldValue);
console.log('變動后的值:' + change.object[change.name]);
console.log('變動類型:' + change.type);
});
}
Object.observe(o, observer);
~~~
參照上面代碼,Object.observe方法指定的回調函數,接受一個數組(changes)作為參數。該數組的成員與對象的變化一一對應,也就是說,對象發生多少個變化,該數組就有多少個成員。每個成員是一個對象(change),它的name屬性表示發生變化源對象的屬性名,oldValue屬性表示發生變化前的值,object屬性指向變動后的源對象,type屬性表示變化的種類。基本上,change對象是下面的樣子。
~~~
var change = {
object: {...},
type: 'update',
name: 'p2',
oldValue: 'Property 2'
}
~~~
Object.observe方法目前共支持監聽六種變化。
* add:添加屬性
* update:屬性值的變化
* delete:刪除屬性
* setPrototype:設置原型
* reconfigure:屬性的attributes對象發生變化
* preventExtensions:對象被禁止擴展(當一個對象變得不可擴展時,也就不必再監聽了)
Object.observe方法還可以接受第三個參數,用來指定監聽的事件種類。
~~~
Object.observe(o, observer, ['delete']);
~~~
上面的代碼表示,只在發生delete事件時,才會調用回調函數。
Object.unobserve方法用來取消監聽。
~~~
Object.unobserve(o, observer);
~~~
注意,Object.observe和Object.unobserve這兩個方法不屬于ES6,而是屬于ES7的一部分。不過,Chrome瀏覽器從33版起就已經支持。