JavaScript之Proxy

Proxy代理是一個共通的概念,可以起到攔截的作用。ES6里將Proxy標(biāo)準(zhǔn)化了,提供了Proxy構(gòu)造函數(shù),用來生成Proxy實(shí)例。例如var p = new Proxy(target, handler);。參照MDN

構(gòu)造函數(shù)有兩個參數(shù),第一個參數(shù)target是要攔截的對象,第二個參數(shù)是攔截函數(shù)對象。先看一個最基本的例子,感受一下:

var handler = {
    get: function(target, name){
        return name in target ? target[name] : 'No prop!';
    }
};

var p = new Proxy({}, handler);
p.a = 1;
p.b = 2;

console.log(p.a);    //1
console.log(p.b);    //2
console.log(p.c);    //No prop!

上面例子中為Object對象定義了get的攔截行為。如果對象內(nèi)有該屬性,就返回屬性值。如果對象內(nèi)沒有該屬性,就返回錯誤信息。結(jié)果一目了然,當(dāng)你要get對象屬性值時,會被Proxy攔截到,最終得到的是經(jīng)由handler攔截函數(shù)處理后的值。小細(xì)節(jié)注意一下,如示例那樣,攔截操作是在Proxy實(shí)例對象p上進(jìn)行的,而非在{}對象上進(jìn)行的。

Proxy的handler回調(diào)函數(shù)提供了13種攔截行為:

  • getPrototypeOf / setPrototypeOf
  • isExtensible / preventExtensions
  • ownKeys / getOwnPropertyDescriptor
  • defineProperty / deleteProperty
  • get / set / has
  • apply / construct

getPrototypeOf / setPrototypeOf

handler.getPrototypeOf(target)可以攔截取對象的原型對象的行為:

Object.getPrototypeOf()
Reflect.getPrototypeOf()
.proto
Object.prototype.isPrototypeOf()
Instanceof

參數(shù)target即想獲取它原型對象的對象。返回值是返回該原型對象或null。參照MDN。例如:

var proto = {};
var p = new Proxy({}, {
    getPrototypeOf(target) {
        return proto;
    }
});
console.log(Object.getPrototypeOf(p) === proto);    // true

handler.setPrototypeOf(target, prototype)可以攔截變更對象的原型對象的行為:

Object.setPrototypeOf()
Reflect.setPrototypeOf()

參數(shù)target是目標(biāo)對象,參數(shù)prototype是給目標(biāo)對象設(shè)置的原型對象或null。返回值如果目標(biāo)對象的原型對象被成功改變,返回true,否則返回false。參照MDN。例如:

var handler = {
    setPrototypeOf (target, prototype) {
        return false;
    }
};
var newProto = {};
var p = new Proxy({}, handler);
console.log(Object.setPrototypeOf(p, newProto));
//TypeError: proxy setPrototypeOf handler returned false

console.log(Reflect.setPrototypeOf(p, newProto));   //false

isExtensible / preventExtensions

handler.isExtensible(target)可以攔截判斷對象是否可擴(kuò)展(即是否能追加新屬性)的行為:

Object.isExtensible()
Reflect.isExtensible()

參數(shù)target是目標(biāo)對象。返回值如果目標(biāo)對象可擴(kuò)展,返回true,否則返回false。參照MDN。例如:

var p = new Proxy({}, {
    isExtensible: function(target) {
        console.log("called");
        return true;
    }
});

console.log(Object.isExtensible(p)); 
//called
//true

handler.preventExtensions(target)可以攔截阻止對象被擴(kuò)展(即不能為對象增加新屬性,但是既有屬性的值仍然可以更改,也可以把屬性刪除)的行為:

Object.preventExtensions()
Reflect.preventExtensions()

參數(shù)target是目標(biāo)對象。返回值如果想阻止對象被擴(kuò)展返回true,否則返回false。但要注意只有在Object.isExtensible(proxy)為false時,才能返回true,否則會報錯。參照MDN。例如:

var obj = {};
var p = new Proxy(obj, {
    preventExtensions: function(target) {
        console.log(Object.isExtensible(target));
        return true;
    }
});
console.log(Object.preventExtensions(p));
//true
//TypeError: proxy can't report an extensible object as non-extensible

