带你重学ES6 | proxy和defineProperty

前言

虽然个人主技术栈是 React 的,可是每当面试的时候,面试官几乎都会问你说一下 React 和 Vue 的区别,在说道双向数据绑定的时候,面试官会下意识的问一句,你说一下 Vue 的双向数据绑定的原理,这个时候 Object.defineProperty 就出场了,可是在 Vue3.0 中,Proxy 取代了 Object.defineProperty,成为双向绑定的底层原理,这个时候 Proxy 就显得尤其重要。前端

本篇文章先以 Object.defineProperty 做为引入,以后讲解 Proxy,最后比较二者之间的优劣。git

一、Object.defineProperty 数据劫持

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。es6

该方法接受三个参数,第一个参数是 obj:要定义属性的对象,第二个参数是 prop:要定义或修改的属性的名称或 Symbol,第三个参数是 descriptor:要定义或修改的属性描述符。github

const obj = {};
Object.defineProperty(obj, "property", {
  value18,
});
console.log(obj.property); // 18
复制代码

虽然咱们能够直接添加属性和值,可是使用这种方式,咱们能进行更多的配置。面试

函数的第三个参数 descriptor 所表示的属性描述符有两种形式:数据描述符和存取描述符。数据描述符是一个具备值的属性,该值能够是可写的,也能够是不可写的。存取描述符是由 getter 函数和 setter 函数所描述的属性。一个描述符只能是这二者其中之一;不能同时是二者。编程

这两种同时拥有下列两种键值:数组

  1. configurable:当且仅当该属性的 configurable 键值为 true 时,该属性的描述符才可以被改变,同时该属性也能从对应的对象上被删除。默认为 false。
  2. enumerable:当且仅当该属性的 enumerable 键值为 true 时,该属性才会出如今对象的枚举属性中。默认为 false。
const obj = { property24 };
Object.defineProperty(obj, "property", {
  configurabletrue,
});
delete obj["property"]; // true
obj; // {}
// 改变状态
const obj = { property24 };
Object.defineProperty(obj, "property", {
  configurablefalse,
});
delete obj["property"]; // false
obj; // {'property': 24}
复制代码
const obj = {
  property124,
  property234,
  property354,
};
Object.defineProperty(obj, "property1", {
  enumerabletrue,
});
for (i in obj) {
  console.log(i);
}
// property1
// property2
// property3
// 改状态
Object.defineProperty(obj, "property1", {
  enumerablefalse,
});
for (i in obj) {
  console.log(i);
}
// property2
// property3
复制代码

数据描述符还具备如下可选键值:markdown

  1. value:该属性对应的值。能够是任何有效的 JavaScript 值(数值,对象,函数等)。默认为 undefined。
  2. writable:当且仅当该属性的 writable 键值为 true 时,属性的值,也就是上面的 value,才能被赋值运算符改变。默认为 false。
const obj = {};
Object.defineProperty(obj, "property1", {
  value18,
});
obj; // {'property1': 18}
复制代码
const obj = {};
Object.defineProperty(obj, "property1", {
  value18,
  writablefalse,
});
obj.property1 = 24;
obj; // {'property1': 18}
// 改变状态
const obj = {};
Object.defineProperty(obj, "property1", {
  value18,
  writabletrue,
});
obj.property1 = 24;
obj; // {'property1': 24}
复制代码

存取描述符还具备如下可选键值:app

  1. get:属性的 getter 函数,若是没有 getter,则为 undefined。当访问该属性时,会调用此函数。执行时不传入任何参数,可是会传入 this 对象(因为继承关系,这里的 this 并不必定是定义该属性的对象)。该函数的返回值会被用做属性的值。默认为 undefined。
  2. set:属性的 setter 函数,若是没有 setter,则为 undefined。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象。默认为 undefined。
const obj = {};
Object.defineProperty(obj, "property1", {
  get(value) {
    return value;
  },
  set(newValue) {
    value = newValue;
  },
});
复制代码

二、Proxy 数据拦截

Object.defineProperty 只能重定义获取和设置的行为,而 Proxy 至关于一个升级,它重定义了更多的行为,接下来咱们对其进行深刻讲解。函数

首先,Proxy 是一个构造函数,能够经过 new 来建立它的实例,其接受两个参数,一个是 target:要使用 Proxy 包装的目标对象(能够是任何类型的对象,包括原生数组,函数,甚至另外一个代理)。另一个是 handler:一个一般以函数做为属性的对象,各属性中的函数分别定义了在执行各类操做时代理实例的行为。

let p = new Proxy(target, handler);
复制代码

handler 对象的方法

