为了账号安全,请及时绑定邮箱和手机立即绑定

ES6代理与反射

标签:
JavaScript

图片描述

什么是代理(Proxy)?

代理(proxy)是一种可以拦截并且改变底层JavaScript引擎操作的包装器,在新语言中通过它暴露内部运作的对象。

简单来说代理允许你拦截目标对象上的底层操作,而这本来是JS引擎的内部能力,拦截行为适用了一个能响应特定操作的函数(被称之为陷阱)。

如何设置代理(Proxy)?

  • 语法:new Proxy(target, handler)
    • target 代理目标对象
    • handler 处理对象的操作程序
let person = {};
let proxy = new Proxy(person, {});
proxy.name = 'bob';

console.log(proxy.name); // bob
console.log(person.name); // bob

person.name = 'lynn';
console.log(proxy.name); // lynn
console.log(person.name); // lynn

以上的代码是通过代理proxy的方式给person对象进行赋值,proxy.nameperson.name的引用地址是一样的,所以最后修改了perosn.nameproxy.nameperson.name都会同时改变。

什么是反射接口?

以Reflect对象的形式出现,对象中方法的默认特性与相同的底层操作一致,而代理可以覆写这些操作,每个代理陷阱对于一个命名和参数都相同的Reflect方法。

代理陷阱的特性

代理方法 覆写的特性 默认特性
get() 读取一个属性值 Reflect.get()
set() 写入一个属性 Reflect.set()
has() in操作符 Reflect.has()
deleteProperty() delete操作符 Reflect.deleteProperty()
getPrototyOf() Object.getPrototypeOf() Reflect.getPrototypeOf()
setPrototyOf() Object.setPrototypeOf() Reflect.setPrototypeOf()
isExtensible() Object.isExtensible() Reflect.isExtensible()
preventExtensions() Object.preventExtensions() Reflect.preventExtensions()
getOwnPropertyDescriptor() Object.getOwnPropertyDescriptor() Reflect.getOwnPropertyDescriptor()
defineProperty() Object.defineProperty() Reflect.defineProperty()
apply() 调用一个函数 Reflect.apply()
construct() 用new调用一个函数 Reflect.construct()
ownkeys() Object.keys()、 Object.getOwnPropertyNames()、 Object.getOwnPropertySymbols() Reflect.ownKeys()

set方法

  • 作用:在对象给属性添加属性值时,可以添加一些判断验证,比如添加的属性值不少数值就抛出错误
  • 参数:
    • trapTargt 代理对象
    • key 要写入的属性键
    • value 被写入的属性值
    • receiver 操作发生的对象
let person = {
    name: 'bob'
}

let proxy = new Proxy(person, {
    set(trapTarget, key, value, receiver) {
        // 防止自身属性受到影响
        if (!trapTarget.hasOwnProperty(key)) {
            // 传入的值必须要是数值
            if (isNaN(value)) {
                throw new TypeError("传入的属性值必须是数字")
            }
        }

        // 如果传入的键值为age,那么为它检测传入的值是否为数值,且代理加上100
        if (key === 'age') {
            // 传入的值必须要是数值
            if (isNaN(value)) {
                throw new TypeError("传入的属性值必须是数字")
            } else {
                value += 100;
            }
        }

        // 通过上面的检测,就添加属性
        return Reflect.set(trapTarget, key, value, receiver);
    }
});

// 添加一个新属性
proxy.count = 1;
console.log(proxy.count); // 1
console.log(person.count); // 1

// 为已有的属性赋值
proxy.name = "lynn";
console.log(proxy.name); // lynn
console.log(person.name); // lynn

// 给不存在的属性赋值就会进入验证传入的值是否是数值,
// 如果否则抛出错误
// proxy.likes = "coding"; // TypeError: 传入的属性值必须是数字 ...
proxy.likes = 123; // 传入数值后正确

proxy.age = 10;
console.log(proxy.age); // 110   传入10,代理自动加上了100
console.log(person.age); // 110  传入10,代理自动加上了100

get方法

  • 作用:在读取属性时候,可以添加一些验证或者操作,比如属性不存在时,可以抛出错误,或者给读取的属性添加一些增改的操作
  • 参数
    • trapTarget 操作的对象
    • key 读取的属性
    • receiver 操作发生的对象
let person = {
    name: 'bob'
}

