属性描述符
为什么要学?
先看一段代码
let aGoods = {pic: '',title: '',desc: '',sellNumber: 1,favorRate: 2,price: 3
}class UIGoods {constructor(g) {this.data = g}
}let g = new UIGoods(aGoods)
console.log(g.data);
这段存在问题吗?
在功能这个层面, 是没问题的
在设计这个层面, 存在设计缺陷
// 按照我们的业务, data必须是一个商品对象
// 目前没有任何限制, 就可能埋下隐患, 带来更多的维护成本
g.data = 'abc'
console.log(g.data);
作为码农, 我们专心把功能实现了就可以了
作为架构师, 我们要打造 "玩不坏" 的程序, 要从根本上杜绝问题的发生
属性描述符
属性描述符就是用来描述属性的特征
let obj = {a: 1,b: 2
}/*** 描述a的特征*/
// 值为1
console.log(obj.a);
// 可重写
obj.a = 10;
// ->可遍历
for (let key in obj) {console.log(key);
}let keys = Object.keys(obj)
console.log(keys);/*** 获取属性描述符* { value: 10, writable: true, enumerable: true, configurable: true }* value描述值* writable是否可重写* enumerable是否可遍历* configurable属性描述对象是否可修改*/
let desc = Object.getOwnPropertyDescriptor(obj, 'a');
console.log(desc);/***设置属性描述符*/
Object.defineProperty(obj, 'a', {value: 10,writable: false, // 不可重写enumerable: false, // 不可遍历configurable: false, // 不可修改描述符本身
})
obj.a = 'abc'
console.log(obj.a); // 10/*** 认识访问器* 读取器和设置器加起来就是访问器*/
Object.defineProperty(obj, 'a', {// 读取器getterget: function () {console.log("hello");return 123;},// 设置器setterset: function (val) {console.log(val);}
})
obj.a = 3 + 2 //为属性赋值就是调用set()方法, 相当于set(3+2), 这里打印5
console.log(obj.a) //读取属性值就是调用get()方法, 相当于get(), 这里打印hello后再打印123
// 更复杂的示例
obj.a = obj.a + 2 // set(get() + 2) => set(打印hello返回123 + 2) => set(125) => 打印125
console.log(obj.a); // 打印hello再打印123/*** 使用访问器设置属性值*/
// 一个错误的示范: Maximum call stack size exceeded(栈内存益出)
// 报错的原因: 死循环
Object.defineProperty(obj, 'a', {get: function () {return obj.a // 等价于get(),一直调用自己},set: function (val) {obj.a = val // 等价于set(val),一直调用自己}
})
obj.a = obj.a + 2
console.log(obj.a);// 正确的示范: 利用临时变量
let temp = null
Object.defineProperty(obj, 'a', {get: function () {return temp},set: function (val) {temp = val}
})
obj.a = obj.a + 2 // set( get() + 2) => set( null + 2) => set(2)
console.log(obj.a); // get() => 2 => 打印2/*** 使用设置器模拟属性只读*/
Object.defineProperty(obj, 'a', {get: function () {return 123},set: function (val) {throw new Error(`你正在给obj.a这个属性赋值,但它是只读的`)}
})
obj.a = 100 //报错: 你正在给obj.a这个属性赋值,但它是只读的
改造代码
通过属性描述符改造我们的代码, 让代码是坚固可靠
let aGoods = {pic: '',title: '',desc: '',sellNumber: 1,favorRate: 2,price: 3
}/*** 原始程序*/
// class UIGoods {
// constructor(g) {
// this.data = g
// }
// }// let g = new UIGoods(aGoods)
// console.log(g.data);/*** 使用属性描述符改造程序* 目标: * 让data属性是只读的, 不会被篡改* 让choose属性的赋值更安全* 让totalPrice使用起来像属性* 保护原始数据不会被修改* 保护实例对象不被添加属性, 且自身属性可以不被影响* 保护对象原型不被添加属性或方法*/
class UIGoods {// es6提供了语法糖, 效果与下面的写法一致get isChoose() {return this.choose > 0}constructor(g) {g = { ...g } //克隆对象: 直接冻结原始对象, 会影响别人Object.freeze(g) // 冻结克隆对象, 保护对象的属性值不被更改Object.defineProperty(this, 'data', {get: function () {return g},set: function () {throw new Error('data属性是只读的')},configurable: false})let internalChooseValue = 0Object.defineProperty(this, 'choose', {configurable: false,get: function () {return internalChooseValue},set: function (val) {if (typeof val !== 'number') {throw new Error('choose属性是数字')}let temp = parseInt(val)if (temp !== val) {throw new Error('choose属性是整数')}if (val < 0) {throw new Error('choose属性必须 >= 0')}internalChooseValue = val}})Object.defineProperty(this, 'totalPrice', {get: function () {return this.choose * this.data.price}})// 不希望被人给自己加属性// 1.把自己冻结, 防止被添加属性, 弊端:其他不能被修改了// Object.freeze(this)// 2.对象密封: 不能添加属性, 但是对象已有的属性支持修改this.a = 1Object.seal(this)}
}Object.freeze(UIGoods.prototype) // 冻结原型, 防止随意添加东西let g = new UIGoods(aGoods)
/// g.data = 'abc' // 报错: data属性是只读的
g.choose = 2
console.log(g.totalPrice); // 6, 调用类似属性, 本质是函数g.data.peice = 100 // g.data只读, 但是原始数据还是可以篡改
console.log(g.data); // { ..., peice: 100 }g.abd = 123 // 别人还能往对象上加属性
console.log(g); // { abd: 123 }g.a = 100 // 不能添加属性, 但是原有属性支持修改
console.log(g); // { a: 100 }UIGoods.prototype.haha = 'abc' // 还能往原型上加东西
console.log(g.haha);