handler 中的全部方法都是可选的,若是没有定义哪一个方法,那就保留原对象的默认行为。

一、get()

用于拦截对象的读取操做。该方法接收三个参数,target:目标对象,property:被获取的属性名,receiver:Proxy 或者继承 Proxy 的对象。

let person = {
  name"Jack",
};
let p = new Proxy(person, {
  get(target, property) {
    if (property in target) {
      return target[property];
    } else {
      throw Error("不存在该属性");
    }
  },
});
p.name; // Jack
p.age; // Uncaught Error: 不存在该属性
复制代码

查看第三个属性

let person = {
  name"Jack",
};
let p = new Proxy(person, {
  get(target, property, receiver) {
    return receiver;
  },
});
p.name === p; // true
复制代码

上面代码中,p 对象的 name 属性是由 p 对象提供的,因此 receiver 指向 proxy 对象。

二、set()

用于设置属性值操做的捕获器。该方法接收四个参数,target:目标对象,property:将被设置的属性名或 Symbol,value:新属性值,receiver:最初被调用的对象。一般是 proxy 自己,但 handler 的 set 方法也有可能在原型链上,或以其余方式被间接地调用(所以不必定是 proxy 自己)。

let person = {};
let p = new Proxy(person, {
  set(target, property, value, receiver) {
    target[property] = value;
  },
});
p.name = 2;
复制代码

当给 Proxy 的实例添加属性的时候,就会调用 set()方法。

下面咱们来看一下 set()方法的第四个参数,通常状况下都是指向 proxy 自己。

let person = {};
let p = new Proxy(person, {
  set(target, property, value, receiver) {
    target[property] = receiver;
  },
});
p.name = 2;
p.name === p; // true
复制代码

可是也有其余状况,咱们借用阮一峰:ECMAScript 6 入门-Proxy中谈到的例子:

