JS基础进阶—绕不开的Object.defineProperty()

Object.defineProperty()来自 ECMAScript 5.1 (ECMA-262) 规范
兼容 Internet Explorer 9+ 等其他现代浏览器

初识Object.defineProperty()

  Object.defineProperty() 方法用于在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象,该方法允许精确添加或修改对象的属性。
  乍看描述,也许会让人不太理解,在对象上定义属性我们通常使用对象字面量语法或者.操作符来处理,比如

  var obj = {}; obj.attr = 1;
  // 或是
  var obj2 = {attr: 2};

  该方法的重要意义在于后面一句,即该方法允许精确添加或修改对象的属性
  举个栗子,仅用于初步体会该方法的效果。我们有时候会需要遍历一个对象的所有属性,这时可能会使用for in方法

  var obj = {
    name: 'sky',
    age: 18,
    sex: 'male'
  }
  for (var i in obj) {
    console.log(i);
  }
  // 打印结果
  // name
  // age
  // sex

  我们知道for in方法会遍历对象和其原型上所有可枚举的属性,我们自己创建的对象的原型是Object.prototype,该原型上有一个可能常用到的方法Object.prototype.toString(),通常我们用这个方法准确判断数据的类型。我们发现这个方法并没有被遍历出来,如前所述,这个方法显然是不可遍历的。那么我们如何自己实现为属性赋予这些‘’精确‘’特性的效果呢。这就轮到Object.defineProperty()出场了。
  在此之前,我们再看一些“有趣”的东西,先不用理解具体原理,我们在上面程序后面加上一句

console.log(Object.getOwnPropertyDescriptor(obj.__proto__, 'toString'));

  打开控制台发现了如下输出



  发现得到了一些有趣的东西,enumerable(可枚举的)属性是false,这似乎就和之前的现象产生了联系。

认识Object.defineProperty()

语法:

Object.defineProperty(obj, prop, descriptor)

参数:
obj 要在其上定义属性的对象。
prop要定义或修改的属性的名称。
descriptor将被定义或修改的属性描述符。

返回: 被传递给函数的对象

我们发现重点就在第三个参数,属性描述符上面。

属性描述符

在理解属性描述符之前,需要先了解一点前置知识。

getter 和 setter

对象属性是由名字、值和一组特性构成的。在ES5中,属性值可以用一个或两个方法替代,这两个方法就是getter和setter。由getter和setter定义的属性称做 "存取器属性",它不同于 "数据属性",数据属性就是一个简单的值。

var prop = {
    a: 0,
    get b(){
        return 1;
    }   
};

console.log(prop.a); //0
console.log(prop.b); //1

  上面代码中,属性a称为“数据属性”,它只有一个简单的值;像属性b这种用getter和setter方法定义的属性称为“存取器属性”。当一个属性被定义为存取器属性时,JavaScript会忽略它的value和writable特性,取而代之的是set和get(还有configurable和enumerable)特性。
  当程序查询存取器属性的值时,JS调用getter方法(无参数)。这个方法的返回值就是属性存取表达式的值。
  当程序设置一个存取器属性的值时,JS调用setter方法,将赋值表达式右侧的值当做参数传入setter。从某种意义上讲,这个方法负责 "设置"属性值。可以忽略setter方法的返回值。
  如果属性同时具有getter和setter,那么它是一个读/写属性。如果它只有getter方法,那么它是一个只读属性。如果它只有setter方法,那么它是一个只写属性,读取只写属性总是返回undefined。

var prop = {
    get b(){
        return 1;
    }   
};
prop.b = 3; // 这个设置是无效的
console.log(prop.b); //1

和数据属性一样,存取器属性是可以继承的。且从 ECMAScript 2015 开始,还可以使用一个计算属性名的表达式绑定到给定的函数。这里不再赘述。

属性描述符的配置

  对象里目前存在的属性描述符有两种主要形式:数据描述符和存取描述符。数据描述符是一个具有值的属性,该值可能是可写的,也可能不是可写的。存取描述符是由getter-setter函数对描述的属性。描述符必须是这两种形式之一,不能同时是两者。
  相信在理解了前置知识后,这一段也是很清晰明了的。
  以下相关定义摘自MDN

数据描述符存取描述符均具有以下可选键值:
  configurable
  当且仅当该属性的 configurable 为 true 时,该属性描述符才能够被改变,同时该属性也能从对应的对象上被删除。默认为 false。
  enumerable
  当且仅当该属性的enumerable为true时,该属性才能够出现在对象的枚举属性中。默认为 false。

数据描述符同时具有以下可选键值:
  value
  该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。默认为 undefined。
  writable
  当且仅当该属性的writable为true时,value才能被赋值运算符改变。默认为 false。

