欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

JavaScript必备知识点之Object.defineProperty与es6Proxy代理二三事(二)

程序员文章站 2022-07-12 21:55:18
...

上次我们了解了 Object.defineProperty。defineProperty 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。属性描述符里面有 get,和 set 方法提供 geeter 和 setter。

认识 proxy

Proxy 对象用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等)。let p = new Proxy(target, handler);参数 target:用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。参数 handler:属性是当执行一个操作时定义代理的行为函数的对象。
如果 handler 参数没有设置任何拦截,就等同于直接通向原对象:

let target = {};
let proxy = new Proxy(target, {});
proxy.name = "a";
target.name = "a";

handler对象用于定义拦截行为。有以下一些拦截操作:

get(target,propKey,value) 方法用于拦截(读取属性)对目标对象属性的访问请求,比如proxy.name proxy['name']

let proxy = new Proxy(
  {},
  {
    get(target, property) {
      return 1;
    }
  }
);
proxy.name; // 1
proxy.age; // 1

无论对 proxy 添加 name 还是 age 属性,返回都是 1。

是对 Proxy 的实例进行操作,而不是{}

let obj = {
  name: "cuihua"
};

let proxy = new Proxy(obj, {
  get(target, property) {
    if (property in target) {
      return target[property];
    } else {
      throw new ReferenceError(`${property} does not exist`);
    }
  }
});
proxy.name; // cuihua
proxy.age; //age does not exist

访问普通对象上不存在的属性会报错undefined,使用 proxy 我们可以自定义错误信息,如上述自定义没有属性则抛出错误。

我们可以利用 proxy 实现链式操作:

let pipe = (() => {
  // 立即执行
  return value => {
    let funcStack = [];
    let oproxy = new Proxy(
      {},
      {
        get(target, property) {
          // 拦截读取事件,pipe(3).double,那么可以拦截到property==='double' --> reverseInt.get可以拦截到 property==='get'
          if (property === "get") {
            //  当我们调用到get方法时
            return funcStack.reduce((val, fn) => {
              // 累加执行函数
              return fn(val);
            }, value);
          }

          funcStack.push(window[property]); // 向funcStack数组添加get拦截到的方法
          return oproxy;
        }
      }
    );
    return oproxy;
  };
})();

let double = n => n * 2; //double函数 3*2
let pow = n => n * n; // pow 6*6
let reverseInt = (
  n // 36转化为字符串并分割成'3''6',然后反转成 '6''3',
) =>
  n
    .toString()
    .split("")
    .reverse()
    .join("") | 0;

pipe(3).double.pow.reverseInt.get; // 63

利用 get 拦截生成各种 DOM 节点的通用函数:

const dom = new Proxy(
  {},
  {
    get(target, property) {
      // 拦截读取,如:'dom.div' 'dom.a' 'dom.li'
      // 返回一个函数,第一个参数存放标签的属性,第二个参数表示子节点
      return (attrs = {}, ...children) => {
        const el = document.createElement(property); //创建以拦截到的property('div'/'ul'/'li')值为名称的标签
        for (let prop of Object.keys(attrs)) {
          el.setAttribute(prop, attrs[prop]); // 循环拿到attrs(标签属性名),设置属性和属性值
        }

        for (let child of children) {
          if (typeof child === "string") {
            // 循环children(子节点),如果子节点是字符串则以文本的形式写入标签
            child = document.createTextNode(child);
          }
          el.appendChild(child); // 添加子节点
        }
        return el;
      };
    }
  }
);

const el = dom.div(
  {},
  "My name is ",
  dom.a({ href: "//example.com" }, "Mark"), // 创建a标签,href属性值为'//example.com',文本为'Mark'
  ". I like:",
  dom.ul({}, dom.li({}, "The web"), dom.li({}, "Food"), dom.li({}, "so on...")) // 创建ul标签及子标签li
);

document.body.appendChild(el);

set(target,propKey,value) 方法用于拦截某个属性(写入)赋值操作。比如proxy.name = 'zhangsan' proxy.age = 21

let proxy = new Proxy(
  {},
  {
    set(target, propKey, value) {
      if (propKey === "age") {
        if (!Number.isInteger(value)) {
          // 如果不是整数抛出错误
          throw new TypeError("The age is not an integer");
        }
        if (value > 200) {
          // 如果大于200抛出错误
          throw new RangeError("The age seems invalid");
        }
      }
      // 存储值
      target[propKey] = value;

      // 成功
      return true;
    }
  }
);
proxy.name = "zhangsan";
proxy.age = 21;
// proxy.age = 201;   // The age seems invalid

console.log(proxy.age); // 21

apply(target, that, args) 方法用于拦截函数的调用、call、apply 的操作。参数分别为:目标对象、目标对象的上下文(this)、目标对象的参数数组。比如let p = new Proxy(target, handler);p();

let p = new Proxy(
  () => {
    return "target";
  },
  {
    apply() {
      return "proxy";
    }
  }
);

console.log(p()); // proxy
console.log(p.call()); // proxy
console.log(p.apply()); // proxy

has(target, key) 方法用于拦截 HasProperty 操作,判断对象是否具有某个属性时生效。比如'_prop' in proxy

let proxy = new Proxy(
  {
    _prop: "foo",
    prop: "foo"
  },
  {
    has(target, key) {
      if (key[0] === "_") {
        // 如果是有_下划线则返回false,否则走正常判断逻辑
        return false;
      }
      return key in target;
    }
  }
);

"_proxy" in proxy;

construct(target,args,newTarget) 方法用于拦截new操作符。参数分别为目标对象、constructor 参数列表、最初被调用的构造函数,必须返回一个对象。比如new proxy(...args) / Reflect.construct()

let p = new Proxy(function() {}, {
  construct(target, argumentsList, newTarget) {
    console.log(`called: ${argumentsList.join(", ")}`);
    return { value: argumentsList[2] * 10 }; // 必须返回一个对象
    // return argumentsList[2] * 10   //
  }
});

console.log(new p(1, 2, 3, 4, 5).value); // called: 1, 2, 3, 4, 5
// 30

Proxy 初始化时,传给它的 target 必须具有一个有效的 constructor 供 new 操作符调用:

var p = new Proxy(
  {},
  {
    construct: function(target, argumentsList, newTarget) {
      return {};
    }
  }
);

new p(); // TypeError is thrown  因为Proxy的第一个参数‘target’必须是能够用new操作符实例化的构造函数

上例 Proxy 的第一个参数‘target’值是{},如果把它改为()=>{}箭头函数也是不行的,因为箭头函数不可以当做构造函数。

defineProperty(target,key,desc) 可以拦截Object.defineProperty操作,如属性描述符,定义一个新的 property。

let p = new Proxy(function() {}, {
  defineProperty(target, key, desc) {
    console.log(desc);
    if (desc && "value" in desc) {
      // 拦截属性描述符,如果描述符是"value"
      console.log(".....");
    }
    return target;
  }
});

p.my_cookie1 = "First haha";
总结

Object.defineProperty:

  • 1.会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。
  • 2.通过赋值操作添加的普通属性是可枚举的。
  • 3.使用 Object.defineProperty() 添加的属性值是不可修改。

Proxy:
Proxy 的功能更加强大,包含了 defineProperty,可以说 Object.defineProperty 是 Proxy 的子集。

  • 1.用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等)。
  • 2.支持数组、不需要对对象的每个属性的 key 遍历。