2 Likes
你可能不知道的 Object.defineProperty()

你可能不知道的 Object.defineProperty()

37 PV2 LikesJavaScript
最近在写一个 《JavaScript API 全解析》系列(刚写完 String,现正在写 Object,https://js.yanceyleo.com),想把 MDN 推荐使用的 API 全部撸一遍,也算是给自己准备一份资料。因为 Object.defineProperty() 涉及到的知识点比较复杂,所以单独拎出来放到这里,欢迎大家拍砖。

最近在写一个 《JavaScript API 全解析》系列(刚写完 String,现正在写 Object,可戳 JavaScript API 全解析),想把 MDN 推荐使用的 API 全部撸一遍,也算是给自己准备一份资料。因为 Object.defineProperty() 涉及到的知识点比较复杂,所以单独拎出来放到这里,欢迎大家拍砖。

语法

defineProperty(o: any, p: PropertyKey, attributes: PropertyDescriptor & ThisType<any>): any;

描述

用于在一个对象上定义新的属性或修改现有属性, 并返回该对象.

参数

  • o 目标对象

  • p 需要定义的属性或方法名 (可修改既有的, 也可添加新属性或方法)

  • attributes 属性描述符, 具体属性如下:

interface PropertyDescriptor { configurable?: boolean; enumerable?: boolean; value?: any; writable?: boolean; get?(): any; set?(v: any): void; }

属性描述符

ECMAScript 中有两种属性: 数据属性访问器属性.

数据属性包括: [[Configurable]], [[Enumerable]], [[Writable]], [[Value]]

访问器属性包括: [[Configurable]], [[Enumerable]], [[Get]], [[Set]]

属性描述符可同时具有的键值

configurableenumerablevaluewritablegetset
数据属性YesYesYesYesNoNo
访问器属性YesYesNoNoYesYes

简言之, 定义了 valuewritable , 一定不能有 getset, 反之亦然, 否则报错.

描述符可同时具有的键值
描述符可同时具有的键值

Configurable

如果某个属性的 configurable 为false, 那么:

  1. 将不能删除此属性, 即 delete obj.xxx 无效, 在严格模式下直接报错.
// 非严格模式下删除一个"不可配置"的属性会返回false const obj = {}; Object.defineProperty(obj, 'name', { value: 'yancey', configurable: false, }); delete obj.name; // false // obj.name并没有被删除 obj.name; // yancey
// 严格模式下删除一个"不可配置"的属性直接报错 (function() { 'use strict'; var o = {}; Object.defineProperty(o, 'b', { value: 2, configurable: false, }); delete o.b; // Uncaught TypeError: Cannot delete property 'b' of #<Object> return o.b; })();
  1. 当 enumerable 或 writable 是false时, 再次将它们变成true则报错; 但当它们是true时, 却可以把它们变成false ( 注意必须是在不可配置的前提下, 如果属性可配置, enumerable 和 writable 可任意切换 true 和 false)
const obj = {}; Object.defineProperty(obj, 'name', { value: 'yancey', configurable: false, writable: false, }); // 当"writable"和"configurable"均为false时, 尝试将"writable"变为true会报错 // Uncaught TypeError: Cannot redefine property: name Object.defineProperty(obj, 'name', { writable: true });
const obj = {}; Object.defineProperty(obj, 'name', { value: 'yancey', configurable: false, writable: true, }); // 但"writable"可成功从true切换到false Object.defineProperty(obj, 'name', { writable: false });
  1. 无论如何再次修改getset都会报错, 因为两者的属性值是一个函数,在 JS 中不可能存在一个相同的函数。

:::tip REVIEW 复杂数据类型在中存储数据名和一个堆的地址, 在中存储属性. 访问时先从栈获取地址, 再到堆中拿出相应的值. :::

const obj = {}; Object.defineProperty(obj, 'name', { value: 'yancey', configurable: false, }); // Uncaught TypeError: Cannot redefine property: name Object.defineProperty(obj, 'name', { get: function() {} }); // Uncaught TypeError: Cannot redefine property: name Object.defineProperty(obj, 'name', { set: function() {} });
  1. 只要writable 是 true, 可以任意重新定义 value, 但当writable是 false 时, 需要看具体数据类型. 第一个例子中, 虽然 configurable 是 false, 但只要 writable 是 true, 便可以重新定义 value; 第二个例子中, value 是 基本数据类型, 所以再次定义 value 时只要覆盖原值即可; 第三个例子 value 是复杂数据类型, 同样因为 堆栈 问题而不能重新赋值.
const obj = {}; Object.defineProperty(obj, 'name', { value: [], configurable: false, writable: true, }); // 任意重定义value不报错 Object.defineProperty(obj, 'name', { value: 123 }); // {name: 123} // 任意重定义value不报错 Object.defineProperty(obj, 'name', { value: {}); // {name: {}}
const obj = {}; Object.defineProperty(obj, 'name', { value: 123, configurable: false, writable: false, }); // 当value是基本数据类型, 用原值覆盖不会报错 Object.defineProperty(obj, 'name', { value: 123 }); // {name: 123} // 用其他值代替必然报错 Object.defineProperty(obj, 'name', { value: {}); // Uncaught TypeError: Cannot redefine property: name
const obj = {}; Object.defineProperty(obj, 'name', { value: [], configurable: false, writable: false, }); // 当value是复杂数据类型, 修改value必定报错, 同样是堆栈的原因 Object.defineProperty(obj, 'name', { value: [] }); // {name: 123}

Writable

如果某个属性的writable设为false, 那么该属性将不能被赋值运算符改变. 但属性值假如是数组时, 将不受 push, splice等方法的影响.

const obj = {}; Object.defineProperty(obj, 'hobby', { value: ['girl', 'music', 'sleep'], writable: false, configurable: true, enumerable: true, }); // "writable: false"并不对push、shift等方法起作用 obj.hobby.push('drink'); obj.hobby; // ['girl', 'music', 'sleep', 'drink'] - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // 当 hobby 被"赋值"给一个空数组时, 此属性的属性值不会被改变 obj.hobby = []; obj.hobby; // ['girl', 'music', 'sleep', 'drink'] - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // 而当使用"严格模式"时, 给一个"不可写"属性赋值将直接报错 (function() { 'use strict'; var o = {}; Object.defineProperty(o, 'b', { value: 2, writable: false }); o.b = 3; // throws TypeError: "b" is read-only return o.b; // 2 }());

Enumerable

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

const obj = { name: 'yancey', age: 18, say() { return 'say something...'; }, }; Object.defineProperty(obj, 'hobby', { value: ['girl', 'music', 'sleep'], enumerable: true, }); Object.defineProperty(obj, 'income', { value: '100,00,000', enumerable: false, }); // 以下迭代器均不能输出"不可枚举属性", 即 income 的相关信息 for (const i in obj) { console.log(obj[i]); } Object.keys(obj); Object.values(obj); Object.entries(obj);

Getter & Setter

Getter 为读取属性时调用的函数. Setter 为设置属性是调用的函数, Setter 会有一个参数, 即设置的那个值.

下面的代码创建一个 obj 对象, 定义了两个属性 name 和 _time, 注意 _time 的下划线是一个常用记号, 用于表示只能通过对象方法访问的属性. 而访问器属性 time 则包含一个 getter 函数和一个 setter 函数. getter 函数返回被修饰的 _time 的值, setter 则根据被设置的值修改 name. 因此当obj.time = 2, name 会变成我为长者+2s. 这是使用访问器属性的常见方式, 即设置一个属性的值会导致其他属性发生变化.

const obj = { name: '长者', _time: 1, }; Object.defineProperty(obj, 'time', { configurable: true, get() { return `default: ${this._time}s`; }, set(newValue) { if (Number(newValue)) { this._time = newValue; this.name = `我为${this.name}+${newValue}s`; } }, }); obj.time; // 'default: 1s' obj.time = 2; // 2 obj.name; // '我为长者+2s'

再看另一个例子, 通过 Object.defineProperty 劫持 obj.input, 将输入的值 set 到 id 为 name 的标签里. 这里便有了种 Vue.js 的味道, 推荐一篇文章 剖析 Vue 实现原理 - 如何实现双向绑定 mvvm.

<p>Hello, <span id='name'></span></p> <input type='text' id='input'> const obj = { input: '', }; const inputDOM = document.getElementById('input'); const nameDOM = document.getElementById('name'); inputDOM.addEventListener('input', function (e) { obj.input = e.target.value; }) Object.defineProperty(obj, 'input', { set: function (newValue) { nameDOM.innerHTML = newValue.trim().toUpperCase(); } })

MVVM?
MVVM?

最后看一个关于继承的例子, 我们创建了一个 Person 构造函数, 它包括两个参数: firstNamelastName, 此构造函数暴露出四个属性: firstName, lastName, fullName, species, 我们想让前三个属性动态变化, 最后一个属性是一个常量而不允许变化.

下面这段代码显然没有达到想要的效果: 在尝试修改 firstNamelastName 时, fullName 并没有实时被更新; species属性能随意被改变.

function Person(firstName, lastName) { this.firstName = firstName; this.lastName = lastName; this.fullName = this.firstName + ' ' + this.lastName; this.species = 'human'; } const person = new Person('Yancey', 'Leo'); // 虽然 firstName 和 lastName 被修改了, 但 fullName 仍然是 "Yancey Leo" person.firstName = 'Sayaka'; person.lastName = 'Yamamoto'; // 我们定义了一个关于“人”的构造函数, 所以并不希望 species 被修改成 fish person.species = 'fish'; // 当我们修改了 fullName, 也同样希望 firstName 和 lastName 被更新 person.fullName = 'Kasumi Arimura';

所以我们使用 Object.defineProperty() 重写这个例子. 需要注意的是: 被劫持的属性应放在原型里. 通过下面这种方式, 即使创建多个实例, 也不会冲突, 所以可以放心使用.

function Person(firstName, lastName) { this.firstName = firstName; this.lastName = lastName; } Object.defineProperty(Person.prototype, 'species', { value: 'human', writable: false, }); Object.defineProperty(Person.prototype, 'fullName', { get() { return this.firstName + ' ' + this.lastName; }, set(newValue) { const newValueArr = newValue.trim().split(' '); if (newValueArr.length === 2) { this.firstName = newValueArr[0]; this.lastName = newValueArr[1]; } }, }); const person = new Person('Yancey', 'Leo'); person.firstName = 'Sakaya'; person.lastName = 'Yamamoto'; person.fullName; // 'Sayaka Yamamoto' person.fullName = 'Kasumi Arimura'; person.firstName; // 'Kasumi' person.lastName; // 'Arimura' person.species = 'fish'; person.species; // 'human'

扩展

除了 Object.defineProperty() 中的 Getter 和 Setter, 还有两种类似的方式.

__defineGetter__ 和 __defineSetter__()

__defineGetter__ 方法可以为一个已经存在的对象设置 (新建或修改) 访问器属性, __defineSetter__ 方法可以将一个函数绑定在当前对象的指定属性上, 当那个属性被赋值时, 你所绑定的函数就会被调用.

var o = {}; o.__defineGetter__('gimmeFive', function() { return 5; }); o.gimmeFive; // 5

:::danger 该特性是非标准的, 请尽量不要在生产环境中使用它!

该特性已经从 Web 标准中删除, 虽然一些浏览器目前仍然支持它, 但也许会在未来的某个时间停止支持, 请尽量不要使用该特性. :::

对象字面量中的 get 语法

对象字面量中的 get 语法只能在新建一个对象时使用.

var o = { get gimmeFive() { return 5; }, }; o.gimmeFive; // 5

参考

Vue 核心之数据劫持

不会 Object.defineProperty 你就 out 了

vue.js 关于 Object.defineProperty 的利用原理

面试官: 实现双向绑定 Proxy 比 defineproperty 优劣如何?

JAVASCRIPT ES5: MEET THE OBJECT.DEFINEPROPERTY() METHOD

深入理解并手写遵循 Promise/A+ 规范的 Promise

PREVIOUS POST

深入理解并手写遵循 Promise/A+ 规范的 Promise

最后一次弄懂 Event Loop

NEXT POST

最后一次弄懂 Event Loop

    Search by