1. 程式人生 > >ES6 Proxy攔截器詳解

ES6 Proxy攔截器詳解

隱藏屬性 char reduce blob 圖片 urn 例子 函數的調用 sum

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則會報錯。
以上代碼的執行結果:
技術分享圖片


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)

ES6 Proxy攔截器詳解