let proxy = new Proxy(person, {
    get(trapTarget, key, receiver) {
        // 检测属性是否存在
        if (!(key in receiver)) {
            throw new TypeError(`属性 ${key} 不存在`);
        }

        return Reflect.get(trapTarget, key, receiver);
    }
})

console.log(proxy.name); // bob
console.log(proxy.age); // TypeError: 属性 age 不存在

上面的例子,在读取属性时候,判断属性是否存在对象中,如果不存在直接抛出属性不存在错误,如果存在则直接读取。

has方法

  • 作用:使用in操作符可以检测属性是否在对象中,在代理has中可以拦截in操作符返回值
  • 参数
    • trapTarget 读取的对象
    • key 检测的属性键
let person = {
    name: 'bob',
    likes: 'coding'
}

let proxy = new Proxy(person, {
    /**
     * 作用:使用in操作符检测对象是否包含该值,使用has可以拦截in操作的返回值
     * @param trapTarget 读取属性的对象
     * @param key 检测的属性键
     */
    has(trapTarget, key) {
        if (key === 'name') {
            return false;
        } else {
            return Reflect.has(trapTarget, key)
        }
    }
});

console.log("name" in proxy); // true
console.log("likes" in proxy); // false

上面的例子,使用has方法拦截了对in操作符检测 “name” 键值返回false,如果检测其他的键值返回默认值。

deleteProperty方法

  • 作用:使用delete操作符可以删除对象属性,使用代理的deleteProperty方法可以进行对使用delete操作符删除属性操作一些验证,比如强制不允许删除
  • 参数
    • trapTarget 需要删除属性的对象
    • key 需要删除的属性键
let person = {
    name: 'bob',
    likes: 'coding'
}

let proxy = new Proxy(person, {
    deleteProperty(trapTarget, key) {
        if (key === 'name') {
            return false;
        } else {
            return Reflect.deleteProperty(trapTarget, key);
        }
    }
})

// 上面的代码限制了不允许删除 name 属性
// 测试一下删除 name 属性是否成功
delete proxy.name;
console.log("name" in proxy); // true // 测试是不能删除name属性的

// 试图删除一下 likes 属性
delete proxy.likes;
console.log("likes" in proxy); // false  // 测试可以删除 likes 属性

getPrototypeOf方法

  • Object.getPrototypeOf(object)返回指定对象的原型属性的值
  • 作用:拦截Object.getPrototypeOf(proxy),返回一个对象
  • 返回的结果必须是对象或者null
let person = {
    name: 'bob',
}

let proxy = new Proxy(person, {
    getPrototypeOf(trapTarget) {
        return null;
    }
})

console.log(Object.getPrototypeOf(person));
/**
 * 输出:
 * constructor: ƒ Object()
 * hasOwnProperty: ƒ hasOwnProperty()
 * isPrototypeOf: ƒ isPrototypeOf()
 * propertyIsEnumerable: ƒ propertyIsEnumerable()
 * toLocaleString: ƒ toLocaleString()
 * toString: ƒ toString()
 * valueOf: ƒ valueOf()
 */
console.log(Object.getPrototypeOf(proxy));

/**
 * 输出:null
 */

上面方法通过代理的getPrototypeOf()方法拦截了Object.prototypeOf()返回的原型对象,且设置返回对象为null

setPrototypeOf方法

  • Object.setPrototypeOf(obj, proto) 设置一个指定的对象的原型到另一个对象或 null
  • 拦截Object.setPrototypeOf(obj, proto)的结果且返回一个布尔值,如果操作失败返回的必须是false

复习一下Object.setPrototypeOf()方法:

// 构造函数
function Person(name) {
    this.name = name;
}

var p = new Person("bob");
//等同于将构造函数的原型对象赋给实例对象p的属性__proto__
p.__proto__ = Object.setPrototypeOf({}, Person.prototype);
Person.call(p,"bob");

代理setPrototypeOf方法:

let person = {
    name: 'bob'
}
let proxy = new Proxy(person, {
    setPrototypeOf(trapTarget, proto) {
        return false;
    }
})
console.log(Object.setPrototypeOf(person, {num: 123}));
console.log(person.name); // bob
console.log(person.num); // 123

console.log(Object.setPrototypeOf(proxy, {num: 456}));
// 输出:Uncaught SyntaxError: Identifier 'person' has already been declared

