ES6新特性之Proxy对象

一、概述

在ES2015的标准中新增了一个Proxy,用于修改某些操作的默认行为,等同于在语言层面作出的修改,所以说这是属于一种元编程 ,(meta programming),即对编程语言进行编程。

Proxy对象用于创建一个对象的“代理”,从而实现基本能操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。

Proxy的语法结构如下所示:

const p = new Proxy(target, handler)

参数说明:

  1. target:要使用Proxy 代理的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
  2. handler:这个参数是一个配置对象,对于每一个被代理的操作,都需要提供一个对应的处理函数。

二、Proxy可拦截的操作

Proxy可拦截的操作如下表所示:

拦截方法

触发方式

get(target, propKey, receiver)

读取某个属性

set(target, propKey, value, receiver)

写⼊某个属性

has(target, propKey)

in操作符

deleteProperty(target, propKey)

delete操作符

getPrototypeOf(target)

Object.getPropertypeOf()

setPrototypeOf(target, proto)

Object.setPrototypeOf()

isExtensible(target)

Object.isExtensible()

preventExtensions(target)

Object.preventExtensions()

getOwnPropertyDescriptor(target, propKey)

Object.getOwnPropertyDescriptor()

defineProperty(target, propKey, propDesc)

Object.defineProperty()

ownKeys(target)

Object.keys() 、Object.getOwnPropertyNames()、Object.getOwnPropertySymbols()

apply(target, thisArg, args)

调⽤⼀个函数

construct(target, args)

⽤new调⽤⼀个函数

以上就是Proxy可拦截的所有操作。

三、Proxy实例的方法

下面我们就对上面那些拦截操作进行逐一演示

1. get()

get方法用于拦截某个属性的读取操作,该方法可以接受是三个参数,分别是目标对象、被获取的属性名和Proxy实例本身(可选)。

下面是对get的简单用法,如果我们访问一个对象中未定义的属性,可以通过get使其抛出一个异常。

const person = {
    name: '风筝'
}
const proxy = new Proxy(person, {
    /**
     * @param {object} target 目标对象
     * @param {string} propKey 被获取的属性名
     * @param {object} receiver Proxy或者继承Proxy的对象
     */
    get (target, propKey, receiver) {
        if (propKey in target) {
            return target[propKey]
        } else {
            throw new ReferenceError(`属性:${propKey} 不存在`)
        }
    }
})
proxy.name // 风筝
proxy.hobby // 抛出异常

get方法是可以继承的,看下面这段代码

const person = {
    name: '风筝'
}
const proxy = new Proxy(person, {
    get (target, properKey, receiver) {
        console.log(`获取${properKey}属性的值:${target[properKey]}`)
        return target[properKey]
    }
})
// 通过proxy创建一个新的对象
const obj = Object.create(proxy)
obj.name   // 获取name属性的值:风筝

上面代码中,拦截操作定义在Prototype对象上面,所以如果读取obj对象继承的属性时,拦截会生效。

我们还可以通过get方法去实现数据读取负数的索引,实现代码如下:

function createArray (...elements) {
    let handler = {
        get (target, properKey, receiver) {
            // 1. 缓存传入的索引值
            let index = Number(properKey)
            if (index < 0) {
                // 例如target的长度为7,传递的index为-1, 最后的properKey等于6即最后一项
                properKey = String(target.length + index)
            }
            // Reflect.get() 的作用是读取一个对象中的值
            return Reflect.get(target, properKey, receiver)
        }
    }
    let target = []
    target.push(...elements)
    return new Proxy(target, handler)
}
const arr = createArray(1, 2, 3, 4, 5)
console.log(arr[-1])   // 5

值得注意的是,如果一个属性是不可配置(configurable)且不可写(writable),则Proxy不能修改该属性,否则就会抛出异常。

2. set()

set方法用于拦截某个对象的修改操作,该方法接受四个参数分别是目标对象、被获取的属性名、修改后的值和Proxy实例本身(可选)。

下面这个例子就规定了修改某个属性必须是某个类型的值,否则就会抛出异常

const person = {
    name: '风筝'
}
// 1. 根据指定条件修改name属性
const proxy = new Proxy(person, {
    /**
     * @param {object} target 目标对象
     * @param {string} propKey 被获取的属性名
     * @param {any} value 修改后的值
     * @param {object} receiver Proxy或者继承Proxy的对象
     * @returns {boolean} 返回true表示成功,严格模式下返回false则会抛出一个异常
     */
    set (target, propKey, value, receiver) {
        // 规定修改name属性只能修改为一个字符串,否则就会抛出异常
        if (propKey === 'name' && typeof value !== "string") {
            throw new TypeError('修改的name并不是一个string')
        }
        target[propKey] = value
        return true
    }
})
// proxy.name = 1 // 抛出异常
proxy.name = '风筝' // 修改成功

我们该可以利用set方法进行数据绑定,如果对象发生改变,就自动更新我们的DOM。

