掌握下Object.defineProperty的用法

225 阅读8分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第21天,点击查看活动详情

前言

文中内容都是参考MDN Object.defineProperty() 以及理解Object.defineProperty的作用 - SegmentFault 思否 内容。

对象是由多个名/值对组成的无序的集合。对象中每个属性对应任意类型的值。

定义对象可以使用构造函数或者对象字面量的形式:

// 使用new操作符后跟Object构造函数
var person = new Object();
person.name = 'zxx';
person.say = function () {};

// 使用对象字面量
var person = {
  name: 'zxx',
  say: function () {}
}

除了上面添加属性的方式,还可以使用Object.defineProperty定义新属性或者修改原有的属性。

Object.defineProperty()

语法:

Object.defineProperty(obj, prop, descriptor);

参数说明:

  • obj: 必需。要定义属性的对象;

  • prop: 必需。要定义或修改的属性的名称或Symbol;

  • descriptor: 必需。要定义或修改的属性描述符。

返回值:

返回被传递给函数的对象。即第一个参数obj

注:通过赋值操作添加的普通属性是可枚举的(在枚举对象属性时会被枚举到(for...in或Object.keys方法)),可以改变这些属性的值,也可以删除这些属性。使用Object.defineProperty()添加的属性值是不可修改(immutable)的

对象里目前存在的属性描述符有两种主要形式:数据描述符和存取描述符。一个描述符只能是这两者其中之一,不能同时是两者。

两者共享以下可选键值(默认值是指在使用 Object.defineProperty() 定义属性时的默认值)

属性默认值说明
configurablefalse当且仅当该属性的 configurable 键值为 true 时,该属性的描述符才能够被改变,同时该属性也能从对应的对象上被删除。
enumerablefalse当且仅当该属性的 enumerable 键值为 true 时,该属性才会出现在对象的枚举属性中。(即是否通过for-in循环或Object.k返回属性)

数据(数据描述符)属性

数据描述符是一个具有值的属性,该值是可写的,也可以是不可写的。

还具有以下可选键值:

属性默认值说明
valueundefined该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。
writablefalse当且仅当该属性的 writable 键值为 true 时,属性的值,也就是上面的 value,才能被赋值运算符改变

访问器(存取描述符)属性

存取描述符是由getter函数和setter函数所描述的属性。

属性默认值说明
getundefined属性的 getter 函数,如果没有 getter,则为undefined。当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的this并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值。
setfalse属性的 setter 函数,如果没有 setter,则为 undefined。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象。

汇总下描述符默认值:

  • 拥有布尔值的键 configurable、enumerable 和 writable 的默认值都是 false

  • 属性值和函数的键 value、get 和 set 字段的默认值为 undefined

数据描述: 当修改或定义对象的某个属性时,给这个属性添加一些特性:

var person = {};         // 创建一个新对象

// 在对象中添加一个属性与数据描述符的示例
// 在对象person中添加了一个属性name,值为zxx
Object.defineProperty(person, "name", {
  value : 'zxx',        // 可定义任意类型的值
  writable : true,
  enumerable : true,
  configurable : true
});

// 在对象中添加一个设置了存取描述符属性的示例
// 在对象person中设置age属性,值为18
var age = 18;
Object.defineProperty(person, "age", {
  // 使用了方法名称缩写(ES2015 特性)
  // 下面两个缩写等价于:
  // get : function() { return age; },
  // set : function(newValue) { age = newValue; },
  get() { return age; },
  set(newValue) { age = newValue; },
  enumerable : true,
  configurable : true
});



// 注:数据描述符和存取描述符不能混合使用
Object.defineProperty(person, "conflict", {
  value: 0x9f91102,
  get() { return 0xdeadbeef; } 
});

// Uncaught TypeError: Invalid property descriptor. Cannot both specify accessors and a value or writable attribute

value

属性对应的值,可以设置为任意类型的值,默认为undefined 

var person = {};
// 不设置value属性默认为undefined
Object.defineProperty(person, "name", {});
console.log(person.name);        // undefined

// 设置value属性
Object.defineProperty(person, "name", {
    value: "zxx"
});
console.log(person.name);        // zxx

writable

属性的值是否可以被重写,设置为true才可以被重写。默认为false

var person = {};
// 设置writable为false,不能重写;
Object.defineProperty(person, "name", {
    value: "zxx",
    writable: false
});

// 修改name的值
person.name = "hello";
console.log(person.name);    // 结果依然是 zxx

// 设置writable的值为true, 可以被重写
Object.defineProperty(person, "name", {
    value: "zxx",
    writable: true
});
// 修改name的值
person.name = "hello";
console.log(person.name);    // 修改成功,输出 hello

enumerable

此属性是否可以被枚举(使用for...inObject.keys())。设置为true才可以被枚举。默认为false。

var person = {};
// 设置enumerable为false,不能被枚举;
Object.defineProperty(person, "name", {
    value: "zxx",
    enumerable: false
});
// 枚举对象的属性
for (var key in person) {
    console.log( key );
}

// 设置enumerable为true,能被枚举;
Object.defineProperty(person, "name", {
    value: "zxx",
    enumerable: true
});
// 枚举对象的属性
for (var key in person) {
    console.log( key );        // name
}

configurable

是否可以删除目标属性或者是否可以再次修改属性的特性(writable, configurable, enumerable),设置为true可以被删除或者可以重新设置特性;设置为false, 定义的属性不能被删除或者不可以重新设置特性。默认值为false。