console.log(proxy.name); //  由于上面报错会下面代码会停止输出:实际上打印是"bob"
console.log(proxy.num); 
//  由于上面报错会下面代码会停止输出:实际上打印是"123",没有设置成功的,
// 原因是因为代理的setPrototypeOf设置返回false

上面的方法代理使用了setPrototypeOf方法设置返回false,这时候回抛出错误。

isExtensible方法和preventExtensions方法

  • Object.isExtensible() 方法判断一个对象是否可以在它上面添加新的属性
  • 代理isExtensible方法
    • 作用:拦截Object.isExtensible()返回的结果
  • Object.preventExtensions()方法让一个对象变不能再添加新的属性
  • 代理preventExtensions方法
    • 作用:拦截Object.preventExtensions()返回的结果
let person = {}

let proxy = new Proxy(person, {
    isExtensible(trapTarget) {
        return Reflect.isExtensible(trapTarget);
    },
    preventExtensions(trapTarget) {
        return false;
    }
})


console.log(Object.isExtensible(person)); // true
console.log(Object.isExtensible(proxy)); // true

Object.preventExtensions(proxy); 
// 代理拦截了设置Object.preventExtensions的结果且返回false,
// 所以下面结果返回仍然是true

console.log(Object.isExtensible(person)); // true
console.log(Object.isExtensible(proxy)); // true

defineProperty方法

  • 代理definedProperty()方法
    • 参数
      • trapTarget 要定义属性的对象
      • key 属性的键
      • descriptor 属性的描述符对象
    • 作用:拦截Object.definedProperty()方法的调用
// 示例:给Object.defineProperty()添加限制
let target = {}

let proxy = new Proxy(target, {
    defineProperty(trapTarget, key, descriptor) {
        if (key === 'name') {
            return false;
        }

        return Reflect.defineProperty(trapTarget, key, descriptor);
    }
})

Object.defineProperty(proxy, "num", {
    value: '16'
})
console.log(proxy.num); // 16

// 会报错,因为代理陷阱设置了 name 属性设置false失败状态了
Object.defineProperty(proxy, 'name', {
    value: 'bob'
})
console.log(proxy.name);

上面的例子,使用代理限制了设置 name 属性设置失败状态,所以会设置不了其值

getOwnPropertyDescriptor方法

  • 代理getOwnPropertyDescriptor()方法
    • 参数
      • trapTarget 要定义属性的对象
      • key 属性的键
// getOwnPropertyDescriptor()
let target = {
    name: 'bob'
}

let proxy = new Proxy(target, {
    getOwnPropertyDescriptor(trapTarget, key) {
        return {
            name: 'lynn'
        }
    }
})

// 报错
let descriptor = Object.getOwnPropertyDescriptor(proxy, 'name');
console.log(descriptor.value);

上面的例子,getOwnPropertyDescriptor()陷阱的限制条件有所不同,它的返回值必须是null,undefined,或是一个对象,如果返回的是对象,那么对象的属性必须是enumerable、configurable,value,wriable,get和set,在返回的对象中使用不被允许的属性会抛出一个错误。

ownKeys陷阱

ownKeys 代理陷阱拦截了内部方法 [[OwnPropertyKeys]] ,并允许你返回一个数组用于重写该行为。返回的这个数组会被用于四个方法: Object.keys() 方法、Object.getOwnPropertyNames() 方法、Object.getOwnPropertySymbols()方法与Object.assign() 方法,其中 Object.assign() 方法会使用该数组来决定哪些属性会被复制。

ownKeys 陷阱函数接受单个参数,即目标对象,同时必须返回一个数组或者一个类数组对象。你可以使用 ownKeys 陷阱函数去过滤特定的属性,以避免这些属性被Object.keys() 方法、Object.getOwnPropertyNames() 方法、Object.getOwnPropertySymbols() 方法或 Object.assign() 方法使用。

apply和construct陷阱

有 apply 与 construct要求代理目标对象必须是一个函数。

  • apply陷阱函数

  • 参数

    • trapTarget :被执行的函数(即代理的目标对象)
    • thisArg :调用过程中函数内部的 this 值
    • argumentsList :被传递给函数的参数数组
  • construct陷阱函数

  • 参数

    • trapTarget :被执行的函数(即代理的目标对象)
    • argumentsList :被传递给函数的参数数组

当使用 new 调用函数时,会触发construct陷阱函数,反之触发apply陷阱函数

情景一:验证函数的参数必须是数值,且不允许使用new操作符调用