因?yàn)镺bject.isExtensible(target);返回ture,表示對象可擴(kuò)展,此時你攔截preventExtensions并返回true的話會報錯,無法阻止一個可擴(kuò)展對象進(jìn)行擴(kuò)展。所以通常應(yīng)該在handler.preventExtensions里調(diào)用Object.preventExtensions來阻止對象的可擴(kuò)展性,讓Object.isExtensible(target);返回false:

var obj = {};
obj.newProp = 1;
console.log(obj.newProp);    //1

var p = new Proxy(obj, {
    preventExtensions: function(target) {
        Object.preventExtensions(target);
        console.log(Object.isExtensible(target));
        return true;
    }
});
console.log(Object.preventExtensions(p));
//false
//Object {}

obj.newProp2 = 2;
console.log(obj.newProp2);    //undefined

ownKeys / getOwnPropertyDescriptor

handler.ownKeys(target)可以攔截獲取屬性名的行為:

Object.getOwnPropertyNames()
Object.getOwnPropertySymbols()
Object.keys()
Reflect.ownKeys()

參數(shù)target是目標(biāo)對象。返回一個數(shù)組包含對象所有自身的屬性,而Object.keys()僅返回對象可遍歷的屬性。參照MDN。例如攔截前綴為下劃線的屬性名:

let person = {
    _age: 33,
    _location: 'shanghai',
    name: 'Jack'
};
let handler = {
    ownKeys (target) {
        return Reflect.ownKeys(target).filter(key => key[0] !== '_');
    }
};
let p = new Proxy(person, handler);
for (let key of Object.keys(p)) {
    console.log(person[key]);
}
//Jack

handler.getOwnPropertyDescriptor(target, prop)可以攔截獲取自身屬性描述的行為:

Object.getOwnPropertyDescriptor()
Reflect.getOwnPropertyDescriptor()

參數(shù)target是目標(biāo)對象,參數(shù)prop是自身的屬性名。返回該屬性的描述或undefined。參照MDN。例如攔截獲取前綴為下劃線的屬性并返回undefined:

var handler = {
    getOwnPropertyDescriptor (target, key) {
        if (key[0] === '_') {
            return;
        }
        return Object.getOwnPropertyDescriptor(target, key);
    }
};
var target = { _foo: 'bar', baz: 'tar' };
var proxy = new Proxy(target, handler);
console.log(Object.getOwnPropertyDescriptor(proxy, 'wat'));     //undefined
console.log(Object.getOwnPropertyDescriptor(proxy, '_foo'));    //undefined
console.log(Object.getOwnPropertyDescriptor(proxy, 'baz'));       
//{ value: 'tar', writable: true, enumerable: true, configurable: true }

defineProperty / deleteProperty

handler.defineProperty(target, property, descriptor)可以攔截定義屬性的行為:

Object.defineProperty()
Reflect.defineProperty()

參數(shù)target是目標(biāo)對象,參數(shù)property是屬性名,參數(shù)descriptor是屬性描述符。返回值如果該屬性被定義成功,返回true,否則返回false。參照MDN。例如:

var obj = {};
var p = new Proxy(obj, {
    defineProperty: function(target, prop, descriptor) {
        console.log("called: " + prop);
        Object.defineProperty(target, "a", desc)
        return true;
    }
});

var desc = { configurable: true, enumerable: true, value: 10 };
console.log(Object.defineProperty(p, "a", desc));
//called: a
//Object { a=10 }
console.log(obj.a); //10

handler.deleteProperty(target, property)可以攔截delete行為:

Property deletion: delete proxy[foo] and delete proxy.foo
Reflect.deleteProperty()

參數(shù)target是目標(biāo)對象,參數(shù)property是要刪除的屬性名。返回值如果該屬性被刪除成功,返回true,否則返回false。參照MDN。例如不允許刪除前綴為下劃線的屬性:

var handler = {
    deleteProperty (target, key) {
        invariant(key, 'delete');
        return true;
    }
};
function invariant (key, action) {
    if (key[0] === '_') {
        throw new Error(`Invalid attempt to ${action} private "${key}" property`);
    }
}
var target = { _prop: 'foo' };
var proxy = new Proxy(target, handler);
delete proxy._prop;  //Error: Invalid attempt to delete private "_prop" property

get / set / has

handler.get(target, property, receiver)可以攔截讀取對象屬性值的行為:

Property access: proxy[foo]and proxy.bar
Inherited property access: Object.create(proxy)[foo]
Reflect.get()

