鍍金池/ 教程/ HTML/ 對(duì)象的擴(kuò)展
數(shù)組的擴(kuò)展
Class和Module
Set 和 Map 數(shù)據(jù)結(jié)構(gòu)
異步操作
對(duì)象的擴(kuò)展
Generator 函數(shù)
數(shù)值的擴(kuò)展
變量的解構(gòu)賦值
Iterator 和 for...of 循環(huán)
Promise 對(duì)象
參考鏈接
ECMAScript 6簡(jiǎn)介
作者簡(jiǎn)介
字符串的擴(kuò)展
編程風(fēng)格
let 和 const 命令
函數(shù)的擴(kuò)展

對(duì)象的擴(kuò)展

屬性的簡(jiǎn)潔表示法

ES6 允許直接寫入變量和函數(shù),作為對(duì)象的屬性和方法。這樣的書寫更加簡(jiǎn)潔。

function f( x, y ) {
  return { x, y };
}

// 等同于

function f( x, y ) {
  return { x: x, y: y };
}

上面是屬性簡(jiǎn)寫的例子,方法也可以簡(jiǎn)寫。


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}

屬性名表達(dá)式

JavaScript 語言定義對(duì)象的屬性,有兩種方法。


// 方法一
obj.foo = true;

// 方法二
obj['a'+'bc'] = 123;

上面代碼的方法一是直接用標(biāo)識(shí)符作為屬性名,方法二是用表達(dá)式作為屬性名,這時(shí)要將表達(dá)式放在方括號(hào)之內(nèi)。

但是,如果使用字面量方式定義對(duì)象(使用大括號(hào)),在 ES5 中只能使用方法一(標(biāo)識(shí)符)定義屬性。


var obj = {
  foo: true,
  abc: 123
};

ES6 允許字面量定義對(duì)象時(shí),用方法二(表達(dá)式)作為對(duì)象的屬性名,即把表達(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()

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) {
      // 針對(duì)+0 不等于 -0的情況
      return x !== 0 || 1 / x === 1 / y;
    }
    // 針對(duì)NaN的情況
    return x !== x && y !== y;
  },
  configurable: true,
  enumerable: false,
  writable: true
});

Object.assign()

Object.assign 方法用來將源對(duì)象(source)的所有可枚舉屬性,復(fù)制到目標(biāo)對(duì)象(target)。它至少需要兩個(gè)對(duì)象作為參數(shù),第一個(gè)參數(shù)是目標(biāo)對(duì)象,后面的參數(shù)都是源對(duì)象。只要有一個(gè)參數(shù)不是對(duì)象,就會(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)對(duì)象與源對(duì)象有同名屬性,或多個(gè)源對(duì)象有同名屬性,則后面的屬性會(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)為對(duì)象添加屬性


class Point {
  constructor(x, y) {
    Object.assign(this, {x, y});
  }
}

上面方法通過 assign 方法,將 x 屬性和 y 屬性添加到 Point 類的對(duì)象實(shí)例。

(2)為對(duì)象添加方法


Object.assign(SomeClass.prototype, {
  someMethod(arg1, arg2) {
    ···
  },
  anotherMethod() {
    ···
  }
});

// 等同于下面的寫法
SomeClass.prototype.someMethod = function (arg1, arg2) {
  ···
};
SomeClass.prototype.anotherMethod = function () {
  ···
};

上面代碼使用了對(duì)象屬性的簡(jiǎn)潔表示法,直接將兩個(gè)函數(shù)放在大括號(hào)中,再使用 assign 方法添加到 SomeClass.prototype 之中。

(3)克隆對(duì)象


function clone(origin) {
  return Object.assign({}, origin);
}

上面代碼將原始對(duì)象拷貝到一個(gè)空對(duì)象,就得到了原始對(duì)象的克隆。

不過,采用這種方法克隆,只能克隆原始對(duì)象自身的值,不能克隆它繼承的值。如果想要保持繼承鏈,可以采用下面的代碼。


function clone(origin) {
  let originProto = Object.getPrototypeOf(origin);
  return Object.assign(Object.create(originProto), origin);
}

(4)合并多個(gè)對(duì)象

將多個(gè)對(duì)象合并到某個(gè)對(duì)象。

const merge =
  (target, ...sources) => Object.assign(target, ...sources);

如果希望合并后返回一個(gè)新對(duì)象,可以改寫上面函數(shù),對(duì)一個(gè)空對(duì)象合并。

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 對(duì)象是默認(rèn)值,options 對(duì)象是用戶提供的參數(shù)。assign 方法將 DEFAULTS 和 options 合并成一個(gè)新對(duì)象,如果兩者有同名屬性,則 option 的屬性值會(huì)覆蓋 DEFAULTS 的屬性值。