存取描述符同时具有以下可选键值:
  get
  一个给属性提供 getter 的方法,如果没有 getter 则为 undefined。该方法返回值被用作属性值。默认为 undefined。
  set
  一个给属性提供 setter 的方法,如果没有 setter 则为 undefined。该方法将接受唯一参数,并将该参数的新值分配给该属性。默认为 undefined。

描述符可同时具有的键值

configurable enumerable value writable get set
数据描述符 Yes Yes Yes Yes No No
存取描述符 Yes Yes No No Yes Yes

如果一个描述符不存在value,writable,configurable和enumerable关键字中任意一个,那么它将被认为是一个数据描述符。如果一个描述符同时有(value或writable)和(get或set)关键字,将会产生一个异常。

实际使用

不要忽视继承

  首先需要注意的是用作属性描述符的对象如果继承了某些关键字属性,可能会影响正常的使用,来看一个例子

  var a = {};
  var descriptor = {
    value: 2
  };
  descriptor.__proto__ = {
    writable: true
  };
  Object.defineProperty(a, 'test', descriptor);
  console.log(a.test); // 2
  a.test = 3;
  console.log(a.test); // 3

  在例子中我们为用作属性描述符的descriptor添加了value属性,而如之前所介绍的,如果其他键值没有设置,则默认为false(此时该描述符被认为是一个数据描述符)。这时我们为a.test属性赋值,发现赋值没有按照预期失败,而是成功了。显然是因为descriptor继承了原型上的writable属性,因此属性a变为可写的。
  你可能认为这种更改显而易见,但实际项目中往往较为复杂,提前意识到这些问题往往可以省下很多时间。

创建属性

  如果对象中不存在指定的属性,Object.defineProperty()就创建这个属性。当描述符中省略某些字段时,这些字段将使用它们的默认值。拥有布尔值的字段的默认值都是false。value,get和set字段的默认值为undefined。一个没有get/set/value/writable定义的属性将被当做数据描述符处理。

  var obj = {}; // 创建一个新对象
  // 为新对象添加属性及其属性描述符(数据描述符)
  Object.defineProperty(obj, 'prop', {
    value: 10,
    writable : true,
    enumerable : true,
    configurable : true
  });
  // 此时对象obj拥有了属性prop,值为10,且可修改
  console.log(obj.prop); // 10
  obj.prop = 20;
  console.log(obj.prop); // 20
  var obj2 = {}; // 创建一个新对象
  // 为新对象添加属性及其属性描述符(存取描述符)
  var val;
  Object.defineProperty(obj, 'prop', {
    get : function(){
      return val;
    },
    set : function(newValue){
      val = newValue;
    },
    enumerable : true,
    configurable : true
  });
  obj2.prop = 20; // 为obj2设置值为20
  console.log(obj2.prop); // 20

对于enumerableconfigurable属性,将在后续详细说明。

修改属性

  如果属性已经存在,Object.defineProperty()将尝试根据描述符中的值以及对象当前的配置来修改这个属性。如果旧描述符将其configurable 属性设置为false,则该属性被认为是“不可配置的”,并且没有属性可以被改变(除了单向改变 writable 为 false)。当属性不可配置时,不能在数据和访问器属性类型之间切换。

  var obj = {}; // 创建一个新对象
  // 为新对象添加属性及其属性描述符
  Object.defineProperty(obj, 'prop', {
    value: 10,
    writable : true,
    enumerable : true,
    configurable : true
  });
  // 更改该对象的属性描述符
  Object.defineProperty(obj, 'prop', {
    writable : false
  });
  console.log(obj.prop); // 10
  // Object.getOwnPropertyDescriptor()方法返回指定对象上一个自有属性对应的属性描述符。
  //(自有属性指的是直接赋予该对象的属性,不需要从原型链上进行查找的属性)
  console.log(Object.getOwnPropertyDescriptor(obj, 'prop'));
  // {value: 10, writable: false, enumerable: true, configurable: true}

  当试图改变不可配置属性(除了单向改变 writable 为 false)的值时会抛出错误,除非当前值和新值相同。

  var obj = {}; // 创建一个新对象
  // 为新对象添加属性及其属性描述符
  Object.defineProperty(obj, 'prop', {
    value: 10,
    writable : true,
    enumerable : true,
    configurable : false
  });
  // 在configurable为false时重新更改属性描述符
  // 注意,如前所述,当将writable从true改为false是允许的
  Object.defineProperty(obj, 'prop', {
    enumerable : false
  });
  // Uncaught TypeError: Cannot redefine property: prop

  下面具体解释属性描述符的可选键值
  例子引用自MSDN:

