【重学前端】ECMAScript6 - Proxy

127 阅读4分钟

Proxy

用法

var proxy = new Proxy(target, handler);

targethandler都是对象,然后我们操作构造出来的proxy对象,操作的一些步骤便会被我们在handler中定义的方法所拦截。 在handler中可以设置的操作有以下13种:

  • get(target, propKey, receiver)
  • set(target, propKey, value, receiver)
  • has(target, propKey)
  • deleteProperty(target, propKey)
  • ownKeys(target)
  • getWonPropertyDescriptor(target, propKey)
  • defineProperty(target, propKey, propDesc)
  • preventExtensions(target)
  • getPrototypeOf()
  • isExtensible(target)
  • setPrototypeOf()
  • apply(target, object, args)
  • construct(target, args)

着重讲几种平时用到比较多的。

get()

get方法用于拦截某个属性的读取操作,可以接受三个参数,依次为目标对象、属性名和Proxy实例本身,第三个参数可选。关于第三个参数receiver的作用,可以看看这个,我目前理解是没事别乱用这个参数。

let person = {
    name: '小明'
}
let proxy = new Proxy(person, {
    get(target, key) {
        if (key === 'name') {
            console.log('劫持get');
            return '小红';
        }
    }
});
console.log(proxy.name);
// 控制台逐行输出'劫持get' '小红'

set()

set方法用来拦截某个属性的赋值操作,可以接受四个参数,依次为目标对象、属性名、属性值和Proxy实例本身,其中最后一个参数可选。

let person = {
    name: '小明'
}
let proxy = new Proxy(person, {
    set(target, key, value, receiver) {
        if (key === 'name') {
            target[key] = '小红';
        } else {
            target[key] = value;
        }
    }
});
proxy.name = '小刚';
proxy.hello = 'world';
console.log(proxy); // Proxy(Object) {name: '小红', hello: 'world'}

has()

判断对象是否具有某个属性时,这个方法会生效。返回布尔值来判断一个对象是否有某个属性。

const person = {
    name: '小明',
    age: 18,
    '@@job': '保密工作',
    [Symbol('hello')]: 'world',
}
let proxy = new Proxy(person, {
    has(targe, key) {
        if (key.startsWith('@@')) {
            return false;
        }
        return key in target;
    }
});

console.log('@@job' in proxy); // false
console.log(Reflect.has(proxy, '@@job')); // false
console.log(Object.keys(proxy)); // ['name', 'age', '@@job']
console.log(Object.getOwnPropertyNames(proxy)); // ['name', 'age', '@@job']
console.log(Reflect.ownKeys(proxy)); // ['name', 'age', '@@job', Symbol(hello)]

在前面学习Symbol的章节中,我们知道使用Symbol值作为键名时,一些遍历操作比如Object.getOwnPropertyNames是无法读取到键名的,可以起到一定的私有作用,在这里,我们使用has拦截,也可以做一些类似实现私有变量的操作。

apply()

apply方法拦截函数的调用、callapply操作。apply方法可以接受三个参数,分别是目标对象、目标对象的上下文对象(this)和目标对象的参数数组。
比如我们可以劫持对应的函数,在函数调用之前或之后,我们做一些其他操作,比如说统计调用次数,上报一些数据等。

function fn() {
    console.log('hello');
}
let callCount = 0;
let proxy = new Proxy(fn, {
    apply(target, ctx, args) {
        callCount++;
        return target(...args);
    }
});
proxy();
proxy();
console.log('被调用:' + callCount + '次'); // 被调用:2次

construct()

construct()方法用于拦截new命令,接收三个参数:目标对象(原来的构造函数)、构造函数的参数数组、创造实例对象时,new命令作用的构造函数(代理后的构造函数)。

function deer(){
    return {
        name: '鹿'
    }
}
let horse = new Proxy(deer, {
    construct(target, args, newTarget) {
        console.log(target, args, newTarget);
        console.log('其实我是:' + target().name);
        return {
            name: '马'
        }
    }
});

