ES6 系列之 defineProperty 与 proxy

前言

我们或多或少都听过“数据绑定”这个词,“数据绑定”的关键在于监听数据的变化,可是对于这样一个对象: varobj={value:1},我们该怎么知道 obj发生了改变呢?

definePropety

ES5提供了 Object.defineProperty 方法,该方法可以在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象。

语法

Object.defineProperty(obj,prop,descriptor)

参数


 
  1. obj: 要在其上定义属性的对象。

  2.  

  3. prop: 要定义或修改的属性的名称。

  4.  

  5. descriptor: 将被定义或修改的属性的描述符。

举个例子:


 
  1. var obj = {};

  2. Object.defineProperty(obj, "num", {

  3. value : 1,

  4. writable : true,

  5. enumerable : true,

  6. configurable : true

  7. });

  8. // 对象 obj 拥有属性 num,值为 1

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

函数的第三个参数 descriptor 所表示的属性描述符有两种形式:数据描述符和存取描述符。

两者均具有以下两种键值:

configurable

当且仅当该属性的 configurabletrue 时,该属性描述符才能够被改变,也能够被删除。默认为 false

enumerable

当且仅当该属性的 enumerabletrue 时,该属性才能够出现在对象的枚举属性中。默认为 false。 数据描述符同时具有以下可选键值:

writable

当且仅当该属性的 writable为 true 时,该属性才能被赋值运算符改变。默认为 false

上面3个属性MDN上给出的默认值都是false,但是实际测试时,定义一个字面量对象,打印它的descriptor,以上三个属性的值都为true...例如下面的示例:


 
  1. const m = { e: 3 }

  2. console.log(Object.getOwnPropertyDescriptor(m, 'e'));

打印结果:


 
  1. {

  2. configurable: true

  3. enumerable: true

  4. value: 3

  5. writable: true

  6. }

value

该属性对应的值。可以是任何有效的 JavaScript值(数值,对象,函数等)。默认为 undefined

存取描述符同时具有以下可选键值

get

一个给属性提供 getter 的方法,如果没有 getter则为 undefined。该方法返回值被用作属性值。默认为 undefined

set

一个给属性提供 setter的方法,如果没有 setter则为 undefined。该方法将接受唯一参数,并将该参数的新值分配给该属性。默认为 undefined

值得注意的是:

属性描述符必须是数据描述符或者存取描述符两种形式之一,不能同时是两者 。这就意味着你可以:


 
  1. Object.defineProperty({}, "num", {

  2. value: 1,

  3. writable: true,

  4. enumerable: true,

  5. configurable: true

  6. });

也可以:


 
  1. var value = 1;

  2. Object.defineProperty({}, "num", {

  3. get : function(){

  4. return value;

  5. },

  6. set : function(newValue){

  7. value = newValue;

  8. },

  9. enumerable : true,

  10. configurable : true

  11. });

但是不可以:


 
  1. // 报错

  2. Object.defineProperty({}, "num", {

  3. value: 1,

  4. get: function() {

  5. return 1;

  6. }

  7. });

此外,所有的属性描述符都是非必须的,但是 descriptor 这个字段是必须的,如果不进行任何配置,你可以这样:


 
  1. var obj = Object.defineProperty({}, "num", {});

  2. console.log(obj.num); // undefined

Setters 和 Getters

之所以讲到 defineProperty,是因为我们要使用存取描述符中的 getset,这两个方法又被称为 gettersetter。由 gettersetter 定义的属性称做 存取器属性

当程序查询存取器属性的值时, JavaScript 调用 getter方法。这个方法的返回值就是属性存取表达式的值。当程序设置一个存取器属性的值时, JavaScript 调用 setter 方法,将赋值表达式右侧的值当做参数传入 setter。从某种意义上讲,这个方法负责“设置”属性值。可以忽略 setter方法的返回值。