示例代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.0.2/css/bootstrap.min.css" rel="stylesheet" />
  <title>通过set自动更新dom</title>
</head>
<body>
  <div class="card" style="width: 300px; margin: 100px auto">
    <div class="card-body">
      <h1 id="name"></h1>
      <button id="btn" class="btn btn-primary">修改</button>
    </div>
  </div>
  <script>
    // 获取DOM节点
    const name = document.getElementById('name')
    const btn = document.getElementById('btn')
    // 定义一个修改值的函数
    const updateDOM = (el, value) => {
      el.innerHTML = value
    }
    const person = new Proxy({
      name: '风正',
    }, {
      set(target, propKey, value) {
        // 如果里面的值改变就去调用我们的updateDOM
        updateDOM(name, value)
        target[propKey] = value
        return true
      },
    })
    name.innerHTML = person.name
    // 点击按钮触发修改操作
    btn.addEventListener('click', () => {
      person.name === '风筝' ? (person.name = '风正') : (person.name = '风筝')
    })
  </script>
</body>
</html>

值得注意的是 如果目标对象自身的某个属性不可写,那么set方法将不起作用。

3. apply()

apply方法拦截函数的调用、call、apply和Reflect.apply()操作。

该方法接受三个参数分别是目标对象、目标对象的上下文对象(this)和目标对象的参数数组。

示例代码如下:

const proxy = new Proxy(sayMe = () => { }, {
    apply (target, thisArg, args) {
        return '风筝'
    }
})
console.log(proxy()) // 风筝

值得注意的是目标对象必须是可调用的,也就是说必须是一个函数。

4. construct()

construct用于拦截new关键字或者Reflect.construct()。

该方法接受三个参数,分别是目标对象、constructor的参数列表和最初被调用的构造函数。该方法必须返回一个对象,否则会抛出异常。

示例代码如下:

function Hero (name) {
    this.name = name
}
const proxy = new Proxy(Hero, {
    construct (target, argArray, newTarget) {
        console.log('construct执行了');
        return new target(...argArray)
    }
})
new proxy('风筝')  // construct执行了

值得注意的是:由于construct()拦截的是构造函数,所以它的目标对象必须是函数,否则就会报错。

5. has()

has方法主要是用来拦截in操作符的,该方法接受两个参数,分别指的是目标对象和需要查询的属性名。

我们通过has方法,使_开头的属性不被in运算符发现,实现代码如下:

const person = {
    name: '风筝',
    _hobby: 'coding'
}
const proxy = new Proxy(person, {
    /** 
     * @param {object} target 目标对象
     * @param {string} propKey 需查询的属性名
     * @returns {boolean} 
     */
    has (target, propKey) {
        if (propKey[0] === '_') return false
        return propKey in target
    }
})
console.log('name' in proxy) // true
console.log('_hobby' in proxy) // false

值得注意的是如果原对象不可配置或者禁止扩展,这时has就会抛出异常。

6. deleteProperty()

deleteProperty方法用于拦截对属性执行的delete操作(delete关键字以及Reflect.deleteProperty()方法),如果返回false或者抛出异常则无法对属性执行删除操作。

该方法接受两个参数,分别是是目标对象和被删除的属性名。

现在我们需要对_开头的属性不可进行delete操作,示例代码如下:

const person = {
    name: '风筝',
    _hobby: 'coding'
}
// 不可对 _ 开头的属性进行删除
const proxy = new Proxy(person, {
    /** 
     * @param {object} target 目标对象
     * @param {string} propKey 需删除的属性名
     * @returns {boolean} 返回true表示删除成功,返回false不可删除
     */
    deleteProperty (target, propKey) {
        if (propKey[0] === '_') return false
        return true
    }
})
delete proxy._hobby
console.log(proxy._hobby)	 // coding

值得注意的是 如果目标对象的属性是不可配置的,那么该属性不能被删除。

7. getPrototypeOf()

getPrototypeOf会拦截如下五种操作(方法/属性/运算符):

  1. Object.prototype.__proto__
  2. Object.prototype.isPrototypeOf()
  3. Object.getPrototypeOf()
  4. Reflect.getPrototypeOf()
  5. instanceof

该方法接受一个参数,就是被代理的目标对象,返回值是一个对象或者null。

示例代码如下:

var obj = {};
var p = new Proxy(obj, {
    getPrototypeOf (target) {
        return Array.prototype;
    }
});
console.log(
    Object.getPrototypeOf(p) === Array.prototype,  // true
    Reflect.getPrototypeOf(p) === Array.prototype, // true
    p.__proto__ === Array.prototype,               // true
    Array.prototype.isPrototypeOf(p),              // true
    p instanceof Array                             // true
);

8. setPrototypeOf()

setPrototypeOf会拦截如下两种种操作:

  1. Object.setPrototypeOf()
  2. Reflect.setPrototypeOf()