console.log(new horse().name);
// 分别输出以下内容:
/**
 ƒ deer(){
      return {
          name: '鹿'
      }
  } [] Proxy(Function) {length: 0, name: 'deer', arguments: null, caller: null, prototype: {…}}
 **/

// 其实我是:鹿
// 马

deleteProperty()

用来拦截删除属性的操作,接收两个参数,目标对象和准备删除的属性。

let person = {
    name: '小明',
    age: 18
}
let proxy = new Proxy(person, {
    deleteProperty(target, propKey) {
        if (propKey === 'name') {
            throw new Error('你不能删除name');
        } else {
            delete target[propKey];
        }
    }
});
delete proxy.age;
console.log(proxy); // Proxy(Object) {name: '小明'}
delete proxy.name; // caught Error: 你不能删除name

用处

沙箱隔离

比如在qiankun微前端中,可以进行全局环境的隔离,当我们在子应用中操作window全局对象时,实际操作的是经过代理的一个假的全局对象。

class ProxySandBox {
    proxyWindow;
    isRunning = false;
    active() {
        this.isRunning = true;
    }
    inactive() {
        this.isRunning = false;
    }
    constructor() {
        const fakeWindow = Object.create(null);
        this.proxyWindow = new Proxy(fakeWindow, {
            set: (target, prop, value, receiver) => {
                if (this.isRunning) {
                    target[prop] = value;
                }
            },
            get: (target, prop, receiver) => {
                return prop in target ? target[prop] : window[prop];
            },
        });
    }
}

const sandbox = new ProxySandBox();
sandbox.active();

sandbox.proxyWindow.hello = "world";

console.log("active:sandbox:window.hello: ", sandbox.proxyWindow.hello); // 'world'

console.log("window:window.hello: ", window.hello); // undefined
sandbox.inactive();

console.log("inactive:sandbox:window.hello: ", sandbox.proxyWindow.hello); // 'world'

console.log("inactive:browser:window.hello: ", window.hello); // undefined

双向绑定

Vue3的数据绑定相信大家再熟悉不过。

<!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" />
    <title>Document</title>
  </head>
  <body>
    <div id="app">
      <p id="paragraph"></p>
      <input type="text" id="input" />
    </div>
    <script>
      const paragraph = document.getElementById("paragraph");
      const input = document.getElementById("input");

      const data = {
        value: "hello world",
      };

      const handler = {
        set: function (target, prop, value) {
          if (prop === "value") {
            target[prop] = value;
            paragraph.innerHTML = value;
            input.value = value;
            return true;
          } else {
            return false;
          }
        },
      };
      const proxy = new Proxy(data, handler);
      proxy.value = data.value;

      input.addEventListener(
        "input",
        function (e) {
          proxy.value = e.target.value;
        },
        false
      );
      
    </script>
  </body>
</html>

不可变结构

使用Immutable.jsImmer.js可以帮我们创建出不可变结构的对象。在Immer.js中,便使用到了Proxy来实现。

class Store {
    constructor(state) {
        this.modified = false;
        this.source = state;
        this.copy = null;
    }
    get(key) {
        if (!this.modified) return this.source[key];
        return this.copy[key];
    }
    set(key, value) {
        if (!this.modified) this.modifing();
        return (this.copy[key] = value);
    }
    modifing() {
        if (this.modified) return;
        this.modified = true;
        this.copy = Array.isArray(this.source)
        ? this.source.slice()
        : { ...this.source };
    }
}

const PROXY_FLAG = Symbol();
const handler = {
    get(target, key) {
        if (key === PROXY_FLAG) return target;
        return target.get(key);
    },
    set(target, key, value) {
        return target.set(key, value);
    },
};

function produce(state, producer) {
    const store = new Store(state);
    const proxy = new Proxy(store, handler);

    producer(proxy);

    const newState = proxy[PROXY_FLAG];
    if (newState.modified) {
        return newState.copy;
    }
    return newState.source;
}

const state = { done: false };
const newState = produce(state, (draft) => { draft.done = true });
console.log(state.done); // false
console.log(newState.done); // true

参考