proto屬性,Object.setPrototypeOf(),Object.getPrototypeOf()

(1)proto屬性

proto屬性,用來讀取或設(shè)置當(dāng)前對(duì)象的 prototype 對(duì)象。該屬性一度被正式寫入 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() 來生成新對(duì)象了。

(2)Object.setPrototypeOf()

Object.setPrototypeOf 方法的作用與proto相同,用來設(shè)置一個(gè)對(duì)象的 prototype 對(duì)象。它是 ES6 正式推薦的設(shè)置原型對(duì)象的方法。


// 格式
Object.setPrototypeOf(object, prototype)

// 用法
var o = Object.setPrototypeOf({}, null);

該方法等同于下面的函數(shù)。


function (obj, proto) {
  obj.__proto__ = proto;
  return obj;
}

(3)Object.getPrototypeOf()

該方法與 setPrototypeOf 方法配套,用于讀取一個(gè)對(duì)象的 prototype 對(duì)象。


Object.getPrototypeOf(obj)

Symbol

概述

在 ES5 中,對(duì)象的屬性名都是字符串,這容易造成屬性名的沖突。比如,你使用了一個(gè)他人提供的對(duì)象,但又想為這個(gè)對(duì)象添加新的方法,新方法的名字有可能與現(xiàn)有方法產(chǎn)生沖突。如果有一種機(jī)制,保證每個(gè)屬性的名字都是獨(dú)一無二的就好了,這樣就從根本上防止屬性名的沖突。這就是 ES6 引入 Symbol 的原因。

ES6 引入了一種新的原始數(shù)據(jù)類型 Symbol,表示獨(dú)一無二的 ID。它通過 Symbol 函數(shù)生成。這就是說,對(duì)象的屬性名現(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è)原始類型的值,不是對(duì)象。也就是說,由于 Symbol 值不是對(duì)象,所以不能添加屬性?;旧?,它是一種類似于字符串的數(shù)據(jù)類型。

Symbol 函數(shù)可以接受一個(gè)字符串作為參數(shù),表示對(duì) 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ù),它們?cè)诳刂婆_(tái)的輸出都是Symbol(),不利于區(qū)分。有了參數(shù)以后,就等于為它們加上了描述,輸出的時(shí)候就能夠分清,到底是哪一個(gè)值。

注意,Symbol 函數(shù)的參數(shù)只是表示對(duì)當(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)'

作為屬性名的 Symbol

由于每一個(gè) Symbol 值都是不相等的,這意味著 Symbol 值可以作為標(biāo)識(shí)符,用于對(duì)象的屬性名,就能保證不會(huì)出現(xiàn)同名的屬性。這對(duì)于一個(gè)對(duì)象由多個(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,將對(duì)象的屬性名指定為一個(gè) Symbol 值。

注意,Symbol 值作為對(duì)象屬性名時(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 值。

同理,在對(duì)象的內(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)的對(duì)象寫法,上面代碼的 obj 對(duì)象可以寫得更簡(jiǎn)潔一些。

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 方法,可以獲取指定對(duì)象的所有 Symbol 屬性名。

Object.getOwnPropertySymbols 方法返回一個(gè)數(shù)組,成員是當(dāng)前對(duì)象的所有用作屬性名的 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)行對(duì)比的例子。

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è)特性,為對(duì)象定義一些非私有的、但又希望只用于內(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)]

上面代碼中,對(duì)象 x 的 size 屬性是一個(gè) Symbol 值,所以Object.keys(x)、Object.getOwnPropertyNames(x)都無法獲取它。這就造成了一種非私有的內(nèi)部方法的效果。

Symbol.for(),Symbol.keyFor()

