《JavaScript高级程序设计》- 第九章:代理与反射

博客

zyzcio.gitee.io

第九章:代理与反射

ES6中新增的代理与反射:提供了拦截向基本操作嵌入额外行为的能力;具体的实现方式就是:通过一个给目标对象定义一个关联的代理对象,通过代理对象内部的操作对目标对象的操作加以控制

注意:代理和反射只在百分百支持它们的平台上有用

9.1 代理基础

从很多方面看,代理类似C++的指针,可以用作对象的替身;当然,目标对象既可以直接操作(会绕过代理)、也可以通过代理来操作。

9.1.1 代理创建

通过Proxy构造函数创建,其接收两个参数:

  • 目标对象
  • 处理程序对象

两个参数是必需的;缺一个就会报错。

// 创建目标对象
const targetObject = {
	name:'target'
}
// 创建空处理程序对象
const handler = { /* 对数据进行一些操作(捕获器) */};

const proxy = new Proxy(targetObject,handler);

// 给目标对象赋值 会反映在两个对象上
targetObject.name = 'hello';
console.log(targetObject.name);		//hello
console.log(proxy.name);			//hello

// 給代理对象赋值 也会反映在两个对象上
proxy.name = 'world';
console.log(targetObject.name);		//world
console.log(proxy.name);			//world

9.1.2 定义捕获器

使用代理的主要目的定义捕获器

  • 捕获器:处理程序对象中定义的“基本操作的拦截器”;
  • 每个程序可以包括0个或多个
  • 每个捕获器对应一种基本操作,可以直接间接在代理对象上调用;

每次在代理对象上调用这些基本操作的时候,代理可以在这些操作传播到目标对象之前先调用捕获器函数,从而拦截并修改相应的行为。

const targetObject = {
    name:'target'
}

const handler = {
    get() {
        return "hello world!"
    }
};

const proxy = new Proxy(targetObject,handler);

console.log(targetObject.name);     // target
console.log(proxy.name);            // hello world!

显然,直接操作对象,会绕过代理器

9.1.3 捕获器参数 结合 反射API

所有捕获器都可以访问相应的参数,可以基于这些参数重建被捕获方法的原始行为。一般参数如下:

  • trapTarget:被捕获的目标对象
  • property:属性
  • value:值
  • receiver:代理程序对象
const targetObject = {
    name:'target'
}

const handler = {
    get(trapTarget,property,receiver) {
        console.log(trapTarget);        // { name:'target' }
        console.log(property);          // name
        console.log(receiver);          // { name:'target' }
        return targetObject[name];	// 如果不返回,则标识返回undefined
    }
};

const proxy = new Proxy(targetObject,handler);

console.log(targetObject.name);     // target
console.log(proxy.name);            // target

实际上,并不需要开发者手动重建原始行为,而可以通过全局Reflect对象的同名方法来进行重建。

const targetObject = {
    name:'target'
}

const handler = {
    get(trapTarget,property,receiver) {
		return Reflect.get(...arguments);
    }
};

const proxy = new Proxy(targetObject,handler);

console.log(targetObject.name);     // target
console.log(proxy.name);            // target

显然,上面只是创建了所有属性的捕获方法,一般实际情况会如下:

const targetObject = {
    id:1,
    name:'target',
    addr:'天堂'
}

const handler = {
    get(trapTarget,property,receiver) {
		let decoration = '';
        if( trapTarget[property] === '天堂' ){
            decoration = "上帝的邻居:";
        }
        return decoration + Reflect.get(...arguments);
    }
};

const proxy = new Proxy(targetObject,handler);

console.log(proxy.name);        // target
console.log(proxy.addr);        // 上帝的邻居:天堂

9.1.4 捕获器不变式

个人理解:捕获器也需要遵循一定的规则

当属性不可配置不可写入的时候,不可进行操作

const target = {}
Object.defineProperty(target, 'sex' , {
 configurable: false,
 writable:false,
 value:'male'
})

const handler = {
 get(){
     return 'female'
 }
}

const proxy = new Proxy(target,handler);

console.log(proxy.sex);     // 报错TypeError

9.1.5 撤销代理