參數(shù)target是目標(biāo)對象,參數(shù)property是屬性名,參數(shù)receiver是一個可選對象,有時我們必須要搜索幾個對象,可能是一個在receiver原型鏈上的對象。返回值就是屬性值。參照MDN。例如:

var person = {
    name: "Jack"
};
var p = new Proxy(person, {
    get: function(target, prop, receiver) {
        if (prop in target) {
            return target[prop];
        } else {
            throw new ReferenceError("Property \"" + prop + "\" does not exist.");
        }
    }
});

console.log(p.name);    //Jack
console.log(p.age);     //ReferenceError: Property "age" does not exist.

handler.set(target, property, value, receiver)可以攔截設(shè)置對象屬性值的行為:

Property assignment: proxy[foo] = bar and proxy.foo = bar
Inherited property assignment: Object.create(proxy)[foo] = bar
Reflect.set()

參數(shù)target是目標(biāo)對象,參數(shù)property是屬性名,參數(shù)value是屬性值,參數(shù)receiver是一個可選對象,有時我們必須要搜索幾個對象,可能是一個在receiver原型鏈上的對象。返回值如果設(shè)值成功,返回true,否則返回false。參照MDN。例如:

var handler = {
    set: function(obj, prop, value) {
        if (prop === 'age') {
            if (!Number.isInteger(value)) {
                throw new TypeError('The age is not an integer');
            }
        }
        obj[prop] = value;
        return true;
    }
};
var p = new Proxy({}, handler);
p.age = 100;
console.log(p.age);  //100
p.age = 'Jack';      //TypeError: The age is not an integer

示例中,如果值非數(shù)字則直接拋出異常。利用set方法,可以實(shí)現(xiàn)數(shù)據(jù)綁定,當(dāng)值發(fā)生變化時,自動更新DOM。

因?yàn)間et和set方法比較常用,再舉個例子。例如私有屬性可以在屬性名請加上_下劃線,但這只是潛規(guī)則,外部仍舊能暢通無阻地讀寫這些屬性?,F(xiàn)在用get和set方法來真正阻止外部讀寫帶下劃線的屬性:

var handler = {
    get (target, property) {
        invariant(property, 'get');
        return target[property];
    },
    set (target, property, value) {
        invariant(property, 'set');
        return true;
    }
};
function invariant (property, action) {
    if (property[0] === '_') {
        throw new Error(`Invalid attempt to ${action} private "${property}" property`);
    }
}
var target = {};
var p = new Proxy(target, handler);
p._prop;         //Error: Invalid attempt to get private "_prop" property
p._prop = 'c';   //Error: Invalid attempt to set private "_prop" property

handler.has(target, prop)可以攔截檢查是否含有該參數(shù)的in行為:

Property query: foo in proxy
Inherited property query: foo in Object.create(proxy)
with check: with(proxy) { (foo); }
Reflect.has()

參數(shù)target是目標(biāo)對象,參數(shù)prop是屬性名。返回值如果含有該屬性,返回true,否則返回false。參照MDN。例如用has方法隱藏帶下劃線前綴的屬性,不讓其被in運(yùn)算符發(fā)現(xiàn):

var handler = {
    has (target, key) {
        if (key[0] === '_') {
            return false;
        }
        return key in target;
    }
};
var target = { _prop: 'foo', prop: 'foo' };
var proxy = new Proxy(target, handler);
console.log('_prop' in proxy);    //false

如果原對象不可擴(kuò)展,用has攔截會報錯。

var obj = { a: 10 };
Object.preventExtensions(obj);
var p = new Proxy(obj, {
    has: function(target, prop) {
        return false;
    }
});

"a" in p; 
//TypeError: proxy can't report an existing own property as non-existent on a non-extensible object

注意,has方法攔截的是hasProperty操作,而不是hasOwnProperty操作,即has方法不care該屬性是對象自身的屬性,還是繼承來的屬性。另外,雖然for…in循環(huán)也用到了in運(yùn)算符,但是Chrome55,F(xiàn)irefox49,Opera39上試下來,for…in里并不觸發(fā)has攔截。

apply / construct

Proxy不止可以攔截對象的操作還能用這兩個方法攔截函數(shù)。

handler.apply(target, thisArg, argumentsList)可以攔截函數(shù)調(diào)用的行為,包括apply調(diào)用,call調(diào)用:

