Loading [MathJax]/jax/input/TeX/config.js
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >彻底搞懂Object.defineProperty

彻底搞懂Object.defineProperty

作者头像
zz_jesse
发布于 2021-01-06 07:19:24
发布于 2021-01-06 07:19:24
1.8K00
代码可运行
举报
文章被收录于专栏:前端技术江湖前端技术江湖
运行总次数:0
代码可运行
  • 本文作者:听风是风
  • 本文链接:https://www.cnblogs.com/echolun/p/13121214.html

前言

早在大半年前,掘金某位用户分享的面试题整理中有一题,简述let与const区别,你能自己模拟实现它们吗?,题目意思大概如此,时间久远我也很难找到那篇文章,当时看到此题对于const实现我的想法就是有个writable属性可以定义值是否可以修改,不过也只是脑中一闪,并未细究。

半个月前,前前同事发了一份深圳某公司的笔试题我,整体题目不难(不难是指每题都知道考的什么知识点,脑中都能想到该用什么去解决,但知识不一定很精通),其中有一道手写编程题,题目描述如下:

使用function和class两种方案,写一个类Person,可以设置年龄为正整数,年龄区段返回少年(0-20),中年(21-40)以及老年(其他)。

例如:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
Person.age = 1;
console.log(Person.age);// '少年'

在我印象里JavaScript对象是可以用gettersetter来解决这个问题的,存一个数字进去,取的时候根据数字范围返回对应年龄段,我只是说了我的想法,并未真正去实现它,因为我对于这两个方法也只是有点印象而已。

昨天,在我通读vue文档过程中,一篇名为深入响应式原理吸引了我的注意,文中简述了vue数据响应式的原理,以及在操作数组与对象时需要注意的点,在实现上vue也使用了Object.defineProperty方法,联想到vue计算属性的gettersetter,我想是时候弄懂这个API了,那么请各位跟随我的脚步,好好认识这个在JavaScript中高频出现的API,本文开始。

从零认识defineProperty

基本用法与属性

让我们从基本概念说起,这里引用MDN解释:

Object.defineProperty方法用于在对象上定义一个新属性,或者修改对象现有属性,并返回此对象。注意,请通过Object构造器调用此方法,而不是对象实例。

方法基本语法如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
Object.defineProperty(obj, prop, descriptor)

OK,结合基本用法与概念,我们来试试添加属性与修改属性。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 添加属性
let o = {};
Object.defineProperty(o, 'name', {value:'echo'});
o.name;// 'echo'

// 修改现有属性
o.age = 27;
// 重返18岁
Object.defineProperty(o, 'age', {value:18});
o.age;// 18

通过上面的例子演示我们可知,语法中的obj是我们要添加/修改属性的对象,prop是我们希望添加/修改的属性名,而descriptor是我们添加/修改属性的具体描述,descriptor包含属性较多,我们展开说。

descriptor中的数据描述符

Object.defineProperty方法中的descriptor属性繁多,所以它也非常强大,我们之前说的数据劫持,数据是否可写,是否可删除,是否可枚举都在这个descriptor中定义。在介绍每个属性前,我们还得引入一个新概念,即:

对象里目前存在的属性描述符有两种主要形式:数据描述符存取描述符数据描述符是一个具有值的属性,该值可以是可写的,也可以是不可写的。存取描述符由 getter 函数和 setter 函数所描述的属性。一个描述符只能是这两者其中之一,不能同时是两者。

descriptor中包含的属性也分为了两类,怕大家弄混淆,这里我用图分个类:

descriptor中属性包含6个(参考上图),我将其分为了3类,数据描述符类(value,writable),存取描述符类(get,set),以及能与数据描述符或者存取描述符共存的共有属性(configurable,enumerable)。

让我们一一介绍它们,在对象添加属性以及修改属性时已经展示过value属性的作用了,所以这里直接从writable开始。

writable是一个布尔值,若不定义默认为false,表示此条属性只可读,不可修改,举个例子:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
let o = {};
Object.defineProperty(o, 'name', {
    value: '听风是风',
    writable: false
});
// 尝试修改name属性
o.name = '时间跳跃';
// 再次读取,结果并未修改成功
o.name;// 听风是风

注意,如果在严格模式下,修改writable属性为false的对象属性会报错。但如果将上述代码的writalbe改为false就可以随意更改了。