除了使用Proxy的构造函数创建代理之外,Proxy还暴露了revocable()revoke()分别用于关联和撤销代理。

const target = {
    id:1,
    name:'zyzc',
    title:'hello'
}

const handler = {
    get(trapObject,property,receiver) {
        return Reflect.get(...arguments);
    }
}

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

console.log(proxy.name);		// zyzc
revoke();
console.log(proxy.name)			// Cannot perform 'get' on a proxy that has been revoked

9.1.6 反射的特点、优点

在某些情况下,应该优先使用反射API:

  1. 反射API 与 对象API对比

    • 大多数反射API在Object类型上有对应的方法:通常,Object上的方法适用于通用程序,反射方法适用于细粒度的对象控制与操作
  2. 状态标记

    很多反射方法会返回称作状态标记的布尔值,表示意图执行的操作是否成功。这些方法有:

    • Reflect.defineProperty()
    • Reflect.preventExtensions()
    • Reflect.setPrototypeOf()
    • Reflect.set()
    • Reflect.deleteProperty()

    使用反射API,可以重构那些:返回修改后对象、抛出错误的方法。

    初始代码:

    const object = {};
    try {
        Object.defineProperty(object,'name','zyzc');
        console.log('success');
    }catch(e){
        console.log('failure');
    }
    

    反射API重构之后:

    const object = {};
    if(Reflect.defineProperty(object,'name','zyzc')){
        console.log('success');
    }else{
        console.log('failure');
    }
    
  3. 一等函数代替操作符

    • Reflect.get():代替属性访问操作符
    • Reflect.set():代替赋值操作符
    • Reflect.has():代替in或with()
    • Reflect.deleteProperty():代替delete
    • Reflect.construct():代替new
  4. 安全地应用函数

    在几乎不可能的情况下,当方法名apply和属性名apply一致的时候,可以使用Reflect.apply()来代替。

9.1.7 代理另一个代理

可以使用一个代理,代理另一个代理,构建多层拦截网

9.1.8 代理的不足

很大程度上,代理作为对象的虚拟层可以正常使用。但是有时候并不完美;

  1. 代理的this

    const target = {
        show(){
            return this === proxy
        }
    }
    
    const proxy = new Proxy(target,{})
    
    console.log(target.show());	// false
    console.log(proxy.show());	// true
    

    这样没什么问题,如果遇到了依赖的实例对象标识,就会出现如下问题:

    const wm = new WeakMap();
    class User {
        constructor(userId){
            wm.set(this,userId);
        }
        
        set id(userId){
            wm.set(this,userId);
        }
        
        get id(){
            return wm.get(this);
        }
    }
    
    const user = new User(123);
    const proxy = new Proxy(user,{});
    console.log(user.id);	// 123;
    console.log(proxy.id);	// undefined
    

    需要这样操作!【个人感觉没什么用…】

    const wm = new WeakMap();
    class User {
        constructor(userId){
            wm.set(this,userId);
        }
        
        set id(userId){
            wm.set(this,userId);
        }
        
        get id(){
            return wm.get(this);
        }
    }
    
    const userClassProxy = new Proxy(User,{});
    const proxy = new userClassProxy(123);
    
    console.log(proxy.id);	// 123
    
  2. 代理与内部槽位

    代理与内置引用类型的实例,一般可以协同,但是有一些内置类型可能会依赖代理无法控制的机制

    比如说DateDate类型方法的执行,依赖this值上的内部槽位[[NumberDate]],但是代理对象并不存在这个槽位,而且内部槽位的值不能通过普通的get()set()操作。所以当代理拦截后,本应转发给目标对象的方法,会抛出TypeError

9.2 代理捕获器与反射方法

介绍前几种,其他的类似就不一一叙述

9.2.1 get()

get()捕获器在获取属性值的操作中被调用。

  • 返回值
    • 没有返回值限制
  • 拦截的操作
    • proxy.property
    • proxy[property]
    • Object.create(proxy)[property]
    • Reflect.get(proxy, property, receiver)
  • 捕获器处理程序参数
    • target:目标对象
    • property:引用的目标对象上的字符串键属性。
    • receiver:代理对象或继承代理对象的对象
  • 捕获器不变式
    • 如果target.property不可写且不可配置, 则处理程序返回的值必须与target.property匹配。
    • 如果target.property不可配置且[[Get]]特性为undefined, 处理程序的返回值也必须是undefined