function sum(...values) {
    return values.reduce((previous, current) => previous + current, 0)
}

let proxy = new Proxy(sum, {
    apply(trapTarget, thisArg, argumentsList) {
        argumentsList.forEach((arg) => {
            if (typeof arg !== 'number') {
                throw new TypeError('所有参数必须是数字');
            }
        })
        return Reflect.apply(trapTarget, thisArg, argumentsList);
    },
    construct(trapTarget, argumentsList) {
        throw new TypeError("该函数不允许使用new关键字调用");
    }
})
console.log(proxy(1, 2, 3, 4, 5, 6)); // 21
console.log(proxy(1, 2, "2", 4, 5, 6)); // 报错:TypeError: 所有参数必须是数字
new proxy(1, 2, 3, 4); // TypeError: 该函数不允许使用new关键字调用

情景二:验证函数的参数必须是数值,且必须使用new操作符调用

function sum(...values) {
    return values.reduce((previous, current) => previous + current, 0)
}

let MyProxy = new Proxy(sum, {
    apply(trapTarget, thisArg, argumentsList) {
        throw new TypeError("该函数必须使用new关键字调用");
    },
    construct(trapTarget, argumentsList) {
        argumentsList.forEach((arg) => {
            if (typeof arg !== 'number') {
                throw new TypeError('所有参数必须是数字');
            }
        });

        return Reflect.construct(trapTarget, argumentsList);
    }
})
// console.log(MyProxy(1, 2, 3, 4, 5, 6)); // TypeError: 该函数必须使用new关键字调用
// console.log(new MyProxy(1, 2, "2", 4, 5, 6)); // 报错:TypeError: 所有参数必须是数字
console.log(new MyProxy(1, 2, 3, 4));

可撤销代理

  • 使用Proxy.revocable(proxy, revoke)
  • 参数
    • proxy 可被撤销的代理对象
    • revoke 撤销代理要调用的函数
let target = {
    name: 'bob'
}

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

console.log(proxy.name);

revoke(); // 移除代理

console.log(proxy.name); // 移除代理后会输出报错

将代理作为原型

将代理作为原型,始终要记住一个规律:当查找一个属性时,当当前对象查找不到时,会往它的原型对象上面找,这时候才会触发代理陷阱函数,可用的代理陷阱函数有get,set,和has。

1.在原型上面使用get陷阱,访问不存在的属性进行报错

// get
// 在原型上读取一个不存在的属性抛出错误
let target = {}
let thing = Object.create(new Proxy(target, {
    get(trapTarget, key, receiver) {
        throw new ReferenceError(`${key} doesn't exist`);
    }
}));

thing.name = 'bob';
console.log(thing.name); // bob

// 访问不存在的属性
console.log(thing.age); // ReferenceError: age doesn't exist

2.在原型上面使用set陷阱

let target = {}

let thing = Object.create(new Proxy(target, {
    set(trapTarget, key, value, receiver) {
        Reflect.set(trapTarget, key, value, receiver);
    }
}))

console.log(thing.hasOwnProperty("name")); // false

// 触发set代理陷阱
thing.name = 'bob';
console.log(thing.hasOwnProperty("name")); // true

// 不会触发set代理陷阱
thing.name = 'lynn';

3.在原型上面上使用has代理陷阱

let target = {}

let thing = Object.create(new Proxy(target, {
    has(trapTarget, key) {
        Reflect.set(trapTarget, key, value, receiver);
    }
}))


// 触发has代理陷阱
console.log('name' in thing);
thing.name = 'bob'

// 不会触发has代理陷阱
console.log('name' in thing);

将代理用作类的原型

function NoSuchPrototype() {
    // todo
}

let proxy = new Proxy({}, {
    get(trapTarget, key, receiver) {
        throw new ReferenceError(`${key} doesn't exist`);
    }
})

NoSuchPrototype.prototype = proxy;

class Square extends NoSuchPrototype {
    constructor(length, width) {
        super();
        this.length = length;
        this.width = width;
    }
}

let area = new Square(2, 4);
console.log(area.length * area.width); // 8

// 当访问不存在的属性时会报错,因为代理陷阱set 设置了false
console.log(area.name); // ReferenceError: name doesn't exist
点击查看更多内容
4人点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
Web前端工程师
手记
粉丝
4320
获赞与收藏
1744

关注作者,订阅最新文章

阅读免费教程

感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消