而在MDN中关于writable属性的描述为:

当该属性的 writable 键值为 true 时,属性的值,也就是上面的 value,才能被赋值运算符改变。

这里我做个知识补充,让MDN这句描述更为准确。

在面试时有时候会被问到,const声明的变量是否可修改,准确来说可以改,分两种情况:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 值为基本类型
const a = 1;
a = 2;// 报错

// 值为复杂类型
const b = [1];
b = [1,2];// 报错

const c = [1];
c[0] = 0;
c;// [0]

如果我们const声明变量赋值是基本类型,只要修改值一定报错;如果值是引用类型,比如值是一个数组,当我们直接使用赋值运算符整个替换数组还是会报错,但如果我们不是整个替换数组而是修改数组中某个元素可以发现并不会报错。

这是因为对于引用数据类型而言,变量保存的是数据存放的引用地址,比如b的例子,原本指向是[1]的地址,后面直接要把地址改成数组[1,2]的地址,这很明显是不允许的,所以报错了。但在c的例子中,我们只是把c地址指向的数组中的一位元素改变了,并未修改地址,这对于const是允许的。

而这个特性对于writable也是适用的,比如下面这个例子:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
let o = {};
Object.defineProperty(o, 'age', {
    value: [27],
    writable: false
});
// 尝试修改name属性
o.age[0] = 18;
// 再次读取,修改成功
o.age; // 18

你看,修改成功了,所以针对MDNwritable为true才能被赋值运算符改变这句话不一定正确,比如上个例子我们就是用赋值运算符修改了数组索引为0的一项的值,具体问题具体看待,这里做个补充。

descriptor中的存取描述符

OK,我们介绍了descriptor中的数据描述符相关的vaulewritbale,接着聊聊有趣的存取描述符,也就是在vue中也出现过getter、setter方法。

我们知道,JavaScript中对象赋值与取值非常方便,有如下两种方式:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
let o = {};
// 通过.赋值取值
o.name = 'echo';
//通过[]赋值取值,这种常用于key为变量情况
o['age'] = 27;

一个很直观的感受就是,对象赋值就是种瓜得瓜种豆得豆,我们给对象赋予了什么,获取的就是什么。那大家有没有想过这种情况,赋值时我提供1,但取值我希望是2。巧了,这种情况我们就可以使用Object.defineProperty()中的存取描述符来解决这个需求。说直白点,存取描述符给了我们赋值/取值时数据劫持的机会,也就就是在赋值与取值时能自定义做一些操作,

getter函数在获取属性值时触发,注意,是你为某个属性添加了getter在获取这个属性才会触发,如果未定义则为undefined,该函数的返回值将作为你访问的属性值。

setter函数在设置属性时触发,同理你得为这个属性提前定义这个方法才行,设置的值将作为参数传入到setter函数中,在这里我们可以加工数据,若未定义此方法默认也是undefined。

OK,让我们用gettersetter模拟最常见的对象赋值与取值,看个例子:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
let o = {};
o.name = '听风是风';
o.name; // '听风是风'

//使用get set模拟赋值取值操作
let age;
Object.defineProperty(o, 'age', {
    get() {
        // 直接返回age
        return age;
    },
    set(val) {
        // 赋值时触发,将值赋予变量age
        age = val;
    }
});
o.age = 18;
o.age; // 18

在上面例子模拟中,只要为o赋值setter就会触发,并将值赋予给age,那么在读取值getter直接返回变量age即可。

OK,到这里我们顺利学习了存取描述符settergetter

descriptor中的共有属性

最后,让我们了解剩余两个属性configurableenumerable

enumerable值类型为Boolean,表示该属性是否可被枚举,啥意思?我们知道对象中有个方法Object.keys()用于获取对象可枚举属性,比如:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
let o = {
    name: '听风是风',
    age: 27
};
Object.keys(o); // ['name','age']

通俗点来说,上面例子中的两个属性还是可以遍历访问的,但如果我们设置enumerable为false,就会变成这样:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
let o = {
    name: '听风是风'
};
Object.defineProperty(o, 'age', {
    value: 27,
    enumerable: false
});
// 无法获取keys
Object.keys(o); // ['name']
// 无法遍历访问
for (let i in o) {
    console.log(i); // 'name'
};