举个例子:


 
  1. var obj = {}, value = null;

  2. Object.defineProperty(obj, "num", {

  3. get: function(){

  4. console.log('执行了 get 操作')

  5. return value;

  6. },

  7. set: function(newValue) {

  8. console.log('执行了 set 操作')

  9. value = newValue;

  10. }

  11. })

  12.  

  13. obj.num = 1 // 执行了 set 操作

  14.  

  15. console.log(obj.num); // 执行了 get 操作 // 1

这不就是我们要的监控数据改变的方法吗?我们再来封装一下:


 
  1. function Archiver() {

  2. var value = null;

  3. // archive n. 档案

  4. var archive = [];

  5.  

  6. Object.defineProperty(this, 'num', {

  7. get: function() {

  8. console.log('执行了 get 操作')

  9. return value;

  10. },

  11. set: function(value) {

  12. console.log('执行了 set 操作')

  13. value = value;

  14. archive.push({ val: value });

  15. }

  16. });

  17.  

  18. this.getArchive = function() { return archive; };

  19. }

  20.  

  21. var arc = new Archiver();

  22. arc.num; // 执行了 get 操作

  23. arc.num = 11; // 执行了 set 操作

  24. arc.num = 13; // 执行了 set 操作

  25. console.log(arc.getArchive()); // [{ val: 11 }, { val: 13 }]

watch API

既然可以监控数据的改变,那我可以这样设想,即当数据改变的时候,自动进行渲染工作。举个例子:

HTML 中有个 span 标签和 button 标签


 
  1. <span id="container">1</span>

  2. <button id="button">点击加 1</button>

当点击按钮的时候, span 标签里的值加 1

传统的做法是:


 
  1. document.getElementById('button').addEventListener("click", function(){

  2. var container = document.getElementById("container");

  3. container.innerHTML = Number(container.innerHTML) + 1;

  4. });

如果使用了 defineProperty


 
  1. var obj = {

  2. value: 1

  3. }

  4.  

  5. // 储存 obj.value 的值

  6. var value = 1;

  7.  

  8. Object.defineProperty(obj, "value", {

  9. get: function() {

  10. return value;

  11. },

  12. set: function(newValue) {

  13. value = newValue;

  14. document.getElementById('container').innerHTML = newValue;

  15. }

  16. });

  17.  

  18. document.getElementById('button').addEventListener("click", function() {

  19. obj.value += 1;

  20. });

代码看似增多了,但是当我们需要改变 span 标签里的值的时候,直接修改 obj.value的值就可以了。

然而,现在的写法,我们还需要单独声明一个变量存储 obj.value的值,因为如果你在 set中直接 obj.value=newValue就会陷入无限的循环中。此外,我们可能需要监控很多属性值的改变,要是一个一个写,也很累呐,所以我们简单写个 watch函数。使用效果如下:


 
  1. var obj = {

  2. value: 1

  3. }

  4.  

  5. watch(obj, "value", function(newvalue){

  6. document.getElementById('container').innerHTML = newvalue;

  7. })

  8.  

  9. document.getElementById('button').addEventListener("click", function(){

  10. obj.value += 1

  11. });

我们来写下这个 watch 函数:


 
  1. (function(){

  2. var root = this;

  3. function watch(obj, name, func){

  4. var value = obj[name];

  5.  

  6. Object.defineProperty(obj, name, {

  7. get: function() {

  8. return value;

  9. },

  10. set: function(newValue) {

  11. value = newValue;

  12. func(value)

  13. }

  14. });

  15.  

  16. if (value) obj[name] = value

  17. }

  18.  

  19. this.watch = watch;

  20. })()

现在我们已经可以监控对象属性值的改变,并且可以根据属性值的改变,添加回调函数,棒棒哒~

proxy

使用 defineProperty 只能重定义属性的读取 (get)和设置 (set)行为,到了 ES6,提供了 Proxy,可以重定义更多的行为,比如 indelete、函数调用等更多行为。

Proxy这个词的原意是代理,用在这里表示由它来“代理”某些操作, ES6原生提供 Proxy构造函数,用来生成 Proxy实例。我们来看看它的语法:


 
  1. var proxy = new Proxy(target, handler);

