Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proxy & Reflect #64

Open
shuangmianxiaoQ opened this issue Jul 13, 2021 · 0 comments
Open

Proxy & Reflect #64

shuangmianxiaoQ opened this issue Jul 13, 2021 · 0 comments
Labels
工作相关 记录工作中遇到的问题或收获

Comments

@shuangmianxiaoQ
Copy link
Owner

shuangmianxiaoQ commented Jul 13, 2021

Proxy

Proxy对象是用于包装另一个对象,并拦截其读/写等操作

语法

const p = new Proxy(target, handler);
  • target: 要包装的目标对象(任何类型的对象,包括函数)
  • handler: 带有钩子函数的对象,用于处理代理对象的各种拦截操作,下面列出一些常用的操作方法:
handler 方法 内部方法 触发时机
get [[Get]] 读取属性
set [[Set]] 写入属性
has [[HasProperty]] in 操作符
deleteProperty [[Delete]] delete 操作符
apply [[Call]] 函数调用
ownKeys [[OwnPropertyKeys]] Object.getOwnPropertyNames Object.getOwnPropertySymbols
getOwnPropertyDescriptor [[GetOwnProperty]] Object.getOwnPropertyDescriptor

get 钩子

const p = new Proxy(target, {
  get: function (target, property, receiver) {},
});
  1. 获取一个数组上的不存在项时,会返回undefined,可以利用代理,使得访问不存在的项返回0
let arr = [1, 2, 3];
arr = new Proxy(arr, {
  get(target, p) {
    if (p in target) {
      return target[p];
    }
    return 0;
  },
});
console.log(arr[1]);
console.log(arr[10]);

总结:借助get钩子可以在读取一个对象上不存在的属性时,得到一个默认值

set 钩子

const p = new Proxy(target, {
  set: function (target, property, value, receiver) {},
});

注意:如果写入成功则返回true,否则返回false(触发TypeError

  1. 创建一个只能写入数字的数组
let arr = [];
arr = new Proxy(arr, {
  set(target, p, value) {
    // 拦截写入操作
    if (typeof value == 'number') {
      target[p] = value;
      return true;
    }
    return false;
  },
});
arr.push(1);
arr.push('2');

总结:借助set钩子可以对一个对象的属性和值进行验证

has 钩子

const p = new Proxy(target, {
  has: function (target, prop) {},
});
  1. 检查数字是否在range范围内
let range = [1, 10];
range = new Proxy(range, {
  has(target, p) {
    return p >= target[0] && p <= target[1];
  },
});
console.log(5 in range);
console.log(100 in range);

ownKeys 钩子

const p = new Proxy(target, {
  ownKeys: function (target) {},
});
  1. 使用for...inObject.keys()遍历对象,并过滤_开头的属性
let obj = {
  name: 'jianwu',
  age: 24,
  _password: '123456',
};
obj = new Proxy(obj, {
  ownKeys(target) {
    return Object.keys(target).filter((key) => !key.startsWith('_'));
  },
});
for (let key in obj) console.log(key);
console.log(Object.keys(obj));
console.log(Object.values(obj));

注意:如果ownKeys钩子返回对象上不存在的属性,Object.getOwnPropertyNames方法可以列出不存在的键;但Object.keys不可以,因为Object.keys方法只返回带有enumerable标记的非Symbol键,可以使用getOwnPropertyDescriptor钩子将enumerable标记改为true,就可列出了。

deleteProperty 钩子

const p = new Proxy(target, {
  deleteProperty: function (target, property) {},
});
  1. 禁止删除对象中_开头的属性
let obj = {
  name: 'jianwu',
  age: 24,
  _password: '123456',
};
obj = new Proxy(obj, {
  deleteProperty(target, p) {
    if (p.startsWith('_')) {
      throw new Error('拒绝访问');
    }
    delete target[p];
    return true;
  },
});
delete obj._password;

apply 钩子

const p = new Proxy(target, {
  apply: function (target, thisArg, argumentsList) {},
});
  1. 实现一个delay(fn, ms)方法,在ms后执行fn函数
function delay(fn, ms) {
  // 返回一个调用 fn 函数的包装器
  return function () {
    setTimeout(() => fn.apply(this, arguments), ms);
  };
}
function sayHi(name) {
  console.log('Hello ' + name);
}
console.log(sayHi.length);
sayHi = delay(sayHi, 3000);
console.log(sayHi.length);
sayHi('jianwu');

下面使用Proxy来包装上面的函数:

function delay(fn, ms) {
  return new Proxy(fn, {
    apply(target, thisArg, args) {
      setTimeout(() => target.apply(thisArg, args), ms);
    },
  });
}
function sayHi(name) {
  console.log('Hello ' + name);
}
console.log(sayHi.length);
sayHi = delay(sayHi, 3000);
console.log(sayHi.length);
sayHi('jianwu');

总结:普通的包装函数不会转发读写等操作,所以无法访问到原始函数的属性,如length, name等;而Proxy则可以在代理对象上的所有操作转发到原始函数,从而实现一个更完整的包装器

Reflect

Reflect对象提供拦截JS操作的方法,与Proxy handler的方法相同,可以简化创建Proxy

内部方法仅在规范中使用,不能直接调用,Reflect的方法对内部方法进行包装,使得调用成为可能

Reflect 方法 等价操作 内部方法
Reflect.get(target, propertyKey, receiver) target[name] [[Get]]
Reflect.set(target, propertyKey, value, receiver) target[name] = value [[Set]]
Reflect.has(target, propertyKey) name in target [[HasProperty]]
Reflect.deleteProperty(target, propertyKey) delete target[name] [[Delete]]

getter 代理

  1. 来看一个栗子,Proxyget钩子返回代理对象原属性
let user = {
  _name: 'jianwu',
  get name() {
    return this._name;
  },
};
let proxy = new Proxy(user, {
  get(target, p, receiver) {
    return target[p];
  },
});
console.log(user.name);
// admin 继承 user 后,admin.name 是什么?
let admin = {
  _name: 'admin',
  __proto__: proxy,
};
console.log(admin.name);

上面的栗子中,触发get钩子会从原对象返回target[p],此处属性是一个getter访问器,this指向原对象,所以返回user.name

对于普通函数,可以使用call/apply来绑定正确的this,但是getter怎么绑定呢?

  1. 使用Reflect.get
let proxy = new Proxy(user, {
  get(target, p, receiver) {
    // receiver -> admin
    return Reflect.get(target, p, receiver);
  },
});

案例

负数索引访问数组

给定一个数组:arr = [1, 2, 3],实现:arr[-1] = 3arr[-2] = 2arr[-3] = 1

arr = new Proxy(arr, {
  get(target, p, receiver) {
    if (p < 0) {
      p = target.length + Number(p);
    }
    return Reflect.get(...arguments);
  },
});

实现简单的 Observable

创建一个makeObservable(target)函数,使得对象可观察

function makeObservable(target) {
  let handlers = [];
  target.observe = function (handler) {
    handlers.push(handler);
  };
  return new Proxy(target, {
    set(target, p, value, receiver) {
      const res = Reflect.set(...arguments);
      if (res) {
        handlers.forEach((handler) => handler(p, value));
      }
      return res;
    },
  });
}

let user = {};
user = makeObservable(user);
user.observe((key, value) => {
  console.log(key + ' = ' + value);
});
user.name = 'wjw';
user.age = 24;
@shuangmianxiaoQ shuangmianxiaoQ added the 工作相关 记录工作中遇到的问题或收获 label May 14, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
工作相关 记录工作中遇到的问题或收获
Projects
None yet
Development

No branches or pull requests

1 participant