configurable的值也是Boolean,默认是false,configurable 特性表示对象的属性是否可以被删除,以及除 valuewritable 特性外的其他特性是否可以被修改。

先说删除,看个例子:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
let o = {
    name: '听风是风'
};
Object.defineProperty(o, 'age', {
    value: 27,
    configurable: false
});

delete o.name;//true
delete o.age;//false

o.name;//undefined
o.age;//18

删除好说,我们来看看它对于其它属性的影响,看个例子:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
var o = {};
Object.defineProperty(o, 'name', {
    get() {
        return '听风是风';
    },
    configurable: false
});
// 报错,尝试通过再配置修改name的configurable失败,因为已经定义过了configurable
Object.defineProperty(o, 'name', {
    configurable: true
});

//报错,尝试修改name的enumerable为true,失败,因为未定义默认为false
Object.defineProperty(o, 'name', {
    enumerable: true
});

//报错,尝试新增set函数,失败,一开始没定义set默认为undefined
Object.defineProperty(o, 'name', {
    set() {}
});

//尝试再定义get,报错,已经定义过了
Object.defineProperty(o, 'name', {
    get() {
        return 1;
    }
});

// 尝试添加数据描述符中的vaule,报错,数据描述符无法与存取描述符共存
Object.defineProperty(o, 'name', {
    value: 12
});

由于前面我们说了,未定义的属性虽然没用代码写出来,但它们其实都有了默认值,当configurable为false时,这些属性都无法被重新定义以及修改。

其它注意点

那么到这里,我们把descriptor中所有属性都介绍完了,在使用中有几点需要强调,这里再汇总一下。

前面概念已经提出对象属性描述符要么是数据描述符(value,writable),要么是存取描述符(get,set),不应该同时存在两者描述符。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
var o = {};
Object.defineProperty(o, 'name', {
    value: '时间跳跃',
    get() {
        return '听风是风';
    }
});

这个例子就会报错,其实不难理解,存取方法就是用来定义属性值的,value也是用来定义值的,同时定义程序也不知道该以哪个为准了,所以用了value/writable其一,就不能用get/set了;不过configurableenumerable这两个属性可以与上面两种属性任意搭配。

我们在前面已经说了各个属性是有默认值的,所以在用Object.defineProperty()时某个属性没定义不是代表没用这条属性,而是会用这条属性的默认值。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
let o = {};
Object.defineProperty(o, 'name', {
    value: '时间跳跃'
});

//等同于
Object.defineProperty(o, 'name', {
    value: '时间跳跃',
    writable: false,
    enumerable: false,
    configurable: false
});

同理,以下代码也对等:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
var o = {};
o.name = '听风是风';

//等同于
Object.defineProperty(o, 'name', {
    value: '听风是风',
    writable: true,
    enumerable: true,
    configurable: true
});

//等同于
let name = '听风是风';
Object.defineProperty(o, 'name', {
    get() {
        return name;
    },
    set(val) {
        name = val;
    },
    enumerable: true,
    configurable: true
});

关于属性分类与默认值,如下表:

configurable

enumerable

value

writable

get

set

数据描述符

可以

可以

可以

可以

不可以

不可以

存取描述符

可以

可以

不可以

不可以

可以

可以

默认值

false

false

false

false

undefined

undefined

现学现用,趁热打铁

那么到这里,我们详细介绍了Object.defineProperty相关属性与用法,趁热打铁,我们活用它来解决一些问题。原本我想通过模拟vue数据双向绑定,模拟const以及解决文章开头面试题,但碍于文章篇幅确实过长了,const模拟大家感兴趣可自行百度,vue数据双向绑定我会另起一篇文章,所以这里就来解决文章开头的题目好了。

我们提取题目细节,年龄只接受正整数(在set中判断),毕竟没人是负年龄,其次对应范围有对应的年龄段,根据年龄返回对应年龄段即可(在get中操作);

这里直接上function的实现:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
function Person() {
    // 初始化年龄
    let age;
    Object.defineProperty(this, "age", {
        get() {
            let ageRange = [41, 20, 0],
                level = ['老年', '中年', '少年'];
            for (let i = 0; i < ageRange.length; i++) {
                // 根据年纪大小返回对应范围
                if (age >= ageRange[i]) {
                    return level[i];
                };
            };
        },
        set(val) {
            // 年龄只保存正整数
            val >= 0 ? age = val : null;
        }
    });
};