有時(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 值,可以在主頁(yè)面得到。

內(nèi)置的 Symbol 值

除了定義自己使用的 Symbol 值以外,ES6 還提供一些內(nèi)置的 Symbol 值,指向語言內(nèi)部使用的方法。

(1)Symbol.hasInstance

對(duì)象的 Symbol.hasInstance 屬性,指向一個(gè)內(nèi)部方法。該對(duì)象使用 instanceof 運(yùn)算符時(shí),會(huì)調(diào)用這個(gè)方法,判斷該對(duì)象是否為某個(gè)構(gòu)造函數(shù)的實(shí)例。比如,foo instanceof Foo在語言內(nèi)部,實(shí)際調(diào)用的是Foo[Symbol.hasInstance](foo)。

(2)Symbol.isConcatSpreadable

對(duì)象的 Symbol.isConcatSpreadable 屬性,指向一個(gè)方法。該對(duì)象使用 Array.prototype.concat() 時(shí),會(huì)調(diào)用這個(gè)方法,返回一個(gè)布爾值,表示該對(duì)象是否可以擴(kuò)展成數(shù)組。

(3)Symbol.isRegExp

對(duì)象的 Symbol.isRegExp 屬性,指向一個(gè)方法。該對(duì)象被用作正則表達(dá)式時(shí),會(huì)調(diào)用這個(gè)方法,返回一個(gè)布爾值,表示該對(duì)象是否為一個(gè)正則對(duì)象。

(4)Symbol.match

對(duì)象的 Symbol.match 屬性,指向一個(gè)函數(shù)。當(dāng)執(zhí)行str.match(myObject)時(shí),如果該屬性存在,會(huì)調(diào)用它,返回該方法的返回值。

(5)Symbol.iterator

對(duì)象的 Symbol.iterator 屬性,指向該對(duì)象的默認(rèn)遍歷器方法,即該對(duì)象進(jìn)行 for...of 循環(huán)時(shí),會(huì)調(diào)用這個(gè)方法,返回該對(duì)象的默認(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

對(duì)象的 Symbol.toPrimitive 屬性,指向一個(gè)方法。該對(duì)象被轉(zhuǎn)為原始類型的值時(shí),會(huì)調(diào)用這個(gè)方法,返回該對(duì)象對(duì)應(yīng)的原始類型值。

(7)Symbol.toStringTag

對(duì)象的 Symbol.toStringTag 屬性,指向一個(gè)方法。在該對(duì)象上面調(diào)用Object.prototype.toString方法時(shí),如果這個(gè)屬性存在,它的返回值會(huì)出現(xiàn)在 toString 方法返回的字符串之中,表示對(duì)象的類型。也就是說,這個(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

對(duì)象的 Symbol.unscopables 屬性,指向一個(gè)對(duì)象。該對(duì)象指定了使用 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

概述

Proxy 用于修改某些操作的默認(rèn)行為,等同于在語言層面做出修改,所以屬于一種“元編程”(meta programming),即對(duì)編程語言進(jìn)行編程。

Proxy 可以理解成在目標(biāo)對(duì)象之前,架設(shè)一層“攔截”,外界對(duì)該對(duì)象的訪問,都必須先通過這層攔截,因此提供了一種機(jī)制,可以對(duì)外界的訪問進(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)對(duì)象(上例是一個(gè)空對(duì)象),即如果沒有 Proxy 的介入,操作原來要訪問的就是這個(gè)對(duì)象;第二個(gè)參數(shù)是一個(gè)設(shè)置對(duì)象,對(duì)于每一個(gè)被代理的操作,需要提供一個(gè)對(duì)應(yīng)的處理函數(shù),該函數(shù)將攔截對(duì)應(yīng)的操作。比如,上面代碼中,設(shè)置對(duì)象有一個(gè) get 方法,用來攔截對(duì)目標(biāo)對(duì)象屬性的訪問請(qǐng)求。get 方法的兩個(gè)參數(shù)分別是目標(biāo)對(duì)象和所要訪問的屬性。可以看到,由于攔截函數(shù)總是返回 35,所以訪問任何屬性都得到 35。

注意,要使得 Proxy 起作用,必須針對(duì) Proxy 實(shí)例(上例是 proxy 對(duì)象)進(jìn)行操作,而不是針對(duì)目標(biāo)對(duì)象(上例是空對(duì)象)進(jìn)行操作。

Proxy 實(shí)例也可以作為其他對(duì)象的原型對(duì)象。


var proxy = new Proxy({}, {
  get: function(target, property) {
    return 35;
  }
});

let obj = Object.create(proxy);

obj.time // 35

上面代碼中,proxy 對(duì)象是 obj 對(duì)象的原型,obj 對(duì)象本身并沒有 time 屬性,所有根據(jù)原型鏈,會(huì)在 proxy 對(duì)象上讀取該屬性,導(dǎo)致被攔截。

對(duì)于沒有設(shè)置攔截的操作,則直接落在目標(biāo)對(duì)象上,按照原先的方式產(chǎn)生結(jié)果。

Proxy 支持的攔截操作一覽。

  • defineProperty(target, propKey, propDesc):返回一個(gè)布爾值,攔截Object.defineProperty(proxy, propKey, propDesc)
  • deleteProperty(target, propKey) :返回一個(gè)布爾值,攔截delete proxy[propKey]
  • enumerate(target):返回一個(gè)遍歷器,攔截for (x in proxy)
  • get(target, propKey, receiver):返回類型不限,攔截對(duì)象屬性的讀取
  • getOwnPropertyDescriptor(target, propKey) :返回屬性的描述對(duì)象,攔截Object.getOwnPropertyDescriptor(proxy, propKey)
  • getPrototypeOf(target) :返回一個(gè)對(duì)象,攔截Object.getPrototypeOf(proxy)
  • has(target, propKey):返回一個(gè)布爾值,攔截propKey in proxy
  • isExtensible(target):返回一個(gè)布爾值,攔截Object.isExtensible(proxy)
  • ownKeys(target):返回一個(gè)數(shù)組,攔截Object.getOwnPropertyPropertyNames(proxy)、Object.getOwnPropertyPropertySymbols(proxy)、Object.keys(proxy)
  • preventExtensions(target):返回一個(gè)布爾值,攔截Object.preventExtensions(proxy)
  • set(target, propKey, value, receiver):返回一個(gè)布爾值,攔截對(duì)象屬性的設(shè)置
  • setPrototypeOf(target, proto):返回一個(gè)布爾值,攔截Object.setPrototypeOf(proxy, proto)

如果目標(biāo)對(duì)象是函數(shù),那么還有兩種額外操作可以攔截。

  • apply 方法:攔截 Proxy 實(shí)例作為函數(shù)調(diào)用的操作,比如 proxy(···)、proxy.call(···)、proxy.apply(···)。
  • construct 方法:攔截 Proxy 實(shí)例作為構(gòu)造函數(shù)調(diào)用的操作,比如 new proxy(···)。

get

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)對(duì)象不存在的屬性,會(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

set 方法用來攔截某個(gè)屬性的賦值操作。假定 Person 對(duì)象有一個(gè) age 屬性,該屬性應(yīng)該是一個(gè)不大于 200 的整數(shù),那么可以使用 Proxy 對(duì)象保證 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');
      }
    }

    // 對(duì)于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)對(duì)象發(fā)生變化時(shí),會(huì)自動(dòng)更新 DOM。

apply

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

ownKeys 方法用來攔截 Object.keys() 操作。


let target = {};

let handler = {
  ownKeys(target) {
    return ['hello', 'world'];
  }
};

let proxy = new Proxy(target, handler);

Object.keys(proxy)
// [ 'hello', 'world' ]

上面代碼攔截了對(duì)于 target 對(duì)象的 Object.keys()操作,返回預(yù)先設(shè)定的數(shù)組。

Proxy.revocable()

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è)對(duì)象,該對(duì)象的 proxy 屬性是 Proxy 實(shí)例,revoke 屬性是一個(gè)函數(shù),可以取消Proxy 實(shí)例。上面代碼中,當(dāng)執(zhí)行 revoke 函數(shù)之后,再訪問 Proxy 實(shí)例,就會(huì)拋出一個(gè)錯(cuò)誤。