9.2.2 set()

set()捕获器设置属性值的操作中被调用。

  • 返回值
    • true
    • false【严格模式下抛出错误】
  • 拦截的操作
    • proxy.property = value
    • proxy[property] = value
    • Object.create(proxy)[property] = value
    • Reflect.set(proxy, property, value, receiver)
  • 捕获器处理程序参数
    • target: 目标对象。
    • property: 引用的目标对象上的字符串键属性。
    • value: 要赋给属性的值。
    • receiver: 接收最初赋值的对象。
  • 捕获器不定式
    • 如果target.property不可写且不可配置, 则不能修改目标属性的值。
    • 如果target.property不可配置且[[Set]]特性为undefined, 则不能修改目标属性的值。
    • 在严格模式下, 处理程序中返回false会抛出TypeError

9.2.3 has()

has()捕获器会在in操作符中被调用。

  • 返回值【表示是否存在】
    • true
    • false
  • 拦截的操作
    • property in proxy
    • property in Object.create(proxy)
    • with(proxy) {(property);}
    • Reflect.has(proxy, property)
  • 捕获器处理程序参数
    • target: 目标对象。
    • property: 引用的目标对象上的字符串键属性。
  • 捕获器不变式
    • 如果target.property存在且不可配置, 则处理程序必须返回true
    • 如果target.property存在且目标对象不可扩展, 则处理程序必须返回true

9.2.4 defineProperty()

defineProperty()捕获器会在Object.defineProperty()中被调用。

  • 返回值【表示是否定义成功】
    • true
    • false
  • 拦截的操作
    • Object.defineProperty(proxy, property, descriptor)
    • Reflect.defineProperty(proxy, property, descriptor)
  • 捕获器处理程序参数
    • target: 目标对象。
    • property: 引用的目标对象上的字符串键属性。
    • descriptor: 包含可选的enumerable、 configurable、 writable、 value、 get和set定义的对象
  • 捕获器不变式
    • 如果目标对象不可扩展, 则无法定义属性
    • 如果目标对象有一个可配置的属性, 则不能添加同名的不可配置属性
    • 如果目标对象有一个不可配置的属性, 则不能添加同名的可配置属性

9.2.5 getOwnPropertyDescriptor()

9.2.6 deleteProperty()

9.2.7 ownKeys()

9.2.8 getPrototypeOf()

9.2.9 setPrototypeOf()

9.2.10 isExtensible()

9.2.11 preventExtensions()

9.2.12 apply()

在函数被调用的时候捕获

  • 返回值
    • 返回值无限制
  • 拦截的操作
    • proxy(...argumentsList)
    • Function.prototype.apply(thisArg, argumentsList)
    • Function.prototype.call(thisArg, ...argumentsList)
    • Reflect.apply(target, thisArgument, argumentsList)
  • 捕获器处理程序参数
    • target: 目标对象。
    • thisArg: 调用函数时的this参数。
    • argumentsList: 调用函数时的参数列表
  • 捕获器不变式
    • target必需是一个对象

9.2.13 construct()

9.3 代理模式

可以利用代理,实现一些编程模式

9.3.1 跟踪属性访问

可以通过get、set和has操作,知道属性对象什么时候被访问、被查询;

const user = {
    name:'zyzc'
}

const proxy = new Proxy(user, {
    get(target,property,receiver){
        console.log(`${target} is Getting ${property}`)
        return Reflect.get(...arguments);
    },
    set(target,property,value,receiver){
        console.log(`${target} is Setting ${property} to ${value}`);
        return Reflect.set(...arguments);  
    }
})

proxy.name;         // [object Object] is Getting name
proxy.age = 21;     // [object Object] is Setting age to 21 

9.3.2 隐藏属性

使得代码对外部不可见

const privateProperty = ['age','sex','addr']
const user = {
    name:'zyzc',
    age:21,
}