这个属性有两个作用:

  • 目标属性是否可以使用delete删除;
  • 目标属性是否可以被再次设置特性;
// 测试目标属性是否能被删除
var person = {};
// 设置configurable为false,不能被删除;
Object.defineProperty(person, "name", {
    value: "zxx",
    configurable: false
});
// 删除name属性
delete person.name;
console.log(person.name);     // zxx

// 设置configurable为true,能被删除;
Object.defineProperty(person, "name", {
    value: "zxx",
    configurable: true
});
// 删除name属性
delete person.name;
console.log(person.name);     // undefined

// 测试目标属性是否可以再次被修改特性
var person = {};
// 设置configurable为false,不能再次修改特性;
Object.defineProperty(person, "name", {
    value: "zxx",
    writable: false,
    enumerable: false,
    configurable: false
});

// 重新修改特性
Object.defineProperty(person, "name", {
    value: "zxx",
    writable: true,
    enumerable: true,
    configurable: true
});
console.log( person.name );    // Uncaught TypeError: Cannot redefine property: name

// 设置configurable为true,能再次修改特性;
Object.defineProperty(person, "name", {
    value: "zxx",
    writable: false,
    enumerable: false,
    configurable: true
});

// 重新修改特性
Object.defineProperty(person, "name", {
    value: "zxx",
    writable: true,
    enumerable: true,
    configurable: true
});
console.log( person.name );    // zxx

除了可以给新定义的属性设置特性,也可以给已有的属性设置特性:

// 定义对象的时候添加的属性,是可删除、可重写、可枚举的
var person = {
    name: "zxx"
};

// 删除
delete person.name;
console.log(person.name);    // undefined

// 重写
person.name = "hello";
console.log(person.name);    // hello

// 枚举
console.log(Object.keys(person));    // ["name"]

// 设置name的writable属性为false
Object.defineProperty(person, "name", {
    writable: false
});

// 重写name
person.name = "hello";
console.log(person.name);    // 依然是 zxx

注:一旦使用Object.defineProperty给对象添加属性,那么如果不设置属性的特性,那么configurable、enumerable 和 writable 的值都是默认的 false

var person = {}
// 不设置属性的特性,那么configurable、enumerable 和 writable 的值都是默认的 false
// 这样就导致name这个属性不能被重写、不能枚举、不能再次被设置特性
Object.defineProperty(person, "name", {});

// 设置值
person.name = "hello";
console.log(person.name);    // undefined

console.log(Object.keys(person));    // []

设置的特性总结为:

  • value: 设置属性的值;

  • writable: 值是否可以被重写。 true | false

  • enumerable: 目标属性是否可以被枚举。 true | false

  • configurable: 目标属性是否可以被删除或是否可以再次修改特性。 true | false

存取器描述

当属性存取器描述属性特性的时候,允许设置以下特性属性:

var person = {};
Object.defineProperty(person, "newKey", {
    get: function () {} | undefined,
    set: function () {} | undefined,
    configurable: true | false,
    enumerable: true | false
});

注:当使用了getter或setter方法,不允许使用writable和value这两个属性

getter/setter

当设置或获取对象的某个属性的值的时候,可以提供getter/setter方法

  • getter 是一种获得属性值的方法
  • setter 是一种设置属性值的方法
var person = {};
var initValue = "zxx"
Object.defineProperty(person, "name", {
    get: function () {
        console.log("当获取值的时候触发的函数");
        return initValue; 
    },
    set: function (value) {
        console.log("当设置值的时候触发的函数,设置的新值通过参数value拿到");
        initValue = value;
    }
});

console.log(person.name);
// 当获取值的时候触发的函数
// zxx


// 修改name值
person.name = "hello";    // 当设置值的时候触发的函数,设置的新值通过参数value拿到
console.log(person.name);    
// 当获取值的时候触发的函数
// hello

注: get或set不是必须成对出现,任写其一就可以,如果不设置方法,则get和set默认值为undefined

兼容性

在ie8下只能在DOM对象上使用,尝试在原生的对象上使用Object.defineProperty()会报错。

实际使用

vue2中使用Object.defineProperty 进行收集依赖,侦听数据变化。
部分代码 defineReactive$$1 如下:

Observer.prototype.walk = function walk(obj) {
  var keys = Object.keys(obj);
  for (var i = 0; i < keys.length; i++) {
    defineReactive$$1(obj, keys[i]);
  }
};

function defineReactive$$1(obj, key, val, customSetter, shallow) {
  var dep = new Dep();

  var property = Object.getOwnPropertyDescriptor(obj, key);
  // 返回指定对象上一个自有属性对应的属性描述符

  if (property && property.configurable === false) {
    return;
  }

  var getter = property && property.get;
  var setter = property && property.set;
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key];
  }

  var childOb = !shallow && observe(val);
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      var value = getter ? getter.call(obj) : val;
      if (Dep.target) {
        dep.depend();
        if (childOb) {
          childOb.dep.depend();
          if (Array.isArray(value)) {
            dependArray(value);
          }
        }
      }
      return value;
    },
    set: function reactiveSetter(newVal) {
      var value = getter ? getter.call(obj) : val;
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return;
      }
      if (customSetter) {
        customSetter();
      }
      if (getter && !setter) {
        return;
      }
      if (setter) {
        setter.call(obj, newVal);
      } else {
        val = newVal;
      }
      childOb = !shallow && observe(newVal);
      dep.notify();
    },
  });
}

参考资料:

MDN Object.defineProperty()

理解Object.defineProperty的作用 - SegmentFault 思否