proxy(…args)
Function.prototype.apply() and Function.prototype.call()
Reflect.apply()

參數(shù)target是函數(shù)對象,參數(shù)thisArg是函數(shù)對象的this,參數(shù)argumentsList是函數(shù)參數(shù)。返回值可返回任意東西。參照MDN。例如:

var target = function () { return 'I am the target'; };
var handler = {
    apply: function () {
        return 'I am proxy';
    }
};
var p = new Proxy(target, handler);
console.log(p());    //I am the proxy

再看看apply和call的攔截:

var twice = {
    apply (target, ctx, args) {
        return Reflect.apply(...arguments) * 2;
    }
};
function sum (left, right) {
    return left + right;
};
var proxy = new Proxy(sum, twice);
console.log(proxy(1, 2));                //6
console.log(proxy.call(null, 3, 4));     //14
console.log(proxy.apply(null, [5, 6]));  //22

handler.construct(target, argumentsList, newTarget)可以攔截new命令:

new proxy(…args)
Reflect.construct()

參數(shù)target是目標(biāo)對象,參數(shù)argumentsList是構(gòu)造函數(shù)參數(shù),參數(shù)newTarget。返回new后的對象,注意必須是對象,否則例如返回數(shù)字會報錯。參照MDN。例如:

var p = new Proxy(function() {}, {
    construct: function(target, args) {
        console.log('called: ' + args.join(', '));
        return { value: args[0] * 10 };
    }
});
console.log(new p(1).value);
//called: 1
//10

同一個攔截器函數(shù),可以同時設(shè)置多個上面介紹的13種攔截方法:

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 {value: args[1]};
    }
};
var fproxy = new Proxy(function(x, y) {
    return x + y;
}, handler);
console.log(fproxy(1, 2));     //1
console.log(new fproxy(1,2));  //Object { value=2}
console.log(fproxy.prototype === Object.prototype);   //true
console.log(fproxy.foo);       //Hello, foo

Proxy.revocable()

上面介紹的都是handler對象的方法。Proxy自身還有個靜態(tài)方法Proxy.revocable(target, handler),用于創(chuàng)建并返回一個可取消的Proxy對象。返回的這個可取消的Proxy對象有兩個屬性:proxy和revoke

屬性proxy會調(diào)用new Proxy(target, handler)創(chuàng)建一個新的Proxy對象。屬性revoke是一個無參函數(shù),用于取消,即讓該P(yáng)roxy對象無效。例如:

var revocable = Proxy.revocable({}, {
    get: function(target, name) {
        return "[[" + name + "]]";
    }
});
var p = revocable.proxy;
console.log(p.foo); // "[[foo]]"

revocable.revoke();

console.log(p.foo);  //TypeError: illegal operation attempted on a revoked proxy
p.foo = 1;           //TypeError: illegal operation attempted on a revoked proxy
delete p.foo;        //TypeError: illegal operation attempted on a revoked proxy
console.log(typeof p); //object

示例中Proxy.revocable方法返回一個可取消的Proxy對象。調(diào)用該對象的proxy屬性得到真實(shí)的Proxy對象。如果不想用了,可以調(diào)用revoke()方法將該P(yáng)roxy對象無效化。之后對Proxy對象的任何操作都將拋出異常。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • defineProperty() 學(xué)習(xí)書籍《ECMAScript 6 入門 》 Proxy Proxy 用于修改某...
    Bui_vlee閱讀 708評論 0 1
  • 本人自學(xué)es6已經(jīng)有一段時間了,只覺得有些時候很是枯燥無味, 時而又覺得在以后的職業(yè)生涯中會很有用,因?yàn)閑s6的很...
    可樂_37d3閱讀 1,665評論 0 0
  • Proxy Proxy用于修改某些操作的默認(rèn)行為,等同于在語言層面作出修改,所以屬于一種“元編程”,即對編程語言進(jìn)...
    南藍(lán)NL閱讀 489評論 0 0
  • Proxy 對象 Proxy 用來修改某些默認(rèn)操作,等同于在語言層面做出修改。所以屬于一種元編程(meta pro...
    faremax閱讀 436評論 0 0
  • 先分享個常見錯誤: public class A {private String aa; public A(Str...
    啊燦閱讀 402評論 0 1

友情鏈接更多精彩內(nèi)容