参考文章出处
本文作者:听风是风
本文链接:https://www.cnblogs.com/echolun/p/13121214.html
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
从零认识defineProperty
1. 基本用法与属性
让我们从基本概念说起,这里引用MDN解释:
Object.defineProperty
方法用于在对象上定义一个新属性,或者修改对象现有属性,并返回此对象。注意,请通过Object构造器调用此方法,而不是对象实例。
方法基本语法如下:
Object.defineProperty(obj, prop, descriptor)
OK,结合基本用法与概念,我们来试试添加属性与修改属性。
// 添加属性
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
包含属性较多,我们展开说。
2. descriptor 中的数据描述符
Object.defineProperty
方法中的 descriptor
属性繁多,所以它也非常强大,我们之前说的 数据劫持
,数据是否可写
,是否可删除
,是否可枚举都在这个 descriptor
中定义。
descriptor中属性包含6个
(参考上图),我将其分为了3类:
- 数据描述符类(
value
,writable
) - 存取描述符类(
get
,set
) - 能与数据描述符或者存取描述符共存的共有属性(
configurable
,enumerable
)。
让我们一一介绍它们。
2.1 value 和 writable
在对象添加属性以及修改属性时已经展示过 value
属性的作用了。所以这里直接从 writable
开始。
writable
是一个布尔值,若不定义默认为 false
,表示此条属性只可读,不可修改,举个例子:
let o = {};
Object.defineProperty(o, 'name', {
value: '听风是风',
writable: false
});
// 尝试修改name属性
o.name = '时间跳跃';
// 再次读取,结果并未修改成功
o.name;// 听风是风
注意,如果在严格模式下,修改 writable
属性为 false
的对象属性会报错。但如果将上述代码的writable
改为 false
就可以随意更改了。
而在MDN中关于writable
属性的描述为:
当该属性的
writable
键值为true
时,属性的值,也就是上面的value
,才能被赋值运算符
改变。
这里我做个知识补充,让 MDN 这句描述更为准确。
在面试时有时候会被问到,const
声明的变量是否可修改,准确来说可以改,分两种情况:
// 值为基本类型
const a = 1;
a = 2; // 报错 VM303:1 Uncaught TypeError: Assignment to constant variable.
// 值为复杂类型
const b = [1];
b = [1,2]; // 报错 VM313:1 Uncaught TypeError: Assignment to constant variable
// 修改引用类型里的某个属性的值,不会报错
const c = [1];
c[0] = 0; // 0
c; // [0]
如果我们 const
声明变量赋值是基本类型,只要修改值一定报错;如果值是引用类型,比如值是一个数组,当我们直接使用赋值运算符整个替换数组还是会报错,但如果我们不是整个替换数组而是修改数组中某个元素可以发现并不会报错。
这是因为对于引用数据类型而言,变量保存的是数据存放的引用地址,比如 b
的例子,原本指向是 [1]
的地址,后面直接要把地址改成数组 [1,2]
的地址,这很明显是不允许的,所以报错了。但在 c
的例子中,我们只是把 c
地址指向的数组中的一位元素改变了,并未修改地址,这对于 const 是允许的。
而这个特性对于writable
也是适用的,比如下面这个例子:
let o = {};
Object.defineProperty(o, 'age', {
value: [27],
writable: false
});
// 尝试修改name属性
o.age[0] = 18;
// 再次读取,修改成功
o.age; // 18
修改成功了,所以针对 MDN writable
为 true
才能被赋值运算符改变这句话不完整。
2.2 getter 和 setter
我们介绍了 descriptor
中的数据描述符相关的 vaule
与 writbale
,接着聊聊有趣的存取描述符,也就是在vue中也出现过 getter
、setter
方法。
我们知道,JavaScript中对象赋值与取值非常方便,有如下两种方式:
let o = {};
// 通过.赋值取值
o.name = 'echo';
//通过[]赋值取值,这种常用于key为变量情况
o['age'] = 27;
一个很直观的感受就是,对象赋值就是种瓜得瓜种豆得豆,我们给对象赋予了什么,获取的就是什么。那大家有没有想过这种情况,赋值时我提供 1
,但取值我希望是 2
。
巧了,这种情况我们就可以使用 Object.defineProperty()
中的存取描述符来解决这个需求。说直白点,存取描述符给了我们 赋值/取值
时 数据劫持
的机会,也就就是在赋值与取值时能自定义做一些操作,
-
getter
函数在获取属性值时触发,注意,是你为某个属性添加了getter
在获取这个属性才会触发,如果未定义则为undefined
,该函数的返回值将作为你访问的属性值。 -
setter
函数在设置属性时触发,同理你得为这个属性提前定义这个方法才行,设置的值将作为参数传入到setter
函数中,在这里我们可以加工数据,若未定义此方法默认也是undefined
。
OK,让我们用 getter
与 setter
模拟最常见的对象赋值与取值,看个例子:
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,到这里我们顺利学习了存取描述符 setter
与 getter
。
2.3 configurable 与 enumerable
最后,让我们了解剩余两个属性 configurable
与 enumerable
。
enumerable
值类型为 Boolean
,表示该属性是否可被枚举,啥意思?我们知道对象中有个方法 Object.keys()
用于获取对象可枚举属性,比如:
let o = {
name: '听风是风',
age: 27
};
Object.keys(o); // ['name','age']
通俗点来说,上面例子中的两个属性还是可以遍历访问的,但如果我们设置 enumerable
为 false
,就会变成这样:
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
特性表示对象的属性是否可以被删除,以及除 value
和 writable
特性外的其他特性是否可以被修改。
先说删除,看个例子:
let o = {
name: '听风是风'
};
Object.defineProperty(o, 'age', {
value: 27,
configurable: false
});
delete o.name; // true
delete o.age; // false
o.name; // undefined
o.age; // 27
删除好说,我们来看看它对于其它属性的影响,看个例子:
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时,这些属性都无法被重新定义以及修改。
2.4 其它注意点
那么到这里,我们把 descriptor
中所有属性都介绍完了,在使用中有几点需要强调,这里再汇总一下。
2.4.1
前面概念已经提出对象属性描述符要么是 数据描述符(value,writable)
,要么是 存取描述符(get,set)
,不应该同时存在两者描述符。
var o = {};
Object.defineProperty(o, 'name', {
value: '时间跳跃',
get() {
return '听风是风';
}
});
这个例子就会报错,其实不难理解,存取方法就是用来定义属性值的,value
也是用来定义值的,同时定义程序也不知道该以哪个为准了,所以用了 value/writable
其一,就不能用 get/set
了;不过 configurable
与 enumerable
这两个属性可以与上面两种属性任意搭配。
2.4.2
我们在前面已经说了各个属性是有默认值的,所以在用 Object.defineProperty()
时某个属性没定义不是代表没用这条属性,而是会用这条属性的默认值。
let o = {};
Object.defineProperty(o, 'name', {
value: '时间跳跃'
});
// 等同于
Object.defineProperty(o, 'name', {
value: '时间跳跃',
writable: false,
enumerable: false,
configurable: false
});
同理,以下代码也对等:
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
});
2.4.3
关于属性分类与默认值,如下表:
configurable | enumerable | value | writable | get | set | |
---|---|---|---|---|---|---|
数据描述符 | 可以 | 可以 | 可以 | 可以 | 不可以 | 不可以 |
存取描述符 | 可以 | 可以 | 不可以 | 不可以 | 可以 | 可以 |
默认值 | false | false | false | false | undefined | undefined |
现学现用,趁热打铁
使用 function 和 class 两种方案,写一个类 Person,可以设置年龄为正整数,年龄区段返回少年(0-20),中年(21-40)以及老年(其他)。
例如:
Person.age = 1;
console.log(Person.age);// '少年'
1. 使用 Object.defineProperty
答案:
function Person() {
// 初始化年龄
let age;
Object.defineProperty(this, "age", {
get() {
const ageRange = [41, 20, 0];
const level = ['老年', '中年', '少年'];
for (let i = 0; i < ageRange.length; i++) {
// 根据年纪大小返回对应范围
if (age >= ageRange[i]) {
return level[i];
};
};
},
set(val) {
// 年龄只保存正整数
val >= 0 ? age = val : null;
}
});
};
const p = new Person();
p.age = 1; // 1
console.log(p.age); // '少年'
p.age = 39; // 39
console.log(p.age); // '中年'
p.age = 41; // 41
console.log(p.age); // '老年'
2.在 function 原型上设置 set 和 get
function Person() {
// 初始化年龄
this.age_ = undefined;
};
// 在函数原型上定义age的get,set方法
Person.prototype = {
get age() {
const ageRange = [41, 20, 0];
const 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;
}
}
const p = new Person();
p.age = 1;
console.log(p.age); // '少年'
3. ES6 的 class 类上定义age的get,set方法
class Person {
constructor(age) {
// 这里就等同于我的第一个实现里面let age,是一个中间变量
this.age_ = undefined;
}
// ES6中,原型方法可直接定义在类中
get age() {
const 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;
}
};
const p = new Person();
p.age = 1;
console.log(p.age); //少年