let p = new Person();
p.age = 1;
console.log(p.age); // '少年'
p.age = 39;
console.log(p.age); // '中年'
p.age = 41;
console.log(p.age); // '老年'

值得一提的是,实现代码中我们将需要年龄与相关返回值配置成了数组,而非常理上的if...else if...,这样做的好处是即便修改年龄或者增加年龄范围,我们要做的也仅仅是修改数组配置即可,而不需要对逻辑层中添加更多的if...else。更多条件判断优雅写法欢迎阅读博主这篇文章 提升代码幸福度,五个技巧减少js开发中的if else语句

为什么我不用ES6的class类来实现上面的操作了,因为公司不允许使用ES6,去年学的关于类好多都忘记了...整理这篇文章也花了好长时间,脑袋有点沉,这个改写就留给各位强大的网友吧。

那么到这里,关于Object.defineProperty的介绍就结束了。

补充

关于上面这道题,考察的虽然是Object.definedProperty的getter与setter,不过出题人的本意不是希望这么用的,任何对象在定义时候可以添加get,set方法,比如:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
let p = {
    age_: 27,
    name: 'echo',
    get age() {
        return this.age_;
    },
    get name() {
        return '听风是风'
    }
};
p.name; // 听风是风
p.age; // 27

那么知道了这一点,我们来按照出题人的本意来分别实现上面的题目,首先是function情况:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
function Person() {
    // 初始化年龄
    this.age_ = undefined;

};
// 在函数原型上定义age的get,set方法
Person.prototype = {
    get age() {
        let ageRange = [41, 20, 0],
            level = ['老年', '中年', '少年'];
        for (let i = 0; i < ageRange.length; i++) {
            // 根据年纪大小返回对应范围
            if (this.age_ >= ageRange[i]) {
                return level[i];
            };
        };
    },
    set age(val) {
        // 年龄只保存正整数
        val >= 0 ? this.age_ = val : null;
    }
}

let p = new Person();
p.age = 1;

其次是ES6的class类:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class Person {
    constructor(age) {
        // 这里就等同于我的第一个实现里面let age,是一个中间变量
        this.age_ = undefined;
    }
    // ES6中,原型方法可直接定义在类中
    get age() {
        let ageRange = [41, 20, 0],
            level = ['老年', '中年', '少年'];
        for (let i = 0; i < ageRange.length; i++) {
            // 根据年纪大小返回对应范围
            if (this.age_ >= ageRange[i]) {
                return level[i];
            };
        };
    }
    set age(age) {
        age >= 0 ? this.age_ = age : null;
    }

};

var p = new Person();
p.age = 1;
console.log(p.age); //少年

OK,这样又有一部分知识串起来了,贼开心!

