shuangmianxiaoQ / study-note

日常学习或工作笔记
6 stars 1 forks source link

Proxy & Reflect #64

Open shuangmianxiaoQ opened 3 years ago

shuangmianxiaoQ commented 3 years ago

Proxy

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

语法

const p = new Proxy(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;