proxy 对象的所有用法,都是上面这种形式,不同的只是 handler参数的写法。其中, newProxy()表示生成一个 Proxy实例, target参数表示所要拦截的目标对象, handler参数也是一个对象,用来定制拦截行为。


 
  1. var proxy = new Proxy({}, {

  2. get: function(obj, prop) {

  3. console.log('设置 get 操作')

  4. return obj[prop];

  5. },

  6. set: function(obj, prop, value) {

  7. console.log('设置 set 操作')

  8. obj[prop] = value;

  9. }

  10. });

  11.  

  12. proxy.time = 35; // 设置 set 操作

  13.  

  14. console.log(proxy.time); // 设置 get 操作 // 35

除了 getset 之外, proxy可以拦截多达 13 种操作,比如 has(target,propKey),可以拦截 propKeyinproxy的操作,返回一个布尔值。


 
  1. // 使用 has 方法隐藏某些属性,不被 in 运算符发现

  2. var handler = {

  3. has (target, key) {

  4. if (key[0] === '_') {

  5. return false;

  6. }

  7. return key in target;

  8. }

  9. };

  10. var target = { _prop: 'foo', prop: 'foo' };

  11. var proxy = new Proxy(target, handler);

  12. console.log('_prop' in proxy); // false

又比如说 apply方法拦截函数的调用、 callapply 操作。

apply方法可以接受三个参数,分别是目标对象、目标对象的上下文对象 (this)和目标对象的参数数组,不过这里我们简单演示一下:


 
  1. var target = function () { return 'I am the target'; };

  2. var handler = {

  3. apply: function () {

  4. return 'I am the proxy';

  5. }

  6. };

  7.  

  8. var p = new Proxy(target, handler);

  9.  

  10. p();

  11. // "I am the proxy"

又比如说 ownKeys 方法可以拦截对象自身属性的读取操作。具体来说,拦截以下操作:

  • Object.getOwnPropertyNames()

  • Object.getOwnPropertySymbols()

  • Object.keys() 下面的例子是拦截第一个字符为下划线的属性名,不让它被 for of 遍历到。


 
  1. let target = {

  2. _bar: 'foo',

  3. _prop: 'bar',

  4. prop: 'baz'

  5. };

  6.  

  7. let handler = {

  8. ownKeys (target) {

  9. return Reflect.ownKeys(target).filter(key => key[0] !== '_');

  10. }

  11. };

  12.  

  13. let proxy = new Proxy(target, handler);

  14. for (let key of Object.keys(proxy)) {

  15. console.log(target[key]);

  16. }

  17. // "baz"

更多的拦截行为可以查看阮一峰老师的 《ECMAScript 6 入门》

值得注意的是, proxy 的最大问题在于浏览器支持度不够,而且很多效果无法使用 poilyfill来弥补。

watch API优化 我们使用 proxy再来写一下 watch 函数。使用效果如下:


 
  1. (function() {

  2. var root = this;

  3.  

  4. function watch(target, func) {

  5.  

  6. var proxy = new Proxy(target, {

  7. get: function(target, prop) {

  8. return target[prop];

  9. },

  10. set: function(target, prop, value) {

  11. target[prop] = value;

  12. func(prop, value);

  13. }

  14. });

  15.  

  16. return proxy;

  17. }

  18.  

  19. this.watch = watch;

  20. })()

  21.  

  22. var obj = {

  23. value: 1

  24. }

  25.  

  26. var newObj = watch(obj, function(key, newvalue) {

  27. if (key == 'value') document.getElementById('container').innerHTML = newvalue;

  28. })

  29.  

  30. document.getElementById('button').addEventListener("click", function() {

  31. newObj.value += 1

  32. });

我们也可以发现,使用 definePropertyproxy 的区别,当使用 defineProperty,我们修改原来的 obj对象就可以触发拦截,而使用 proxy,就必须修改代理对象,即 Proxy 的实例才可以触发拦截。

 

原文https://mp.weixin.qq.com/s/uEpD3S_80y_MqcxvEcxtpA

上一篇:object.defineProperty()用法


下一篇:08 总结