# defineProperty和Proxy

# 1、defineProperty

Object.defineProperty可以在一个对象上定义一个新的属性,或者修改一个对象的现有属性, 并且返回这个对象。

# 语法

Object.defineProperty(obj, prop. descriptor)

# 参数

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

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

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

比如

let obj = {};
Object.defineProperty(obj, 'name', {
  value: '1',
  configurable: true,
  enumerable: true,
  writable: true
});

使用这种方式我们可以进行更多的配置,descriptor所表示的属性描述符有两种: 数据描述符和存取描述符

1、两者均具有以下两种键值

configurable

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

enumerable

当且仅当该属性的 enumerable 为 true 时,该属性才能够出现在对象的枚举属性中。默认为 false。

2、数据描述符

value

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

writable

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

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

get

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

set

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

属性描述符必须是数据描述符或者存取描述符两种形式之一,不能同时是两者

也就是说value writable和set get不能同时使用, 下面这种方式是不允许的

// 报错 Invalid property descriptor. Cannot both specify accessors and a value or writable attribute
Object.defineProperty({}, "num", {
    value: 1,
    get: function() {
        return 1;
    }
});

# Setters 和 Getters

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

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

let obj = {},
  value = null;

Object.defineProperty(obj, 'num', {
  set(val) {
    console.log('执行了set');
    value = val;
  },
  get() {
    console.log('执行了get');
    return value;
  }
});

obj.num = 1; // 执行了set
console.log(obj.num); // 执行了get
	

封装一个存档构造函数

function Archiver(prop) {
  let value = null;
  let archive = [];
  Object.defineProperty(this, prop, {
    set: function (v) {
      archive.push({
        method: 'set',
        value: v
      });
      value = v;
    },
    get: function () {
      archive.push({
        method: 'get',
        value: value
      });
      return value;
    }
  });
  this.getArchive = function () {
    return archive;
  };
  return this;
}

const arc = new Archiver('num');
arc.num = 1;
arc.num = 2;
console.log(arc.num);
console.log(arc.getArchive());

# watch API

简单写一个实现数据变化的时候,DOM自动触发渲染的例子

  <body>
    <span id="num"></span>
    <br />
    <button id="add_btn">add 1</button>
    <script>
      let num = 1;
      let obj = {};
      const numSpan = document.querySelector('#num');
      const addBtn = document.querySelector('#add_btn');
      Object.defineProperty(obj, 'num', {
        configurable: true,
        enumerable: true,
        set: function (val) {
          num = val;
          numSpan.textContent = val;
          console.log('set');
        },
        get: function () {
          return num;
        }
      });
      obj.num = num;
      addBtn.addEventListener('click', e => {
        obj.num++;
      });
    </script>
  </body>

代码看似很多, 也没有直接的关联了,但是当我们需要改变 span 标签里的值的时候,直接修改 obj.value 的值就可以了。如果需要监测的属性比较多,一个个写比较麻烦,提供一个简单的watch函数

watch(obj, 'num', val => {
  numSpan.textContent = val;
});
function watch(obj, name, callback) {
  var value = obj[name];
  Object.defineProperty(obj, name, {
    get: function () {
      return value;
    },
    set: function (newValue) {
      value = newValue;
      console.log('set');
      callback(value);
    }
  });
  if (value) obj[name] = value;
}

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

# 2、Proxy

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

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

var proxy = new Proxy(target, handler);

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

var proxy = new Proxy({}, {
    get: function(obj, prop) {
        console.log('设置 get 操作')
        return obj[prop];
    },
    set: function(obj, prop, value) {
        console.log('设置 set 操作')
        obj[prop] = value;
    }
});

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

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

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

const handler = {
  has: (target, key) => {
    if (key[0] === '_') {
      return false;
    }
    return key in target;
  }
};

const target = new Proxy({ _foo: 'foo', zoo: 'zoo' }, handler);
console.log('_foo' in target); // false
console.log('zoo' in target); // true

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

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

const handler = {
  apply: (func, context, args) => {
    console.log(func, context, args);
    return 'return from proxy';
  }
};

const fn = function (a, b) {
  return a + b;
};

const fnP = new Proxy(fn, handler);
console.log(fnP.call({ x: 1 }, 1, 2));

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

  • Object.getOwnPropertyNames()
  • Object.getOwnPropertySymbols()
  • Object.keys()
let target = {
  _bar: 'foo',
  _prop: 'bar',
  prop: 'baz'
};

let handler = {
  ownKeys (target) {
    return Reflect.ownKeys(target).filter(key => key[0] !== '_');
  }
};

let proxy = new Proxy(target, handler);
for (let key of Object.keys(proxy)) {
  console.log(target[key]);
}
// "baz"

# 使用proxy重写watch函数

function watch(target, callback) {
  const proxy = new Proxy(target, {
    get: function (target, prop) {
      return target[prop];
    },
    set: function (target, prop, value) {
      target[prop] = value;
      callback(prop, value);
    }
  });
  return proxy;
}
const proxy = watch({}, (prop, val) => {
  if (prop === 'num') {
    numSpan.textContent = val;
  }
});
proxy.num = 1;
addBtn.addEventListener('click', e => {
  proxy.num += 1;
})
上次更新: 1/22/2025, 9:39:13 AM