// 将age设为不可见
const proxy = new Proxy(user,{
    get(target,property,receiver) {
        if(privateProperty.includes(property)){
            return undefined;
        }else {
            return Reflect.get(...arguments);
        }
    },
    has(target,property,receiver) {
        if(privateProperty.includes(property)){
            return false;
        }else{
            return Reflect.has(...arguments);
        }
    }
})

console.log(proxy.name);    // zyzc
console.log(proxy.age);     // undefined
console.log('age' in proxy);// false

9.3.3 属性验证

使得设置属性的时候,判断是否允许赋值

const target = {
    name:'zyzc',
    age:21
}

const proxy = new Proxy(target,{
    set(target,property,value,receiver){
        if(property === 'age') {
            if(typeof value === 'number' ){
                return Reflect.set(...arguments);
            }else{
                return false;
            }
        }
    }
})

proxy.age = 'haha';
console.log(proxy.age); //  21
proxy.age = 22;
console.log(proxy.age); //  22

9.3.4 函数与构造函数参数验证

function getMedian(...nums){
    return nums.sort()[Math.floor(nums.length / 2)];
}

const proxy = new Proxy(getMedian, {
    apply(target,thisArg,argumentsList) {
        for(const arg of argumentsList) {
            if(typeof arg !== 'number'){
                throw 'You should pass a Number';
            }
        }
        return Reflect.apply(...arguments);
    }
})
console.log(proxy(9,8,7,1,2,3));        // 7
console.log(proxy("haha",1,2,3,4));     // You should pass a Number

9.3.5 数据绑定与观察

可以将被代理的类,绑定到一个全局实例集合中

const userList = {};
class User {
    constructor(name){
        this._name = name;
    }
}

const proxy = new Proxy(User , {
    construct(){
        const newUser = Reflect.construct(...arguments);
        userList.push(newUser);
        return newUser;
    }
});

new User('zyzc1');
new User('zyzc2');

console.log(userList);	// [ User { _name: 'zyzc1' }, User { _name: 'zyzc2' } ]

还可以给集合绑定一个监听事件

const userList = [];    // 具有内部属性 index、length,所以每次set的时候,都会遍历自定义属性和内部属性
function emit(newValue) {
    console.log("你插入了"+newValue)
}

const proxy = new Proxy(userList , {
    set(target,property,value,receiver){
        const result = Reflect.set(...arguments);
        if(result) {
            emit(Reflect.get(target,property,receiver));
        }
        return result;
    }
});
proxy.push('zyzc1');	// 你插入了:1 你插入了:zyzc1
proxy.push('zyzc2');	// 你插入了:2 你插入了:zyzc2
console.log(proxy)		// [ 'zyzc1', 'zyzc2' ]

为什么会多了一个1和2呢?我认为应该是在if里面,获取目标对象的属性时,会遍历其内部属性,如index或者length之类的,导致多出了1和2

const userList = [];    
function emit(newValue) {
    console.log("你插入了:"+newValue)
}

const proxy = new Proxy(userList , {
    set(target,property,value,receiver){
        const result = Reflect.set(...arguments);
        if(result) {
            let temp = Reflect.get(target,property,receiver);
            console.log("获取的目标对象是:"+target);       // ['zyzc1'] ['zyzc1'] ['zyzc1','zyzc2'] ['zyzc1','zyzc2']
            console.log("获取的目标属性是:"+property);     // 0         length    1		            length
            //emit();   
        }
        return result;
    },
});

proxy.push('zyzc1');	// 你插入了:1 你插入了:zyzc1
proxy.push('zyzc2');	// 你插入了:2 你插入了:zyzc2
console.log(proxy);

目前还没有想出有什么好的解释;

9.4 小结

  • 代理,开辟出JS的元编程抽象的新区域
  • 反射API,封装了一整套与捕获器拦截的操作相对应的方法,可以说是所有对象的API基础
  • 代理与反射API,可以编写出类似于验证器、属性拦截器等模式。
  • 听说vue3就是使用了代理来写dom的虚拟层的【后面学vue3原理的时候,再盘点一下】

你可能感兴趣的:(JavaScript,javascript,代理模式,前端)