ES6 Proxy攔截器詳解

apy發表於2018-11-18

Proxy 攔截器

如有錯誤,麻煩指正,共同學習

Proxy的原意是“攔截”,可以理解為對目標物件的訪問和操作之前進行一次攔截。提供了這種機制,所以可以對目標物件進行修改和過濾的操作。

    const proxy = new Proxy({}, {
        get(target, proper(Key) {
            console.log(`你的訪問被我攔截到了`) 
            return 1;s
        },
        set(target, properKey, properValue) {
            console.log(`你修改這個屬性被我攔截到了`)
        }
    })

Proxy 實際上過載了點運算子,即用自己的定義覆蓋了語言的原始定義。

語法:
const proxy = new Proxy(target, hanlder)

new Proxy生成一個 proxy的例項, target表示要攔截的目標,可以是物件或函式等。凡是對目標物件的一些操作都會經過攔截器的攔截處理。 hanlder 引數也是一個物件,它表示攔截配置,就如上例所示。

Proxy 例項也可以作為其他物件的原型物件。對這個目標物件進行操作,如果它自身沒有設定這個屬性,就會去它的原型物件上面尋找,從而出發攔截行為。如下例:

  const proxy = new Proxy({}, {
      get(target, key) {
          consoloe.log(`你訪問的屬性是${key}`)
      }
  })
  const newObject = Object.create(proxy)
  newObject.a  // 你訪問的屬性是a

提醒

  • 同一個攔截器可以攔截多個操作,只需要在第二個引數(hanlder)配置新增

  • 如果對這個目標物件沒有設定攔截行為,則直接落在目標物件上。

Proxy 支援的攔截操作

  • get(target, propKey, receiver) 攔截物件屬性讀取
  • set(target, propKey, value, receiver) 攔截物件的屬性設定
  • has(target, propKey) 攔截propkey in proxy
  • deleteProperty(target, propKey) 攔截delete proxy[propKey]
  • ownKeys(target)
  • getOwnPropertyDescriptor(target, propKey) 返回物件屬性的描述物件攔截
  • defineProperty(target, propKey, propDesc)
  • proventExtensions(target)
  • getPrototypeOf(target)
  • isExtensible(target)
  • setPrototypeOf(target, proto)
  • apply(target, object, args)
  • construct(target, args) 攔截 proxy 例項作為建構函式呼叫的操作

Proxy 例項的方法

get(target, key): 當訪問目標物件屬性的時候,會被攔截。

target: 目標物件
key: 訪問的key值

   const proxy = new Proxy({a:1,b:2}, {
       get(target, key) {
           console.log(`called`)
           return target[key]
       }
   }) 
   proxy.a // 1
   // called 會被列印出來 

上面的程式碼中,當讀取代理物件屬性的時候,會被get方法攔截。所以可以在攔截前做一些事情,比如必須訪問這個物件存在的屬性,如果訪問物件不存在的屬性就丟擲錯誤! 如下例:

    const obj = {
        name: `qiqingfu`,
        age: 21
    }
    const proxy = new Proxy(obj, {
        get(target, key) {
            if (key in target) {
                return target[key]
            } else {
                throw Error(`${key}屬性不存在`)
            }
        }
    })

以上程式碼讀取代理物件的屬性,如果存在就正常讀取,負責提示錯誤訪問的key值不存在。

如果一個屬性不可配置(configurable), 或者不可寫(writeble),則該屬性不能被代理

    const obj = Object.defineProperties({}, {
        foo: {
            value: `a`,
            writeble: false,  // 不可寫
            configurable: false, //不可配置
        }
    })
    const proxy = new Proxy(obj, {
        get(target, key) {
            return `qiqingfu`
        }
    })
    proxy.value // 報錯
場景例子:

通過get()方法可以實現一個函式的鏈式操作

    const pipe = (function(){
        return function (value) {
            const funcStack = []; // 存放函式的陣列
            const proxy = new Proxy({}, {
                get(target, fnName) {
                    if (fnName === `get`) {
                        return funcStack.reduce((val, nextfn) => {
                            return fn(val)
                        }, value)
                    }
                    funcStack.push(window[fnName])
                    return proxy  //返回一個proxy物件,以便鏈式操作
                }
            })
            return proxy
        }
    }())

    var add = x => x * 2;
    var math = y => y + 10;
    pipe(3).add.math.get // 16

set(target, key, value)方法用於攔截某個屬性的賦值操作

target: 目標物件
key: 要設定的key值
value: 設定的value值
返回值: Boolean

假如有一個prosen物件,要設定的值不能小於100,那麼就可以使用 set方法攔截。

    const prosen = {
        a: 101,
        b: 46,
        c: 200
    }
    const proxy = new Proxy(prosen, {
        set(target, key, value) {
            if (value < 100) {
                throw Error(`${value}值不能小於100`)
            } 
            target[key] = value
        }
    })

上面程式碼對prosen物件賦值,我們可以攔截判斷它賦值如果小於100就給它提示錯誤。

使用場景

  • 可以實現資料繫結,即資料發生變化時,我們可以攔截到,實時的更新DOM元素。
  • 還可以設定物件的內部資料不可被修改,表示這些屬性不能被外部訪問和修改,這是可以使用getset, 如下例

規定物件的內部屬性以_開頭的屬性不能進行讀寫操作。

    const obj = {
        name: `qiqingfu`,
        age: 21,
        _money: -100000,
        _father: `xxx`
    }
    function isSeal(key) {
        if (key.charAl(0) === `_`) {
            return true
        }
        return false
    }
    const proxy = new Proxy(obj, {
        get(target, key) {
            if (isSeal(key)) {
                throw Error(`${key},為內部屬性,不可以讀取`)
            }
            return target[key]
        },
        set(target, key, value) {
            if (isSeal(key)) {
                throw Error(`${key},為內部屬性,不可以修改`)
            }
            target[key] = value
            return true
        }
    })

以上程式碼obj物件設定了內部屬性,以_開頭的不支援讀寫。那麼可以使用Proxy對其進行攔截判斷。get和set中的key屬性如果是以_開頭的屬性就提示錯誤。 set方法修改完值後,返回的是一個布林值。 true成功,反則false為修改失敗。


apply(target, context, args) 方法可以攔截函式的呼叫,call()、apply()

target: 目標物件,
context: 目標物件的上下文物件
args: 函式呼叫時的引數陣列

const proxy = new Proxy(function(){}, {
    apply(target, context, args) {
            console.log(target, `target`)
            console.log(context, `context`)
            console.log(args, `args`)
            }
        })
    const obj = {
        a: 1
    }
    proxy.call(obj,1,2,3)

上面的程式碼是攔截一個函式的執行,分別列印:
target -> function(){}: 目標物件
context -> {a: 1}: 目標物件的上下文物件,也就是函式的呼叫者,這裡我們使用call,讓obj物件來呼叫這個函式。
args -> [1,2,3]: 目標物件函式呼叫時我們傳遞的引數,這裡會以陣列的形式接受。

例子:
再說下面一個例子之前,先了解一下Reflect.apply(), 下面是 MDN 的解釋
Reflect.apply() 通過指定的引數列表發起對目標(target)函式的呼叫。

語法: Reflect.apply(target, context, args)

target: 目標函式
context: 目標函式執行的上下文
args: 函式呼叫時傳入的實參列表,該列表應該是一個類陣列的物件

該方法和ES5的 function.prototype.apply() 方法類似。

下面對 sum 函式的呼叫進行攔截,並且將函式的執行結果 *2

    const sum = (num1, num2) => {
        return num1 + num2
    }
    const proxy = new Proxy(sum, {
        apply(target, context, args) {
            // 我們可以通過 Reflect.apply()來呼叫目標函式
            return Reflect.apply(...arguments) * 2
        }
    })
    
    proxy(3,4)  // 14

以上程式碼是對 sum函式進行代理,並且將其執行結果 * 2


has(target, key ) 方法即攔截 hasProperty操作, 判斷物件是否具有某個屬性時,這個方法會生效。

target: 目標物件,
key: 物件的屬性
返回值是一個布林值

如果原物件不可配置或者禁止擴充套件, 那麼has攔截會報錯。 for in迴圈雖然也有 in 操作符,但是has對 for in 迴圈不生效.

has在什麼情況下會進行攔截:

  • 屬性查詢: 例如 foo in window
  • 繼承屬性查詢: foo in Object.create(proxy)
  • with檢查: with(proxy) {}
  • Reflect.has()

例1:
使用 has方法隱藏屬性,使其不被 in 操作符發現。 就比如說物件以_開頭的屬性不能被發現。

    const prosen = {
        name: `qiqingfu`,
        _age: 21
    }
    const proxy = new Proxy(prosen, {
        has(target, key) {
            if (key.chatAt(0) === `_`) {
                return false
            }
            return key in target
        }
    })

例2: with檢查

with的定義總結

  • 在with語句塊中,只是改變了對變數的遍歷順序,由原本的從執行環境開始變為從with語句的物件開始。當嘗試在with語句塊中修改變數時,會搜尋with語句的物件是否有該變數,有就改變物件的值,沒有就建立,但是建立的變數依然屬於with語句塊所在的執行環境,並不屬於with物件。

  • 離開with語句塊後,遍歷順序就會再次變成從執行環境開始。
  • with語句接收的物件會新增到作用域鏈的前端並在程式碼執行完之後移除。

關於js with語句的一些理解

    let a = `global a` 
    const obj = {
        a: 1,
        b: 2
    }
    const fn = key => {
        console.log(key)
    }
    const proxy = new Proxy(obj, {
        has(target, key) {
            console.log(target, `target`)
            console.log(key, `key`)
        }
    })
    with(proxy) {
        fn(`a`)
    }
    //依此列印
    // {a: 1, b: 2} target
    // fn  key
    // a

以上程式碼是對obj物件進行代理, 通過with檢查, 訪問代理物件的 a 屬性會被 has方法攔截。那麼攔截的第一個target就是目標物件, 而第二個引數key是訪問 a時的with語句塊所在的執行環境。


construct(target, args) 方法用於攔截 new 命令。

target: 目標函式,
args: 建構函式的引數物件
返回值必須是一個 物件, 否則會報錯。

    const proxy = new Proxy(function() {}, {
        construct(target, args) {
            console.log(target, `target`)
            console.log(args, `args`)
            return new target(args)
        }
    })
    new proxy(1,2)
    
    // function() {}  `target`
    // [1,2]  `args`

如果返回值不是物件會報錯


deleteProperty(target, key) 攔截物件的 delete操作

target: 目標物件
key: 刪除的哪個key值
返回值: 布林值, true成功,false失敗

目標物件不可配置(configurable)屬性不能被deleteProperty刪除, 否則會報錯

const obj = Object.defineProperties({}, {
    a: {
        value: 1,
        configurable: false,
    },
    b: {
        value: 2,
        configurable: true
    }
})
const proxy = new Proxy(obj, {
    deleteProperty(target, key) {
        delete target[key]
        return true;
    }
    })
    
delete proxy.a  // 報錯
delete proxy.b // true

以上程式碼攔截 obj物件, 當進行刪除不可配置的屬性a時,會報錯。刪除b屬性時則成功。

應用場景:
我們可以指定內建屬性不可被刪除。如以_開頭的屬性不能被刪除

const obj = {
    _a: `a`,
    _b: `b`,
    c:  `c`
}
const proxy = new Proxy(obj, {
    deleteProperty(target, key) {
        if (key.charAt(0) === `_`) {
            throw Error(`${key}屬性不可被刪除`)
            return false
        }
        delete target[key]
        return true
    }
})

defindProperty(target, key, descriptor)方法攔截Object.defindProperty()操作

target: 目標物件,
key: 目標物件的屬性
descriptor: 要設定的描述物件
返回值: 布林值, true新增屬性成功, false則會報錯

const proxy = new Proxy({}, {
    defineProperty(target, key, descriptor) {
        console.log(target, `target`)
        console.log(key, `key`)
        console.log(descriptor, `descriptor`)
        return true
    }
})
Object.defineProperty(proxy, `a`, {
    value: 1
})

以上程式碼是攔截一個物件的Object.defindProperty()新增屬性的操作, 如果返回值為true,表示新增成功。返回值false則會報錯。
以上程式碼的執行結果:
ES6 Proxy攔截器詳解


getPrototypeOf(target) 方法,用來攔截獲取物件原型。

target: 代理物件

可以攔截一下獲取原型的操作:

  • Object.prototype. __ proto __
  • Object.prototype.isPrototypeOf()
  • Object.getPrototypeOf() 獲取一個物件的原型物件
  • instance 操作符

Object.prototype.isPrototypeOf() 方法

檢測一個物件的原型鏈上有沒有這個物件

語法: Objectactive.isPrototypeOf(object), 檢測object物件的原型鏈上有沒有Objectactive這個物件, 如果有返回true, 否則返回false

    const Objectactive = {a: 1}
    const object = Object.create(Objectactive)
    Objectactive.isPrototypeOf(object) // true

以上程式碼 Objectactive作為 object的原型物件,然後通過 isPrototypeOf 檢測object物件的原型鏈上有沒有Objectactive這個物件。 理所當然返回 true

使用 getPrototypeOf()攔截

const Objectactive = {a: 1}
    const object = Object.create(Objectactive)
    const proxy = new Proxy(object, {
        getPrototypeOf(target) {
            console.log(target, `target`)
            return Object.getPrototypeOf(target)
        }
    })
    let bl = Objectactive.isPrototypeOf(proxy)
    console.log(bl)
    
    // 依此列印結果: 
    /*
        {
            __proto__:
            a: 1,
            __proto__: Object
        } `target`
        
        true
    */

以上程式碼對 object物件進行代理,當訪問原型物件時,通過getPrototypeOf()方法攔截,target就是代理物件。

getPrototypeOf()方法的返回值必須是 null 或者物件,否則報錯。

isExtensible(target) 方法攔截 Object.isExtensible()方法

Object.isExtensible() 方法返回一個布林值,其檢查一個物件是否可擴充套件。

target: 目標物件

isExtensible()方法有一個強限制,它的返回值必須與目標物件的 isExtensible屬性保持一致。

    const testObj = {
        name: `apy`
    }
    const proxy = new Proxy(testObj, {
        isExtensible(target) {
            console.log(`攔截物件的isExtensible操作`)
            return true; // 這裡要返回true, 因為目標物件現在是可擴充套件的,如果返回 false會報錯
        }
    })
    console.log(Object.isExtensible(proxy)) 
    
    // 列印:
    
    // 攔截物件的isExtensible操作
    // true

以上程式碼通過Object.isExtensible()檢測一個物件是否可擴充套件,會被配置選項中的 isExtensible方法攔截。

那麼什麼情況下可以 return false

Object.preventExtensions(object): 將一個物件設定為不可擴充套件的

const testObj = {
    name: `apy`
}
Object.preventExtensions(testObj) // 將 testObj物件設定為不可擴充套件 

const proxy = new Proxy(testObj, {
    isExtensible(target) {
        console.log(`攔截物件的isExtensible操作`)
        return false // 因為testObj物件不可擴充套件,返回值要和目標物件的 Object.isExtensible一致。
    }
})

Object.isExtensible(testObj)

以上程式碼通過 proxy攔截物件的 Object.isExtensible方法, 並且攔截的返回值與Object.isExtensible一致。否則報錯


ownKeys(target)方法用於攔截物件自身的屬性讀取操作

target: 目標物件
返回值: Array<String, Symbol>, 返回值為陣列,且陣列中只能包含字串或Symbol型別的

會被 ownKeys 攔截的讀取操作

  • Object.getOwnPropertyNames()
  • Object.getOwnPropertySymbols()
  • Object.keys()
使用Object.keys 方法時,有三類屬性會被 ownKeys 過濾掉,並不會返回.
  • 目標物件 target 上壓根不存在的屬性
  • 屬性名為 Symbol
  • 還有就是目標物件上不可遍歷的屬性
    const obj = {
        a: 1,
        b: 2,
        [Symbol.for(`c`)]: 3
    }
    Object.defineProperty(obj, `d`, {
        value: 4,
        enumerable: false
    })
    const proxy = new Proxy(obj, {
        ownKeys(target) {
            return [`a`, `b`, [Symbol(`c`)], `d`, `e`]
        }
    })
    Object.keys(obj).forEach(key => {
        console.log(key)
    })
    // a
    // b

以上程式碼定義了一個 obj物件, 有其屬性a, b, [Symbol],d。並且d屬性是不可擴充套件的。那麼 ownKeys方法顯式返回 不可遍歷的屬性(d)Symbol和不存在的屬性e都會被過濾掉,那麼最終返回a和b

注意:
  • 如果目標物件包含不可配置(configurable)的屬性,那麼該屬性必須被 ownkeys方法返回。
  • 如果目標物件是不可擴充套件(preventExtensions)的物件,那麼 ownkeys返回必須返回這個物件的原有屬性,不能包含額外的屬性。

setPrototypeOf(target, proto) 方法攔截 Object.setPrototypeOf方法

target: 目標物件
proto: 要設定的原型物件
返回值 布林值

設定一個物件的原型物件操作,會被 setPrototypeOf攔截。

    const obj = {}
    const proxy = new Proxy(obj, {
        setPrototypeOf(target, proto) {
            console.log(`攔截設定原型操作`)
            // 內部手動設定原型,並且返回 boolean
            return Object.setPrototype(target, proto)
        }
    })
    Object.setPrototypeOf(proxy, {a: 1})

以上程式碼攔截Object.setPrototypeOf方法,所以會列印 攔截設定原型操作

使用場景, 禁止修改一個物件的原型,否則報錯

如上例子,攔截一個修改物件原型的操作,丟擲相應的錯誤就可以。

    const foo = {}
    const proxy = new Proxy(foo, {
        setPrototypeOf(target, key) {
            throw Error(`${target}不可以修改原型物件`)
        }
    })
    Object.setPrototypeOf(proxy, {a: 1})  // 報錯

相關文章