场景引入
当前有一个对象,你需要对该对象进行封装。举例来说,对象可以是简单的商品对象数据:
1 2 3 4 5 6 7 8
| const goods = { pic: '/img/1.webp', title: 'simple title', desc: 'simple desc', sellNumber: 1, favorRate: 20, price: 20, };
|
自然的,你为了更方便的实现业务,会为它加上一些缺失的属性,比如选择的商品数量和总价,但是为了不更改原始对象数据 goods,所以你使用 class 来进行封装:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| const goods = { pic: '/img/1.webp', title: 'simple title', desc: 'simple desc', sellNumber: 1, favorRate: 20, price: 20, };
class Goods { constructor(goods) { this.data = goods; this.chooseNum = 0; this.totalPrice = 0; } } const g = new Goods(goods); console.log(g);
|
然后,你意识到 totalPrice 可以由 data 和 chooseNum 算出来,所以你为了避免将来因为忘记有这回事,更新了选中商品数量却忘记更新总价数据,导致数据不一致的问题出现,选择去除这个会造成数据冗余的属性 totalPrice,而是将它改为函数:
1 2 3 4 5 6 7 8 9 10 11 12
| class Goods { constructor(goods) { this.data = goods; this.chooseNum = 0; } getTotalPrice() { return this.chooseNum * this.data.price; } } const g = new Goods(goods); console.log(g.getTotalPrice());
|
终于,现在封装的像样子了…… 吗?
发散地想,随着业务进行,代码量越来越多,甚至要和其他人一起协作完成。此时,有人使用到了这个 g,并且在不注意的情况下,直接更改了 g 的 data 属性呢?比如:
1 2 3 4 5 6 7 8 9 10 11 12 13
| class Goods { constructor(goods) { this.data = goods; this.chooseNum = 0; } getTotalPrice() { return this.chooseNum * this.data.price; } } const g = new Goods(goods); g.data = 'AAA建材招租'; console.log(g.getTotalPrice());
|
此时,调用的 g.getTotalPrice() 运行出错,再也得不到正确的结果,一个方法如此,如果有多个依靠 data 的方法,那情况更灾难了。我这里举的例子比较夸张,知道意思即可。
实际上,我们希望的效果肯定是原始数据不能改动,因为很多东西都是在这个数据的基础上建立起来的。
属性描述符
上述情况其实就是写库、写框架的人时刻在考虑的事:时刻考虑各种可能发生的 "神奇操作",并在代码上杜绝错误或不期望的操作生效,使得最终效果为期望效果。那么,属性描述符能一定程度上解决这种问题。
什么是属性描述符
假设有这样一个简单对象:
我们来想想,平时都是怎么描述一个对象的某个属性?
值为多少?可遍历?可重写?这些其实就是属性描述的一部分。那么,在代码表现上,怎么知道一个对象属性的描述?
JS 在这方面提供的函数 API 是 getOwnPropertyDescriptor:
1 2 3 4
| const desc = Object.getOwnPropertyDescriptor(obj, 'a') console.log(desc);
|
得到结果的是对象,它描述的就是对象 obj 的属性 a。这个对象内的属性,描述的意思都很简单:
value:属性值
writable:属性是否可重写
enumerable:属性是否可遍历
configurable:属性描述是否可再次配置
这就是说,对象的每一个属性都有一套属性描述符来描述。其中,第四个描述属性 configurable 决定了某个对象属性的属性描述符是否可以再次被配置。那么,如何配置一个属性的属性描述符呢?
JS 提供的函数 API 是 defineProperty:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| const obj = { a: 1, } Object.defineProperty(obj, 'a', { value: 2, writable: false, enumerable: true, configurable: false, }) obj.a = 3; Object.defineProperty(obj, 'a', { value: 4, }) console.log('obj.a:', obj.a);
|
当我们将 configurable 设为 false 之后,就无论如何都无法更改某个属性的属性描述符,更改则触发报错。
只有 configurable 会触发报错,其他属性描述符若存在冲突,不会触发任何显式提示
这时,我们回过头来看引入部分的场景,我们是不是可以对原始数据进行 "上锁" 了?
访问器
预期:原始数据无法被更改,并且在数据操作和我的预期有冲突时,"显式的提示" 给编程人员。
为了实现预期效果,上面的四个属性描述符是不够的,还需要认识两个属性描述符的配置:get() 和 set(),示例如下:
1 2 3 4
| Object.defineProperty(obj, 'b', { get() {}, set() {}, })
|
get() 和 set() 整体称为访问器。
一旦设置了 get(),那么将来读取这个被设置了属性描述符的属性时,就不再会去内存中寻找,而是直接运行 get() 函数。也就是说,console.log(obj.a) 等效于 console.log(get()),打印的就是 get() 函数的运行结果。例如:
1 2 3 4 5 6 7 8 9
| Object.defineProperty(obj, 'b', { get() { return 'aHa'; }, set() {}, }) console.log('obj.b:', obj.b);
|
对于 set() 也是如此,当赋值发生时,逻辑变成直接运行 set() 函数。例如:
1 2 3 4 5 6 7 8 9 10 11 12 13
| let tempVal = undefined; Object.defineProperty(obj, 'b', { get() { return tempVal; }, set(val) { tempVal = val; }, }) obj.b = 'vamos'; console.log('obj.b:', obj.b);
|
于是,借助 get() 和 set() 就可以实现我们的预期效果了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80
| const goods = { pic: '/img/1.webp', title: 'simple title', desc: 'simple desc', sellNumber: 1, favorRate: 20, price: 20, };
class Goods { constructor(goods) { goods = { ...goods } Object.freeze(goods)
Object.defineProperty(this, 'data', { configurable: false, get() { return goods; }, set() { throw new Error('data 属性只读,不可修改'); }, }) let internalData = 0; Object.defineProperty(this, 'chooseNum', { configurable: false, get() { return internalData; }, set(val) { if (typeof val !== 'number') { throw new Error('chooseNum 属性必须是数字'); } if (val < 0) { throw new Error('chooseNum 属性必须是大于等于0的数字'); } let tempNum = parseInt(val) if (tempNum !== val) { throw new Error('chooseNum 属性必须是整数'); } internalData = val; } }) Object.defineProperty(this, 'totalPrice', { configurable: false, get() { return this.data.price * this.chooseNum; } }) this.example = 'example'; Object.seal(this); } } const g = new Goods(goods); g.data.favorRate = 99; g.chooseNum = 2; g.ak = 100; g.example = 'new example'; console.log('g.data: ', g.data); console.log('g.chooseNum: ', g.chooseNum); console.log('g.totalPrice: ', g.totalPrice); console.log('g.ak: ', g.ak); console.log('g.example: ', g.example);
|