Object.observe(),Object.unobserve()

Object.observe 方法用來監(jiān)聽對(duì)象(以及數(shù)組)的變化。一旦監(jiān)聽對(duì)象發(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 對(duì)象。一旦該對(duì)象發(fā)生變化,就自動(dòng)生成 fullName 屬性。

一般情況下,Object.observe 方法接受兩個(gè)參數(shù),第一個(gè)參數(shù)是監(jiān)聽的對(duì)象,第二個(gè)函數(shù)是一個(gè)回調(diào)函數(shù)。一旦監(jiān)聽對(duì)象發(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 對(duì)象發(fā)生變化,就會(huì)自動(dòng)更新 DOM。如果配合 jQuery 的 change 方法,就可以實(shí)現(xiàn)數(shù)據(jù)對(duì)象與 DOM 對(duì)象的雙向自動(dòng)綁定。

回調(diào)函數(shù)的 changes 參數(shù)是一個(gè)數(shù)組,代表對(duì)象發(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ù)組的成員與對(duì)象的變化一一對(duì)應(yīng),也就是說,對(duì)象發(fā)生多少個(gè)變化,該數(shù)組就有多少個(gè)成員。每個(gè)成員是一個(gè)對(duì)象(change),它的 name 屬性表示發(fā)生變化源對(duì)象的屬性名,oldValue 屬性表示發(fā)生變化前的值,object 屬性指向變動(dòng)后的源對(duì)象,type 屬性表示變化的種類?;旧?,change 對(duì)象是下面的樣子。


var change = {
  object: {...},
  type: 'update',
  name: 'p2',
  oldValue: 'Property 2'
}

Object.observe 方法目前共支持監(jiān)聽六種變化。

  • add:添加屬性
  • update:屬性值的變化
  • delete:刪除屬性
  • setPrototype:設(shè)置原型
  • reconfigure:屬性的 attributes 對(duì)象發(fā)生變化
  • preventExtensions:對(duì)象被禁止擴(kuò)展(當(dāng)一個(gè)對(duì)象變得不可擴(kuò)展時(shí),也就不必再監(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)支持。

上一篇:異步操作下一篇:字符串的擴(kuò)展