该方法接受两个参数,一个是要被修改的原型的目标对象 和对象的新原型或者null 。方法返回一个布尔值,返回true表示修改成功,返回false表示修改失败。

示例代码如下(只要修改原型就会抛出异常):

var proto = {};
var proxy = new Proxy({}, {
    /**
     * @param {object} target 要被修改的原型的目标对象
     * @param {object} proto 对象的新原型或者null
     * @returns {boolean} true表示修改成功,返回false表示修改失败
     */
    setPrototypeOf (target, proto) {
        throw new Error('无法改变原型');
    }
});
Object.setPrototypeOf(proxy, proto);  // Error: 无法改变原型

9. isExtensible()

isExtensible方法用于拦截Object.isExtensible()或者Reflect.isExtensible()操作。该方法接受一个参数,就是目标对象,返回一个布尔值(否则就会抛出异常)。

示例代码如下:

const proxy = new Proxy({}, {
    isExtensible () {
        console.log('isExtensible执行了');
        return true
    }
})
Object.isExtensible(proxy) // isExtensible执行了

10. preventExtensions()

preventExtensions方法用于拦截Object.preventExtensions()或者Reflect.preventExtensions()操作。该方法接受一个参数,就是目标对象,返回一个布尔值(如果目标对象是可扩展的,那么只能返回 false)。

示例代码如下:

const proxy = new Proxy({}, {
    preventExtensions (target) {
        console.log('preventExtensions执行了')
        Object.preventExtensions(target)
        return true
    }
})
Object.preventExtensions(proxy) // preventExtensions执行了

11. getOwnPropertyDescriptor()

getOwnPropertyDescriptor方法用于拦截Object.getOwnPropertyDescriptor()或者Reflect.getOwnPropertyDescriptor()操作。

该方法接受两个参数,一个是目标对象 和返回属性名称的描述 。该方法的返回值必须是一个Object或者undefined。

示例代码如下:

const proxy = new Proxy({ name: '风筝' }, {
    /**
     * @param {object} target 目标对象
     * @param {string} propKey 属性名
     */
    getOwnPropertyDescriptor (target, propKey) {
        console.log('getOwnPropertyDescriptor执行了');
        return Object.getOwnPropertyDescriptor(target, propKey)
    }
})
Object.getOwnPropertyDescriptors(proxy, 'name') // getOwnPropertyDescriptor执行了

12. defineProperty()

defineProperty方法用于拦截以下操作:

  1. Object.defineProperty()
  2. Reflect.defineProperty()
  3. proxy.property='value'

该方法接受三个参数,分别是目标对象,属性名和待定义或修改的属性的描述符。返回值是一个布尔型表示是否修改成功。

示例代码如下:

const proxy = new Proxy({ name: '风筝' }, {
    /**
     * @param {object} target 目标对象
     * @param {string} propKey 属性名
     * @param {object} descriptor 待定义或修改的属性的描述符
     * @returns {boolean} 是否修改成功
     */
    defineProperty (target, propKey, descriptor) {
        console.log('defineProperty执行了');
        Object.defineProperty(target, propKey, descriptor)
        return true
    }
})
Object.defineProperty(proxy, 'name', {
    value: '风筝',
    writable: false
}) // defineProperty执行了
console.log(proxy) // { name: '风筝' }

13. ownKeys()

ownKeys方法用来拦截对象自身属性的读取操作。具体来说,拦截以下操作。

  1. Object.getOwnPropertyNames()
  2. Object.getOwnPropertySymbols()
  3. Object.keys()
  4. Reflect.ownKeys()
  5. for...in循环

该方法接受一个参数,就是目标对象,必须返回一个可枚举对象,否则就会抛出异常。

示例代码如下:

const proxy = new Proxy({}, {
    ownKeys: function (target) {
        console.log('ownKeys执行了');
        return ['a', 'b', 'c'];
    }
});
Object.getOwnPropertyNames(proxy) // ownKeys执行了

四、Proxy.revocable()

Proxy.revocable() 方法可以用来创建一个可撤销的拦截对象。

语法结构如下所示:

let { proxy, revoke } = Proxy.revocable(target, handler);

  1. proxy:拦截对象,与new Proxy(target, handler)创建的无区别
  2. revoke:一个方法,执行该方法及撤销这个拦截对象
  3. target和handler同上

示例代码如下:

const person = { name: '风筝' }
const { proxy, revoke } = Proxy.revocable(person, {
    get (target, propKey, receiver) {
        if (propKey in target) {
            return target[propKey]
        } else {
            throw new ReferenceError(`属性:${propKey} 不存在`)
        }
    }
})
console.log(proxy.name)
// console.log(proxy.age) // ReferenceError: 属性:age 不存在
// 调用revoke无需传递任何参数
revoke()
// 一旦撤销就不允许再次访问
console.log(proxy.name) // Cannot perform 'get' on a proxy that has been revoked

五、this指向

在Proxy中参数target中的this是指向target的,而handler中的this是指向handler的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值