我们常听说vue是用getter与setter实现数据监控的,那么getter与setter到底是什么东西,它与defineProperty是什么关系,平时有哪些用处呢?本文将为大家一一道来。
对象的属性
按照一贯的“由浅到深”行文原则,我们先温习一下对象的属性。我们知道对象有自身的属性以及原型上的属性,它们都可以通过obj.key这样的方式访问到。
要设置/修改对象的属性也是很简单的,只需obj.key='value'即可。要注意的是,如果key位于原型上,那么此时会在对象自身设置该值,而不是修改原型上的。
另外需要注意的是,原型上的属性有时候会被for in给“不小心”遍历出来,例如下面的代码:
var arr = [1,2,3];
arr.__proto__.test = 4;
for(i in arr){
console.log(arr[i]);
}
//输出:1234
所以我们一般在用for in的时候都要加上hasOwnProperty判断,或者是抛弃for in,用forEach.
认识defineProperty
defineProperty是挂载在Object上的一个方法,作用是:为对象定义一个属性,或是修改已有属性的值,并设置该属性的描述符。该方法返回修改后的对象。
如果没有后半句作用的话,那它与obj.key = 'value'这种赋值语句没什么两样。他的完整语法是这样:Object.defineProperty(obj, prop, descriptor)
obj: 目标对象
prop: 属性名称
descriptor: 属性描述符
前两个就不必讲了,需要重点理解的是第三参数。属性描述符用于定义该属性的一些特性。具体来讲分了两类:数据描述符(data descriptor)、访问描述符(accessor descriptor).
这两类描述符有两个必选项:
-
configurable
从字面意思看它表示“可配置”,含义是:当它为true时,该属性的描述符可被修改,并且该属性可被delete删除。同理,当它为false时,我们无法再次调用defineProperty去修改描述符,也不可通过delete删除。 -
enumerable
从字面意思看它表示“可枚举”,含义是:当它为true时,该属性可被迭代器枚举出来。比如使用for in或者是Object.keys。
接下来就是数据描述符(data descriptor)了,有两个:
-
value
这个就是该属性的值啦,即通过obj.key访问时返回。任何js数据类型都可以使用(number,string,object,function等)。 -
writable
这个也很好理解,表示该属性是否可写。当它为false时,属性不可被任何赋值语句重写。然而,此时还可以调用defineProperty来修改value,当然前提是configurable为true啦。
剩下的就是访问描述符啦,先卖个关子讲两个注意事项。
描述符的原型与默认值
一般情况,我们会创建一个descriptor对象,然后传给defineProperty方法。如下:
var descriptor = {
writable: false
}
Object.defineProperty(obj, 'key', descriptor);
这种情况是有风险的,如果descriptor的原型上面有相关特性,也会通过原型链被访问到,算入在对key的定义中。比如:
descriptor.__proto__.enumerable = true;
Object.defineProperty(obj, 'key', descriptor);
Object.getOwnPropertyDescriptor(obj,'key'); //返回的enumerable为true
为了避免发生这样的意外情况,官方建议使用Object.freeze冻结对象,或者是使用Object.create(null)创建一个纯净的对象(不含原型)来使用。
接下来的注意点是默认值,首先我们会想普通的赋值语句会生成怎样的描述符,如obj.key="value"
。
可以使用Object.getOwnPropertyDescriptor来返回一个属性的描述符:
obj = {};
obj.key = "value";
Object.getOwnPropertyDescriptor(obj, 'key');
/*输出
{
configurable:true,
enumerable:true,
value:"value",
writable:true,
}
*/
这也是复合我们预期的,通过赋值语句添加的属性,相关描述符都为true,可写可配置可枚举。但是使用defineProperty定义的属性,默认值就不是这样了,其规则是这样的:
configurable: false
enumerable: false
writable: false
value: undefined
所以这里还是要注意下的,使用的时候把描述符写全,免得默认都成false了。
getter与setter
所谓getter与setter其实是两个概念,并没有这样的属性。与之对应的是两个访问描述符(access descriptor):
- get
它是一个函数,访问该属性时会自动调用,函数的返回值即为该属性的value。默认为undefined。
你可能会想,既有value又有get函数,那么属性的值是什么呢?那你就想多了,这种情况在定义的时候就直接报错了,本身逻辑就矛盾嘛。
- set
它是一个函数,为该属性赋值时会自动调用,并且新值会被当做参数传入。
看到这里你可能就眼前一亮了,为属性赋值的时候会自动执行一个函数,那岂不是就能监控到数据的变化,从而实现mvvm的双向绑定?其实vue的数据监控用到的核心原理也就是这个啦。如果你用过knockout可能感受会更深,knockout能做到在IE6都支持双向绑定,就是强制让属性值为函数类型,必须手动执行函数才能拿到值。
还好现在有了浏览器的默认支持,ES5开始就支持gettter、setter了,现在移动端基本完全可用,pc端需要IE9+。
实际应用
这么好用的方法,我们平时好像也不怎么用呀?写业务代码可能用到的确实少,但是当你要写一个公共模块乃至写一个框架时,就可能用到啦。
比如你写一个公共模块,会往window上挂一些全局属性,并且你不希望别人在其他地方不小心覆盖这个属性,那就可以用defineProperty让该属性不可写、不可配置。贴一个我们项目中的代码:
//向全局挂载通用方法
for(let key in methods){
if(methods.hasOwnProperty(key)){
Object.defineProperty(WIN, key, {
value : methods[key]
});
}
}
另外一个用途呢,就是你自己想干坏事。覆盖别人写的代码,比如写chrome插件刷页面。或者说是想篡改浏览器的一些信息。
比如你想把浏览器的userAgent给改了,直接写navigator.userAgent = 'iPhoneX'
.你再输出一下userAgent,发现并没有修改。这是为什么呢?我们用这行代码看一下:
Object.getOwnPropertyDescriptor(window, 'navigator');
//输出
{
configurable:true,
enumerable:true,
get:ƒ (),
set:undefined
}
原因就找到了,navigator是有setter的,每次取值总会执行这个set函数来做返回。但是好消息是什么呢?configurable为true,那就意味这我们可以通过defineProperty来修改这个属性,代码就相当简单了:
Object.defineProperty(navigator, 'userAgent', {get: function(){return 'iphoneX'}})
console.log(navigator.userAgent); //输出iphoneX
喏,篡改浏览器userAgent的方法我教给你了。