const handler = {
  setfunction (obj, prop, value, receiver{
    obj[prop] = receiver;
  },
};
const proxy = new Proxy({}, handler);
const myObj = {};
Object.setPrototypeOf(myObj, proxy);

myObj.foo = "bar";
myObj.foo === myObj; // true
复制代码

上面代码中,设置 myObj.foo 属性的值时,myObj 并无 foo 属性,所以引擎会到 myObj 的原型链去找 foo 属性。myObj 的原型对象 proxy 是一个 Proxy 实例,设置它的 foo 属性会触发 set 方法。这时,第四个参数 receiver 就指向原始赋值行为所在的对象 myObj。

值得注意一点的是,当该对象不可配置不可编写的时候,那么 set()方法将不起做用。

const obj = {};
Object.defineProperty(obj, "foo", {
  value18,
  writablefalse,
});
let p = new Proxy(obj, {
  set(target, property, value, receiver) {
    target[property] = value;
  },
});
p.foo = 28;
p.foo; // 18
复制代码

三、apply()

用于拦截函数的调用。该方法接受三个参数,target:目标对象(函数)。thisArg:被调用时的上下文对象。argumentsList:被调用时的参数数组。

当 Prxoy 的实例当作函数使用时,就会执行该方法。

let p = new Proxy(function ({}, {
  apply(target, thisArg, argumentsList) {
    console.log("Hello Word");
  },
});
p(); // Hello Word
复制代码

四、has()

是针对 in 操做符的代理方法。该方法接受两个参数,target:目标对象。prop:须要检查是否存在的属性。

let p = new Proxy(
  {},
  {
    has(target, prop) {
      console.log(target, prop); // {} 'a'
    },
  }
);
console.log("a" in p); // false
复制代码

has()方法只对 in 运算符有效,对 for...in...运算没有实际做用。

let p = new Proxy(
  { value18 },
  {
    has(target, prop) {
      if (prop === "value" && target[prop] < 20) {
        console.log("数值小于20");
        return false;
      }
      return prop in target;
    },
  }
);
"value" in p; // 数值小于20 false
for (let a in p) {
  console.log(p[a]); // 18
}
复制代码

从上述例子能够看出,has 方法拦截只对 in 运算符有效,对 for...in...来讲没有拦截效果。

五、construct

用于拦截 new 操做符. 为了使 new 操做符在生成的 Proxy 对象上生效,用于初始化代理的目标对象自身必须具备[[Construct]]内部方法(即 new target 必须是有效的)。该方法接收三个参数,target:目标对象。argumentsList:constructor 的参数列表。newTarget:最初被调用的构造函数。

let p = new Proxy(function ({}, {
  construct(target, argumentsList, newTarget) {
    return { value: argumentsList[0] };
  },
})(new p(1)).value; // 1
复制代码

construct()方法必须返回一个对象,否则不报错。

let p = new Proxy(function ({}, {
  construct(target, argumentsList, newTarget) {
    console.log("Hello Word");
  },
});
new p(); // Uncaught TypeError: 'construct' on proxy: trap returned non-object ('undefined')
复制代码

六、deleteProperty

用于拦截对对象属性的 delete 操做。该方法接受两个参数。target:目标对象。property:待删除的属性名。

let p = new Proxy(
  {},
  {
    deleteProperty(target, property) {
      console.log("called: " + property);
      return true;
    },
  }
);
delete p.a; // "called: a"
复制代码

若是这个方法抛出错误或者返回 false,当前属性就没法被 delete 命令删除。注意,目标对象自身的不可配置(configurable)的属性,不能被 deleteProperty 方法删除,不然报错。

七、defineProperty()

用于拦截对对象的 Object.defineProperty() 操做。该方法接受三个参数,target:目标对象。property:待检索其描述的属性名。descriptor:待定义或修改的属性的描述符。

let p = new Proxy(
  {},
  {
    definePropertyfunction (target, prop, descriptor{
      console.log("called: " + prop);
      return true;
    },
  }
);

let desc = { configurabletrueenumerabletruevalue10 };
Object.defineProperty(p, "a", desc); // "called: a"
复制代码

若是 defineProperty()方法内部没有任何操做,只返回 false,致使添加新属性老是无效。注意,这里的 false 只是用来提示操做失败,自己并不能阻止添加新属性。

注意,若是目标对象不可扩展(non-extensible),则 defineProperty()不能增长目标对象上不存在的属性,不然会报错。另外,若是目标对象的某个属性不可写(writable)或不可配置(configurable),则 defineProperty()方法不得改变这两个设置。

八、defineProperty()

用法是拦截 Object.getOwnPropertyDescriptor(),必须返回一个 object 或 undefined。该方法接受两个参数,target:目标对象。prop:返回属性名称的描述。

let p = new Proxy(
  { a20 },
  {
    getOwnPropertyDescriptorfunction (target, prop{
      console.log("called: " + prop);
      return { configurabletrueenumerabletruevalue10 };
    },
  }
);

console.log(Object.getOwnPropertyDescriptor(p, "a").value); // "called: a"
// 10
复制代码

九、getPrototypeOf()

是一个代理(Proxy)方法,当读取代理对象的原型时,该方法就会被调用。该方法只接受一个参数,target:被代理的目标对象。返回值必须是一个对象或者 null。 触发该方法的条件总共有 5 种:

  1. Object.getPrototypeOf()
  2. Reflect.getPrototypeOf()
  3. __proto__
  4. Object.prototype.isPrototypeOf()
  5. instanceof
let proto = {};
let p = new Proxy(
  {},
  {
    getPrototypeOf(target) {
      return proto;
    },
  }
);
Object.getPrototypeOf(p) === proto; // true
复制代码

十、isExtensible()

用于拦截对对象的 Object.isExtensible()。该方法接受一个参数。target:目标对象。返回值必须返回一个 Boolean 值或可转换成 Boolean 的值。

let p = new Proxy(
  {},
  {
    isExtensiblefunction (target{
      console.log("called");
      return true;
    },
  }
);

console.log(Object.isExtensible(p)); // "called"
// true
复制代码

注意,该方法有一个强制的约束,即 Object.isExtensible(proxy) 必须同 Object.isExtensible(target)返回相同值。也就是必须返回 true 或者为 true 的值,返回 false 和为 false 的值都会报错。

let p = new Proxy(
  {},
  {
    isExtensiblefunction (target{
      return false;
    },
  }
);

Object.isExtensible(p); // Uncaught TypeError: 'isExtensible' on proxy: trap result does not reflect extensibility of proxy target (which is 'true')
复制代码

十一、ownKeys()

用来拦截对象自身属性的读取操做。该方法接受一个参数,target:目标对象。

具体拦截以下:

  1. Object.getOwnPropertyNames()
  2. Object.getOwnPropertySymbols()
  3. Object.keys()
  4. for...in 循环

该方法有几个约束条件:

  1. ownKeys 的结果必须是一个数组
  2. 数组的元素类型要么是一个 String ,要么是一个 Symbol
  3. 结果列表必须包含目标对象的全部不可配置(non-configurable )、自有(own)属性的 key
  4. 若是目标对象不可扩展,那么结果列表必须包含目标对象的全部自有(own)属性的 key,不能有其它值
let target = {
  a1,
  b2,
  c3,
};
let p = new Proxy(target, {
  ownKeys(target) {
    return ["a"];
  },
});
Object.keys(p); // ['a']
复制代码

十二、preventExtensions()

用于设置对 Object.preventExtensions()的拦截。该方法接受一个参数,target:所要拦截的目标对象。该方法返回一个布尔值。该方法有个限制,即若是目标对象是可扩展的,那么只能返回 false。

let p = new Proxy(
  {},
  {
    preventExtensionsfunction (target{
      return true;
    },
  }
);

Object.preventExtensions(p); // Uncaught TypeError: 'preventExtensions' on proxy: trap returned truish but the proxy target is extensible
复制代码

上面代码中,proxy.preventExtensions()方法返回 true,但这时 Object.isExtensible(proxy)会返回 true,所以报错。

为了防止出现这个问题,一般要在 proxy.preventExtensions()方法里面,调用一次 Object.preventExtensions()。

let p = new Proxy(
  {},
  {
    preventExtensionsfunction (target{
      console.log("called");
      Object.preventExtensions(target);
      return true;
    },
  }
);

console.log(Object.preventExtensions(p)); // "called"
// false
复制代码

1三、setPrototypeOf()

用来拦截 Object.setPrototypeOf()。该方法接受两个参数,target:被拦截目标对象。prototype:对象新原型或为 null。若是成功修改了[[Prototype]], setPrototypeOf 方法返回 true,不然返回 false。

var handler = {
  setPrototypeOf(target, proto) {
    throw new Error("Changing the prototype is forbidden");
  },
};
var proto = {};
var target = function ({};
var proxy = new Proxy(target, handler);
Object.setPrototypeOf(proxy, proto);
// Error: Changing the prototype is forbidden
复制代码

上面代码中,只要修改 target 的原型对象,就会报错。

注意,若是目标对象不可扩展(non-extensible),setPrototypeOf()方法不得改变目标对象的原型。

三、Object.defineProperty 和 Proxy 的区别

最主要的区别实际上是应用到 Vue 的双向数据绑定中,其实也有因为 Vue 的双向数据绑定,才让这两个方法愈来愈多的进入到人们的视野中。

在此咱们不深刻讲解双向数据绑定,因此分别用这两个方法实现个简单版的双向数据绑定,来看看二者之间的区别。

3.一、Object.defineProperty 版

const obj = {};
Object.defineProperty(obj, "text", {
  getfunction ({
    console.log("get val");
  },
  setfunction (newVal{
    console.log("set val:" + newVal);
    document.getElementById("input").value = newVal;
    document.getElementById("span").innerHTML = newVal;
  },
});

const input = document.getElementById("input");
input.addEventListener("keyup"function (e{
  obj.text = e.target.value;
});
复制代码

能够看出来这个简单版的透露出Object.defineProperty一个很明显的缺点,就是只能对对象的一个属性进行监听,若是想要对对象的全部属性监听的话,就要去遍历,而且还有一个问题就是没法去监听数组,可是仍是有优势的,优势就是兼容性好,这也是为何Vue2.0优先选择了Object.defineProperty的缘由。

3.二、Proxy 版

const input = document.getElementById('input');
const p = document.getElementById('p');
const obj = {};

const newObj = new Proxy(obj, {
  getfunction(target, key, receiver{
    console.log(`getting ${key}!`);
    return Reflect.get(target, key, receiver);
  },
  setfunction(target, key, value, receiver{
    console.log(target, key, value, receiver);
    if (key === 'text') {
      input.value = value;
      p.innerHTML = value;
    }
    return Reflect.set(target, key, value, receiver);
  },
});

input.addEventListener('keyup'function(e{
  newObj.text = e.target.value;
});
复制代码

从上述能够看出Proxy能够对整个对象进行拦截,而且其能返回一个新的对象,除此以外其能对数组进行拦截。并且最直观的就是其有13个拦截方式。可是其最致命的问题就是兼容性很差,并且没法用polyfill磨平,所以尤大大才声明须要等到下个大版本(3.0)才能用Proxy重写。

后语

相关文章:

以为还能够的,麻烦走的时候能给点个赞,你们一块儿学习和探讨!

还能够关注个人博客但愿能给个人github上点个Start,小伙伴们必定会发现一个问题,个人全部用户名几乎都与番茄有关,由于我真的很喜欢吃番茄❤️!!!

想跟车不迷路的小伙还但愿能够关注公众号 前端老番茄 或者扫一扫下面的二维码👇👇👇。

我是一个编程界的小学生,您的鼓励是我不断前进的动力,😄但愿能一块儿加油前进。

本文使用 mdnice 排版

相关文章
相关标签/搜索