ES6 允許直接寫入變量和函數(shù),作為對象的屬性和方法。這樣的書寫更加簡潔。
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!";
}
};
下面是一個(gè)更實(shí)際的例子。
var Person = {
name: '張三',
//等同于birth: birth
birth,
// 等同于hello: function ()...
hello() { console.log('我的名字是', this.name); }
};
這種寫法用于函數(shù)的返回值,將會(huì)非常方便。
function getPoint() {
var x = 1;
var y = 10;
return {x, y};
}
getPoint()
// {x:1, y:10}
JavaScript 語言定義對象的屬性,有兩種方法。
// 方法一
obj.foo = true;
// 方法二
obj['a'+'bc'] = 123;
上面代碼的方法一是直接用標(biāo)識(shí)符作為屬性名,方法二是用表達(dá)式作為屬性名,這時(shí)要將表達(dá)式放在方括號(hào)之內(nèi)。
但是,如果使用字面量方式定義對象(使用大括號(hào)),在 ES5 中只能使用方法一(標(biāo)識(shí)符)定義屬性。
var obj = {
foo: true,
abc: 123
};
ES6 允許字面量定義對象時(shí),用方法二(表達(dá)式)作為對象的屬性名,即把表達(dá)式放在方括號(hào)內(nèi)。
let propKey = 'foo';
let obj = {
[propKey]: true,
['a'+'bc']: 123
};
下面是另一個(gè)例子。
var lastWord = "last word";
var a = {
"first word": "hello",
[lastWord]: "world"
};
a["first word"] // "hello"
a[lastWord] // "world"
a["last word"] // "world"
表達(dá)式還可以用于定義方法名。
let obj = {
['h'+'ello']() {
return 'hi';
}
};
console.log(obj.hello()); // hi
Object.is() 用來比較兩個(gè)值是否嚴(yán)格相等。它與嚴(yán)格比較運(yùn)算符(===)的行為基本一致,不同之處只有兩個(gè):一是 +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 方法用來將源對象(source)的所有可枚舉屬性,復(fù)制到目標(biāo)對象(target)。它至少需要兩個(gè)對象作為參數(shù),第一個(gè)參數(shù)是目標(biāo)對象,后面的參數(shù)都是源對象。只要有一個(gè)參數(shù)不是對象,就會(huì)拋出 TypeError 錯(cuò)誤。
var target = { a: 1 };
var source1 = { b: 2 };
var source2 = { c: 3 };
Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}
注意,如果目標(biāo)對象與源對象有同名屬性,或多個(gè)源對象有同名屬性,則后面的屬性會(huì)覆蓋前面的屬性。
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 類的對象實(shí)例。
(2)為對象添加方法
Object.assign(SomeClass.prototype, {
someMethod(arg1, arg2) {
···
},
anotherMethod() {
···
}
});
// 等同于下面的寫法
SomeClass.prototype.someMethod = function (arg1, arg2) {
···
};
SomeClass.prototype.anotherMethod = function () {
···
};
上面代碼使用了對象屬性的簡潔表示法,直接將兩個(gè)函數(shù)放在大括號(hào)中,再使用 assign 方法添加到 SomeClass.prototype 之中。
(3)克隆對象
function clone(origin) {
return Object.assign({}, origin);
}
上面代碼將原始對象拷貝到一個(gè)空對象,就得到了原始對象的克隆。
不過,采用這種方法克隆,只能克隆原始對象自身的值,不能克隆它繼承的值。如果想要保持繼承鏈,可以采用下面的代碼。
function clone(origin) {
let originProto = Object.getPrototypeOf(origin);
return Object.assign(Object.create(originProto), origin);
}
(4)合并多個(gè)對象
將多個(gè)對象合并到某個(gè)對象。
const merge =
(target, ...sources) => Object.assign(target, ...sources);
如果希望合并后返回一個(gè)新對象,可以改寫上面函數(shù),對一個(gè)空對象合并。
const merge =
(...sources) => Object.assign({}, ...sources);
(5)為屬性指定默認(rèn)值
const DEFAULTS = {
logLevel: 0,
outputFormat: 'html'
};
function processContent(options) {
let options = Object.assign({}, DEFAULTS, options);
}
上面代碼中,DEFAULTS 對象是默認(rèn)值,options 對象是用戶提供的參數(shù)。assign 方法將 DEFAULTS 和 options 合并成一個(gè)新對象,如果兩者有同名屬性,則 option 的屬性值會(huì)覆蓋 DEFAULTS 的屬性值。
(1)proto屬性
proto屬性,用來讀取或設(shè)置當(dāng)前對象的 prototype 對象。該屬性一度被正式寫入 ES6 草案,但后來又被移除。目前,所有瀏覽器(包括 IE11)都部署了這個(gè)屬性。
// es6的寫法
var obj = {
__proto__: someOtherObj,
method: function() { ... }
}
// es5的寫法
var obj = Object.create(someOtherObj);
obj.method = function() { ... }
有了這個(gè)屬性,實(shí)際上已經(jīng)不需要通過 Object.create() 來生成新對象了。
(2)Object.setPrototypeOf()
Object.setPrototypeOf 方法的作用與proto相同,用來設(shè)置一個(gè)對象的 prototype 對象。它是 ES6 正式推薦的設(shè)置原型對象的方法。
// 格式
Object.setPrototypeOf(object, prototype)
// 用法
var o = Object.setPrototypeOf({}, null);
該方法等同于下面的函數(shù)。
function (obj, proto) {
obj.__proto__ = proto;
return obj;
}
(3)Object.getPrototypeOf()
該方法與 setPrototypeOf 方法配套,用于讀取一個(gè)對象的 prototype 對象。
Object.getPrototypeOf(obj)
在 ES5 中,對象的屬性名都是字符串,這容易造成屬性名的沖突。比如,你使用了一個(gè)他人提供的對象,但又想為這個(gè)對象添加新的方法,新方法的名字有可能與現(xiàn)有方法產(chǎn)生沖突。如果有一種機(jī)制,保證每個(gè)屬性的名字都是獨(dú)一無二的就好了,這樣就從根本上防止屬性名的沖突。這就是 ES6 引入 Symbol 的原因。
ES6 引入了一種新的原始數(shù)據(jù)類型 Symbol,表示獨(dú)一無二的 ID。它通過 Symbol 函數(shù)生成。這就是說,對象的屬性名現(xiàn)在可以有兩種類型,一種是原來就有的字符串,另一種就是新增的 Symbol 類型。凡是屬性名屬于 Symbol 類型,就都是獨(dú)一無二的,可以保證不會(huì)與其他屬性名產(chǎn)生沖突。
let s = Symbol();
typeof s
// "symbol"
上面代碼中,變量 s 就是一個(gè)獨(dú)一無二的 ID。typeof 運(yùn)算符的結(jié)果,表明變量 s 是 Symbol 數(shù)據(jù)類型,而不是字符串之類的其他類型。
注意,Symbol 函數(shù)前不能使用 new 命令,否則會(huì)報(bào)錯(cuò)。這是因?yàn)樯傻?Symbol 是一個(gè)原始類型的值,不是對象。也就是說,由于 Symbol 值不是對象,所以不能添加屬性?;旧希且环N類似于字符串的數(shù)據(jù)類型。
Symbol 函數(shù)可以接受一個(gè)字符串作為參數(shù),表示對 Symbol 實(shí)例的描述,主要是為了在控制臺(tái)顯示,或者轉(zhuǎn)為字符串時(shí),比較容易區(qū)分。
var s1 = Symbol('foo');
var s2 = Symbol('bar');
s1 // Symbol(foo)
s2 // Symbol(bar)
s1.toString() // "Symbol(foo)"
s2.toString() // "Symbol(bar)"
上面代碼中,s1 和 s2 是兩個(gè) Symbol 值。如果不加參數(shù),它們在控制臺(tái)的輸出都是Symbol()
,不利于區(qū)分。有了參數(shù)以后,就等于為它們加上了描述,輸出的時(shí)候就能夠分清,到底是哪一個(gè)值。
注意,Symbol 函數(shù)的參數(shù)只是表示對當(dāng)前 Symbol 類型的值的描述,因此相同參數(shù)的 Symbol 函數(shù)的返回值是不相等的。
// 沒有參數(shù)的情況
var s1 = Symbol();
var s2 = Symbol();
s1 === s2 // false
// 有參數(shù)的情況
var s1 = Symbol("foo");
var s2 = Symbol("foo");
s1 === s2 // false
上面代碼中,s1 和 s2 都是 Symbol 函數(shù)的返回值,而且參數(shù)相同,但是它們是不相等的。
Symbol 類型的值不能與其他類型的值進(jìn)行運(yùn)算,會(huì)報(bào)錯(cuò)。
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 類型的值可以轉(zhuǎn)為字符串。
var sym = Symbol('My symbol');
String(sym) // 'Symbol(My symbol)'
sym.toString() // 'Symbol(My symbol)'
由于每一個(gè) Symbol 值都是不相等的,這意味著 Symbol 值可以作為標(biāo)識(shí)符,用于對象的屬性名,就能保證不會(huì)出現(xiàn)同名的屬性。這對于一個(gè)對象由多個(gè)模塊構(gòu)成的情況非常有用,能防止某一個(gè)鍵被不小心改寫或覆蓋。
var mySymbol = Symbol();
// 第一種寫法
var a = {};
a[mySymbol] = 'Hello!';
// 第二種寫法
var a = {
[mySymbol]: 123
};
// 第三種寫法
var a = {};
Object.defineProperty(a, mySymbol, { value: 'Hello!' });
// 以上寫法都得到同樣結(jié)果
a[mySymbol] // "Hello!"
上面代碼通過方括號(hào)結(jié)構(gòu)和 Object.defineProperty,將對象的屬性名指定為一個(gè) Symbol 值。
注意,Symbol 值作為對象屬性名時(shí),不能用點(diǎn)運(yùn)算符。
var mySymbol = Symbol();
var a = {};
a.mySymbol = 'Hello!';
a[mySymbol] // undefined
a['mySymbol'] // "Hello!"
上面代碼中,因?yàn)辄c(diǎn)運(yùn)算符后面總是字符串,所以不會(huì)讀取 mySymbol 作為標(biāo)識(shí)名所指代的那個(gè)值,導(dǎo)致 a 的屬性名實(shí)際上是一個(gè)字符串,而不是一個(gè) Symbol 值。
同理,在對象的內(nèi)部,使用 Symbol 值定義屬性時(shí),Symbol 值必須放在方括號(hào)之中。
let s = Symbol();
let obj = {
[s]: function (arg) { ... }
};
obj[s](123);
上面代碼中,如果 s 不放在方括號(hào)中,該屬性的鍵名就是字符串s,而不是 s 所代表的那個(gè) Symbol 值。
采用增強(qiáng)的對象寫法,上面代碼的 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');
還有一點(diǎn)需要注意,Symbol 值作為屬性名時(shí),該屬性還是公開屬性,不是私有屬性。
Symbol 作為屬性名,該屬性不會(huì)出現(xiàn)在 for...in、for...of 循環(huán)中,也不會(huì)被Object.keys()
、Object.getOwnPropertyNames()
返回。但是,它也不是私有屬性,有一個(gè) Object.getOwnPropertySymbols 方法,可以獲取指定對象的所有 Symbol 屬性名。
Object.getOwnPropertySymbols 方法返回一個(gè)數(shù)組,成員是當(dāng)前對象的所有用作屬性名的 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)]
下面是另一個(gè)例子,Object.getOwnPropertySymbols 方法與 for...in 循環(huán)、Object.getOwnPropertyNames 方法進(jìn)行對比的例子。
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 方法。
另一個(gè)新的 API,Reflect.ownKeys 方法可以返回所有類型的鍵名,包括常規(guī)鍵名和 Symbol 鍵名。
let obj = {
[Symbol('my_key')]: 1,
enum: 2,
nonEnum: 3
};
Reflect.ownKeys(obj)
// [Symbol(my_key), 'enum', 'nonEnum']
由于以 Symbol 值作為名稱的屬性,不會(huì)被常規(guī)方法遍歷得到。我們可以利用這個(gè)特性,為對象定義一些非私有的、但又希望只用于內(nèi)部的方法。
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 屬性是一個(gè) Symbol 值,所以Object.keys(x)
、Object.getOwnPropertyNames(x)
都無法獲取它。這就造成了一種非私有的內(nèi)部方法的效果。
有時(shí),我們希望重新使用同一個(gè) Symbol 值,Symbol.for
方法可以做到這一點(diǎn)。它接受一個(gè)字符串作為參數(shù),然后搜索有沒有以該參數(shù)作為名稱的 Symbol 值。如果有,就返回這個(gè) Symbol 值,否則就新建并返回一個(gè)以該字符串為名稱的 Symbol 值。
var s1 = Symbol.for('foo');
var s2 = Symbol.for('foo');
s1 === s2 // true
上面代碼中,s1 和 s2 都是 Symbol 值,但是它們都是同樣參數(shù)的Symbol.for
方法生成的,所以實(shí)際上是同一個(gè)值。
Symbol.for()
與Symbol()
這兩種寫法,都會(huì)生成新的 Symbol。它們的區(qū)別是,前者會(huì)被登記在全局環(huán)境中供搜索,后者不會(huì)。Symbol.for()
不會(huì)每次調(diào)用就返回一個(gè)新的Symbol類型的值,而是會(huì)先檢查給定的 key 是否已經(jīng)存在,如果不存在才會(huì)新建一個(gè)值。比如,如果你調(diào)用Symbol.for("cat")
30 次,每次都會(huì)返回同一個(gè) Symbol 值,但是調(diào)用Symbol("cat")
30次,會(huì)返回30個(gè)不同的Symbol值。
Symbol.for("bar") === Symbol.for("bar")
// true
Symbol("bar") === Symbol("bar")
// false
上面代碼中,由于Symbol()
寫法沒有登記機(jī)制,所以每次調(diào)用都會(huì)返回一個(gè)不同的值。
Symbol.keyFor 方法返回一個(gè)已登記的 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 值登記的名字,是全局環(huán)境的,可以在不同的 iframe 或 service worker 中取到同一個(gè)值。
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 值以外,ES6 還提供一些內(nèi)置的 Symbol 值,指向語言內(nèi)部使用的方法。
(1)Symbol.hasInstance
對象的 Symbol.hasInstance 屬性,指向一個(gè)內(nèi)部方法。該對象使用 instanceof 運(yùn)算符時(shí),會(huì)調(diào)用這個(gè)方法,判斷該對象是否為某個(gè)構(gòu)造函數(shù)的實(shí)例。比如,foo instanceof Foo
在語言內(nèi)部,實(shí)際調(diào)用的是Foo[Symbol.hasInstance](foo)
。
(2)Symbol.isConcatSpreadable
對象的 Symbol.isConcatSpreadable 屬性,指向一個(gè)方法。該對象使用 Array.prototype.concat() 時(shí),會(huì)調(diào)用這個(gè)方法,返回一個(gè)布爾值,表示該對象是否可以擴(kuò)展成數(shù)組。
(3)Symbol.isRegExp
對象的 Symbol.isRegExp 屬性,指向一個(gè)方法。該對象被用作正則表達(dá)式時(shí),會(huì)調(diào)用這個(gè)方法,返回一個(gè)布爾值,表示該對象是否為一個(gè)正則對象。
(4)Symbol.match
對象的 Symbol.match 屬性,指向一個(gè)函數(shù)。當(dāng)執(zhí)行str.match(myObject)
時(shí),如果該屬性存在,會(huì)調(diào)用它,返回該方法的返回值。
(5)Symbol.iterator
對象的 Symbol.iterator 屬性,指向該對象的默認(rèn)遍歷器方法,即該對象進(jìn)行 for...of 循環(huán)時(shí),會(huì)調(diào)用這個(gè)方法,返回該對象的默認(rèn)遍歷器,詳細(xì)介紹參見《Iterator和for...of循環(huán)》一章。
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 屬性,指向一個(gè)方法。該對象被轉(zhuǎn)為原始類型的值時(shí),會(huì)調(diào)用這個(gè)方法,返回該對象對應(yīng)的原始類型值。
(7)Symbol.toStringTag
對象的 Symbol.toStringTag 屬性,指向一個(gè)方法。在該對象上面調(diào)用Object.prototype.toString
方法時(shí),如果這個(gè)屬性存在,它的返回值會(huì)出現(xiàn)在 toString 方法返回的字符串之中,表示對象的類型。也就是說,這個(gè)屬性可以用來定制[object Object]
或[object Array]
中 object 后面的那個(gè)字符串。
class Collection {
get [Symbol.toStringTag]() {
return 'xxx';
}
}
var x = new Collection();
Object.prototype.toString.call(x) // "[object xxx]"
(8)Symbol.unscopables
對象的 Symbol.unscopables 屬性,指向一個(gè)對象。該對象指定了使用 with 關(guān)鍵字時(shí),那些屬性會(huì)被 with 環(huán)境排除。
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']
上面代碼說明,數(shù)組有 6 個(gè)屬性,會(huì)被 with 命令排除。
// 沒有unscopables時(shí)
class MyClass {
foo() { return 1; }
}
var foo = function () { return 2; };
with (MyClass.prototype) {
foo(); // 1
}
// 有unscopables時(shí)
class MyClass {
foo() { return 1; }
get [Symbol.unscopables]() {
return { foo: true };
}
}
var foo = function () { return 2; };
with (MyClass.prototype) {
foo(); // 2
}
Proxy 用于修改某些操作的默認(rèn)行為,等同于在語言層面做出修改,所以屬于一種“元編程”(meta programming),即對編程語言進(jìn)行編程。
Proxy 可以理解成在目標(biāo)對象之前,架設(shè)一層“攔截”,外界對該對象的訪問,都必須先通過這層攔截,因此提供了一種機(jī)制,可以對外界的訪問進(jìn)行過濾和改寫。proxy 這個(gè)詞的原意是代理,用在這里表示由它來“代理”某些操作。
ES6 原生提供 Proxy 構(gòu)造函數(shù),用來生成 Proxy 實(shí)例。
var proxy = new Proxy({}, {
get: function(target, property) {
return 35;
}
});
proxy.time // 35
proxy.name // 35
proxy.title // 35
作為構(gòu)造函數(shù),Proxy 接受兩個(gè)參數(shù)。第一個(gè)參數(shù)是所要代理的目標(biāo)對象(上例是一個(gè)空對象),即如果沒有 Proxy 的介入,操作原來要訪問的就是這個(gè)對象;第二個(gè)參數(shù)是一個(gè)設(shè)置對象,對于每一個(gè)被代理的操作,需要提供一個(gè)對應(yīng)的處理函數(shù),該函數(shù)將攔截對應(yīng)的操作。比如,上面代碼中,設(shè)置對象有一個(gè) get 方法,用來攔截對目標(biāo)對象屬性的訪問請求。get 方法的兩個(gè)參數(shù)分別是目標(biāo)對象和所要訪問的屬性??梢钥吹剑捎跀r截函數(shù)總是返回 35,所以訪問任何屬性都得到 35。
注意,要使得 Proxy 起作用,必須針對 Proxy 實(shí)例(上例是 proxy 對象)進(jìn)行操作,而不是針對目標(biāo)對象(上例是空對象)進(jìn)行操作。
Proxy 實(shí)例也可以作為其他對象的原型對象。
var proxy = new Proxy({}, {
get: function(target, property) {
return 35;
}
});
let obj = Object.create(proxy);
obj.time // 35
上面代碼中,proxy 對象是 obj 對象的原型,obj 對象本身并沒有 time 屬性,所有根據(jù)原型鏈,會(huì)在 proxy 對象上讀取該屬性,導(dǎo)致被攔截。
對于沒有設(shè)置攔截的操作,則直接落在目標(biāo)對象上,按照原先的方式產(chǎn)生結(jié)果。
Proxy 支持的攔截操作一覽。
如果目標(biāo)對象是函數(shù),那么還有兩種額外操作可以攔截。
get 方法用于攔截某個(gè)屬性的讀取操作。上文已經(jīng)有一個(gè)例子,下面是另一個(gè)攔截讀取操作的例子。
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 // 拋出一個(gè)錯(cuò)誤
上面代碼表示,如果訪問目標(biāo)對象不存在的屬性,會(huì)拋出一個(gè)錯(cuò)誤。如果沒有這個(gè)攔截函數(shù),訪問不存在的屬性,只會(huì)返回 undefined。
利用 proxy,可以將讀取屬性的操作(get),轉(zhuǎn)變?yōu)閳?zhí)行某個(gè)函數(shù)。
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
上面代碼設(shè)置 Proxy 以后,達(dá)到了將函數(shù)名鏈?zhǔn)绞褂玫男Ч?/p>
set 方法用來攔截某個(gè)屬性的賦值操作。假定 Person 對象有一個(gè) age 屬性,該屬性應(yīng)該是一個(gè)不大于 200 的整數(shù),那么可以使用 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' // 報(bào)錯(cuò)
person.age = 300 // 報(bào)錯(cuò)
上面代碼中,由于設(shè)置了存值函數(shù) set,任何不符合要求的 age 屬性賦值,都會(huì)拋出一個(gè)錯(cuò)誤。利用 set 方法,還可以數(shù)據(jù)綁定,即每當(dāng)對象發(fā)生變化時(shí),會(huì)自動(dòng)更新 DOM。
apply 方法攔截函數(shù)的調(diào)用、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 的實(shí)例,當(dāng)它作為函數(shù)調(diào)用時(shí)(p()),就會(huì)被 apply 方法攔截,返回一個(gè)字符串。
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()操作,返回預(yù)先設(shè)定的數(shù)組。
Proxy.revocable 方法返回一個(gè)可取消的 Proxy 實(shí)例。
let target = {};
let handler = {};
let {proxy, revoke} = Proxy.revocable(target, handler);
proxy.foo = 123;
proxy.foo // 123
revoke();
proxy.foo // TypeError: Revoked
Proxy.revocable 方法返回一個(gè)對象,該對象的 proxy 屬性是 Proxy 實(shí)例,revoke 屬性是一個(gè)函數(shù),可以取消Proxy 實(shí)例。上面代碼中,當(dāng)執(zhí)行 revoke 函數(shù)之后,再訪問 Proxy 實(shí)例,就會(huì)拋出一個(gè)錯(cuò)誤。
Object.observe 方法用來監(jiān)聽對象(以及數(shù)組)的變化。一旦監(jiān)聽對象發(fā)生變化,就會(huì)觸發(fā)回調(diào)函數(shù)。
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 方法監(jiān)聽 user 對象。一旦該對象發(fā)生變化,就自動(dòng)生成 fullName 屬性。
一般情況下,Object.observe 方法接受兩個(gè)參數(shù),第一個(gè)參數(shù)是監(jiān)聽的對象,第二個(gè)函數(shù)是一個(gè)回調(diào)函數(shù)。一旦監(jiān)聽對象發(fā)生變化(比如新增或刪除一個(gè)屬性),就會(huì)觸發(fā)這個(gè)回調(diào)函數(shù)。很明顯,利用這個(gè)方法可以做很多事情,比如自動(dòng)更新 DOM。
var div = $("#foo");
Object.observe(user, function(changes){
changes.forEach(function(change) {
var fullName = user.firstName+" "+user.lastName;
div.text(fullName);
});
});
上面代碼中,只要 user 對象發(fā)生變化,就會(huì)自動(dòng)更新 DOM。如果配合 jQuery 的 change 方法,就可以實(shí)現(xiàn)數(shù)據(jù)對象與 DOM 對象的雙向自動(dòng)綁定。
回調(diào)函數(shù)的 changes 參數(shù)是一個(gè)數(shù)組,代表對象發(fā)生的變化。下面是一個(gè)更完整的例子。
var o = {};
function observer(changes){
changes.forEach(function(change) {
console.log('發(fā)生變動(dòng)的屬性:' + change.name);
console.log('變動(dòng)前的值:' + change.oldValue);
console.log('變動(dòng)后的值:' + change.object[change.name]);
console.log('變動(dòng)類型:' + change.type);
});
}
Object.observe(o, observer);
參照上面代碼,Object.observe 方法指定的回調(diào)函數(shù),接受一個(gè)數(shù)組(changes)作為參數(shù)。該數(shù)組的成員與對象的變化一一對應(yīng),也就是說,對象發(fā)生多少個(gè)變化,該數(shù)組就有多少個(gè)成員。每個(gè)成員是一個(gè)對象(change),它的 name 屬性表示發(fā)生變化源對象的屬性名,oldValue 屬性表示發(fā)生變化前的值,object 屬性指向變動(dòng)后的源對象,type 屬性表示變化的種類?;旧?,change 對象是下面的樣子。
var change = {
object: {...},
type: 'update',
name: 'p2',
oldValue: 'Property 2'
}
Object.observe 方法目前共支持監(jiān)聽六種變化。
Object.observe 方法還可以接受第三個(gè)參數(shù),用來指定監(jiān)聽的事件種類。
Object.observe(o, observer, ['delete']);
上面的代碼表示,只在發(fā)生 delete 事件時(shí),才會(huì)調(diào)用回調(diào)函數(shù)。
Object.unobserve 方法用來取消監(jiān)聽。
Object.unobserve(o, observer);
注意,Object.observe 和 Object.unobserve 這兩個(gè)方法不屬于 ES6,而是屬于 ES7 的一部分。不過,Chrome 瀏覽器從 33 版起就已經(jīng)支持。