Writable 属性

  当writable属性设置为false时,该属性被称为“不可写”。此时无法为属性赋一个新值。在非严格模式下,试图写入非可写属性不会改变它,也不会引发错误。

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

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

  console.log(o.a); // 37
  o.a = 25; // 此时不会抛出错误
  // 如果在严格模式下,即使重新赋的值与原来相同,仍旧会抛出错误
  console.log(o.a); // 37 重新赋值不会起作用

  // 严格模式下
  (function() {
    'use strict';
    var o = {};
    Object.defineProperty(o, 'b', {
      value: 2,
      writable: false
    });
    o.b = 3; // throws TypeError:  Cannot assign to read only property 'b' of object '#<Object>'
    return o.b; // 2
  }());
Enumerable 属性

  enumerable定义了对象的属性是否可以在 for...in循环和 Object.keys()中被枚举。

  var o = {};
  Object.defineProperty(o, "a", { value : 1, enumerable:true });
  Object.defineProperty(o, "b", { value : 2, enumerable:false });
  Object.defineProperty(o, "c", { value : 3 }); // enumerable 的默认值为false
  o.d = 4; // 如果使用直接赋值的方式创建对象的属性,则这个属性的enumerable为true

  for (var i in o) {
    console.log(i);  // 打印 'a' 和 'd'
  }

  console.log(Object.keys(o)); // ["a", "d"]
  // propertyIsEnumerable() 方法返回一个布尔值,表示指定的属性是否可枚举。
  o.propertyIsEnumerable('a'); // true
  o.propertyIsEnumerable('b'); // false
  o.propertyIsEnumerable('c'); // false
Configurable 属性

  configurable属性性表示对象的属性是否可以被删除,以及除writable属性外的其他属性是否可以被修改。

  var o = {};
  Object.defineProperty(o, "a", {
    get: function () {
      return 1;
    },
    configurable: false
  });

  // 抛出一个错误
  Object.defineProperty(o, "a", { configurable: true });
  // 抛出一个错误
  Object.defineProperty(o, "a", { enumerable: true });
  // 抛出一个错误 (set 熟悉之前未定义)
  Object.defineProperty(o, "a", {
    set: function () {
    }
  });
  // 抛出一个错误 (即使get属性做了与之前相同的事情)
  Object.defineProperty(o, "a", {
    get: function () {
      return 1;
    }
  });
  // 抛出一个错误
  Object.defineProperty(o, "a", { value: 12 });

  console.log(o.a); // 1
  delete o.a; // 不会起作用,严格模式下抛出错误TypeError: Cannot delete property 'a' of #<Object>
  console.log(o.a); // 1

  如果o.a的configurable属性为true,则不会抛出任何错误,并且该属性将在最后被删除。

set和get

  在了解了getter和setterd的原理和使用之后,这里也便理解了get和set属性的用法。
  同样使用MSDN上的例子,例子展示了如何实现一个自存档对象。 当设置temperature 属性时,archive 数组会获取日志条目。

  function Archiver() {
    var temperature = null;
    var archive = [];

    Object.defineProperty(this, 'temperature', {
      get: function () {
        console.log('get!');
        return temperature;
      },
      set: function (value) {
        temperature = value;
        archive.push({ val: temperature });
      }
    });

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

  var arc = new Archiver();
  arc.temperature; // 'get!'
  arc.temperature = 11;
  arc.temperature = 13;
  arc.getArchive(); // [{ val: 11 }, { val: 13 }]
需要注意的地方

使用点.运算符和Object.defineProperty()为对象的属性赋值时,数据描述符中的属性默认值是不同的,如下例所示。

  var o = {};
  o.a = 1;
  // 等同于
  Object.defineProperty(o, "a", {
    value: 1,
    writable: true,
    configurable: true,
    enumerable: true
  });

  // 另一方面,
  Object.defineProperty(o, "a", { value: 1 });
  // 等同于
  Object.defineProperty(o, "a", {
    value: 1,
    writable: false,
    configurable: false,
    enumerable: false
  });

相关的一些api

1.Object.defineProperties()的用法和Object.defineProperty()基本一样,不同的是可以为对象的多个属性添加描述符。

  var obj = {};
  Object.defineProperties(obj, {
    'prop1': {
      value: 1,
      writable: true
    },
    'prop2': {
      value: 2,
      writable: false
    }
  });

2.Object.getOwnPropertyDescriptor()方法返回指定对象上一个自有属性对应的属性描述符。(自有属性指的是直接赋予该对象的属性,不需要从原型链上进行查找)。在一开始举例的时候就用到了这个api。
3.Object.getOwnPropertyDescriptors()方法用来获取一个对象的所有自身属性的描述符。

Object.defineProperty()的重要意义

  个人认为理解掌握Object.defineProperty()对于JS从基础走向进阶具有帮助作用:
  1.了解框架原理。如Vue使用Object.defineProperty用于构成其响应式系统的一部分。
  2.阅读框架源码。Object.defineProperty是你在阅读许多框架源码无法绕过的一个api。
  3.了解babel语法转换器转译原理。babel的语法转换经常用到Object.defineProperty
  4.做更多你想做的事:)

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266

推荐阅读更多精彩内容