Object.defineProperty()和Proxy用法 - JavaScript

Object.defineProperty()为对象添加属性时,会为属性设置对应的get/set方法,做为属性的观察者,ES6的Proxy是一个完整的代理方案。

使用Object.defineProperty添加的属性,能在属性发生变化或被读取时,调用对应的方法,Vue.js早期使用该方法实现数据双向绑定。

Vue3.0使用Proxy代替Object.defineProperty。若对兼容性没严格要求,Proxy是很好的解决方案。

Object.defineProperty()

Object.defineProperty(obj, prop, descriptor)

Object.defineProperty参数说明

obj:任意js对象,属性所属的对象。

prop:属性的名称,可以是新属性,也可以是要修改的属性。

descriptor:属性描述符,定义属性所拥有的特性。

如:能否被赋值、使用for...in遍历时属性能否被看到。

注:descriptor参数是最变化多端的地方,决定了Object.defineProperty的很多特性。

Object.defineProperty descriptor参数

descriptor参数定义了属性所拥有的特性:数据属性、访问属性。

这两个特性不能在同一个属性上兼而有之。

数据属性

当descriptor为数据属性时,descriptor参数拥有一个value属性,value属性的值做为初始化参数,赋给obj.prop,writable属性决定obj.prop能否被重新赋值。

var o = {};
Object.defineProperty(o, "a", {
  value : 37,
  writable : true,
  enumerable : true,
  configurable : true
});

在示例中,为对象o添加了一个属性a,a的初始值为37。

属性a能够被重新赋值(writable)。

for(var i in o){
  console.log(i)
}

使用for循环时属性a能被遍历出来,可见(enumerable)。

可使用delete操作删除属性a(configurable)。

访问属性

当descriptor为访问属性时,对象的属性可拥有一对get/set方法,对属性执行赋值和读取操作时,会触发这对方法。

var value = 1,obj = {};

Object.defineProperty(obj, "p", {
  get : function(){
    return value;
  },
  set : function(newValue){
    value = newValue;
  },
  enumerable : true,
  configurable : true
});

在示例中为obj对象添加了属性p,并附带了两个代理方法。

descriptor参数说明

configurable

当该值为true时,属性能够被修改,也就是说属性可以在两种形式(数据属性/访问属性)之间自由切换。

同时,表示对象上的属性能否被删除,其他特性能否被修改(除value和writable外),默认为false。

var obj = {},value = 12;

var nobj = Object.defineProperty(obj, "b", {
 get : function(){
  return value;
 },
 set : function(newValue){
   value = newValue;
 },
 enumerable : true,
 configurable : false
});

var ab = Object.defineProperty(obj, 'b', {
 value: 37,
 writable: true
});

enumerable

当该值为true时,属性能够被js的枚举方法看到,这意味着能够使用for...in和Object.keys()操作对象上的属性,默认为false。

var obj = {};

var ab = Object.defineProperty(obj, 'b', {
  value: 37,
  enumerable : false,
  writable: true
});

for(var p in obj){
    console.log(obj[p]);
}

以上两个参数是descriptor参数通用属性。

当descriptor为数据属性时有以下可选值:

value

与属性关联的值,可以是任何符合JavaScript规范的值(数字、对象、函数等),默认为undefined。

writable

当该值为true时,属性的值可使用赋值运算符修改,默认为false。

var o = {};

Object.defineProperty(o, 'a', {
  value: 37,
  writable: false
});

console.log(o.a); //37
o.a = 25;
console.log(o.a); //37

// strict 模式
(function() {
  'use strict';
  var o = {};
  Object.defineProperty(o, 'b', {
    value: 2,
    writable: false
  });
  o.b = 3; // throws TypeError: "b"为只读
  return o.b; // returns 2
}());

当descriptor为访问属性时有以下可选值:

get

作为属性对应的getter方法,在属性被访问时,会调用此getter方法。默认为undefined。

set

作为属性对应setter的方法,当属性值被修改时,该方法会被调用,方法接受一个参数,即,属性的新值。默认为undefined。

Object.defineProperty注意事项

如果descriptor参数,不具有value、writable、get、set任意一个属性,将被默认为是一个数据描述符。

如果一个描述符同时具有value/writable和get/set属性,将会出现异常。

有时将一个对象做为descriptor参数传入时,为防止此对象继承一些未知属性,可使用Object.create(null)创建一个无任何继承的干净对象。

var obj = {};

var descriptor = Object.create(null); // 没有属性继承的对象
// 默认没有 enumerable、configurable、writable

descriptor.value = 'static';
Object.defineProperty(obj, 'key', descriptor);

继承属性

如果,访问者的属性是被继承的,它的get和set方法能被子对象访问。

若这些方法共享一个变量,该值将会被所有对象共享。

function myclass() {}

var value;

Object.defineProperty(myclass.prototype, "x", {
  get() {
    return value;
  },
  set(x) {
    value = x;
  }
});

var a = new myclass();
var b = new myclass();

a.x = 1;
console.log(b.x); // 1

将值存储在私有属性上,能够解决此类问题,在get和set方法中,this指向属性所在的对象。

function myclass() {}

Object.defineProperty(myclass.prototype, "x", {
  get() {
    return this.stored_x;
  },
  set(x) {
    this.stored_x = x;
  }
});

var a = new myclass();
var b = new myclass();
a.x = 1;
console.log(b.x); // undefined

与访问者属性不同的是,值属性始终在对象自身上设置,而非原型上。

如果一个不可覆写的属性被继承时,它仍具有防止被修改的特性。

function myclass() {}

myclass.prototype.x = 1;
Object.defineProperty(myclass.prototype, "y", {
  writable: false,
  value: 1
});

var a = new myclass();

a.x = 2;
console.log(a.x); // 2
console.log(myclass.prototype.x); // 1

a.y = 2; // Ignored, throws in strict mode
console.log(a.y); // 1
console.log(myclass.prototype.y); // 1

js的方法与函数在概念上是有明确定义的,在上述讨论中有意忽略了这一点。