点个『在看』支持下

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-12-29,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端技术江湖 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
双向数据绑定中重要属性-Object.defineProperty()详解
给这个对象添加一个fullName属性,fullName的值为obj.firstName-obj.lastName
用户10106350
2022/10/28
9410
双向数据绑定中重要属性-Object.defineProperty()详解
深入浅出Object.defineProperty()
本文部分参考了书籍《你不知道的javascript》上卷 对象的定义与赋值 经常使用的定义与赋值方法obj.prop =value或者obj['prop']=value Object.defineP
IT架构圈
2018/06/01
7453
Object.defineProperty方法详解
Object.defineProperty() 方法在 JavaScript 中被用来直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象。这个方法允许你精确地控制属性在对象上的行为,包括属性的值、可写性、可枚举性和可配置性。
jack.yang
2025/04/05
1860
使用 Object.defineProperty 为对象定义属性
目前前端开发中比较流行的两个框架: Angular 和 Vue 都采用了数据双向绑定的技术。 Angular1 中数据双向绑定是通过「脏检测」的方式实现,每当数据发生变更,对所有的数据和视图的绑定关系进行一次检测,识别是否有数据发生了变化以及这个变化是否会影响其它数据的变化,然后将变更的数据发送到视图,更新页面展示。
李振
2021/11/26
1.2K0
【面试题解】Object.defineProperty 都能 \&#34;define\&#34; 什么?
可以看到,我分别打印了原始对象,修改属性后的对象,添加属性后的对象。如果第二个参数 prop 存在,则是修改属性操作,如果 prop 不存在,则是添加属性操作。
一尾流莺
2022/12/10
3350
【面试题解】Object.defineProperty 都能 \&#34;define\&#34; 什么?
聊聊 Object.defineProperty()
如果我们直接为对象添加一个属性,比如 obj.a = 10 我们说 a 是 普通属性,他的值既可以被改变,也可以被删除,还可以被for..in 或 Object,keys 枚举遍历。
mafeifan
2019/06/17
7140
js Object.defineProperty()详解
要修改属性的默认特性,就必须使用 Object.defineProperty()方法 ;在了解Object.defineProperty()之前,需要先明白对象属性的一些特性,明白了这些特性之后,对Object.defineProperty()的学习就会顺利很多了。
IT工作者
2022/05/12
2.7K0
JavaScript object.defineproperty 学习
Object.defineProperty 是 JavaScript 中的一个方法,用于直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。这个方法允许你精确地控制属性的特性,比如是否可写、是否可枚举、是否可配置等。
高老师
2024/12/24
1130
【春节日更】理解Object.defineProperty方法
经常听到使用Object.defineProperty 数据劫持,它是如何实现劫持的呢?除了数据劫持还可以做啥呢?今天,就来详细认识一下它
用户9914333
2022/07/22
2850
【春节日更】理解Object.defineProperty方法
[OHIF-Viewers]医疗数字阅片-医学影像-Module: Panel-自定义面板-中-es6-Object.defineProperty()定义属性
Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
landv
2020/07/17
7650
Object.definePropety
Object.defineProperty()方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象,也就是说,该方法允许精确地添加或修改对象的属性。
WindRunnerMax
2020/08/27
5650
不会Object.defineProperty你就out了
IMWeb前端团队
2017/12/29
1.2K0
JavaScript数据属性和访问器属性
看《深入理解JavaScript》的this篇时看到“访问器属性”这个不熟悉的名词,百度后找到两篇感觉比较合适的文章,整合记录一下,以参考资料2为主,参考资料1为辅助补充。
WindCoder
2018/09/20
1.9K0
JS面向对象
表示能否通过delete删除此属性,能否修改属性的特性,或能否修改把属性修改为访问器属性,如果直接使用字面量定义对象,默认值为true
4O4
2022/04/25
8.5K0
ES6 系列之 defineProperty 与 proxy
我们或多或少都听过“数据绑定”这个词,“数据绑定”的关键在于监听数据的变化,可是对于这样一个对象:var obj = {value: 1},我们该怎么知道 obj 发生了改变呢?
夜尽天明
2019/07/17
5570
详解JavaScript之神奇的Object.defineProperty
摘要: JavaScript有个很神奇的Object.defineProperty(),了解一下?
Fundebug
2018/07/05
6410
01 - vue源码解析之vue 数据绑定实现的核心 Object.defineProperty()
**通过结果我们发现, enumerable属性值为true 时可以被枚举, 不为true时是不可以被枚举的 **
全栈若城
2024/02/29
1340
01 - vue源码解析之vue 数据绑定实现的核心 Object.defineProperty()
Decorator 从原理到实践
ES6 已经不必在过多介绍,在 ES6 之前,装饰器可能并没有那么重要,因为你只需要加一层 wrapper 就好了,但是现在,由于语法糖 class 的出现,当我们想要去在多个类之间共享或者扩展一些方法的时候,代码会变得错综复杂,难以维护,而这,也正式我们 Decorator 的用武之地。
Nealyang
2019/09/29
5810
Decorator 从原理到实践
17Object对象
Javascript提供了一个内部数据结构,用于描述对象的值,控制其行为,例如该属性是否可写、是否可配置、是否可修改以及是否可枚举等。这个内部数据结构被称为“属性描述符”。
Dreamy.TZK
2020/05/27
6840
代码世界的构建有一个不可或缺的支柱(如何让代码更加稳健)
最近学JavaScript的过程中,有幸学到了一个新的知识点----属性描述符,尽管它在业务代码中可能并不常见,但是许多库源码中都有它的身影,比如vue
一枕眠秋雨
2024/03/11
1550
相关推荐
双向数据绑定中重要属性-Object.defineProperty()详解
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验