理解defineProperty以及getter、setter


我们常听说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).

这两类描述符有两个必选项:

  1. configurable
    从字面意思看它表示“可配置”,含义是:当它为true时,该属性的描述符可被修改,并且该属性可被delete删除。同理,当它为false时,我们无法再次调用defineProperty去修改描述符,也不可通过delete删除。

  2. enumerable
    从字面意思看它表示“可枚举”,含义是:当它为true时,该属性可被迭代器枚举出来。比如使用for in或者是Object.keys。

接下来就是数据描述符(data descriptor)了,有两个:

  1. value
    这个就是该属性的值啦,即通过obj.key访问时返回。任何js数据类型都可以使用(number,string,object,function等)。

  2. writable
    这个也很好理解,表示改属性是否可写。当它为false时,属性不可被任何赋值语句重写。想要通过调用defineProperty修改value的方式也是不行的。

剩下的就是访问描述符啦,先卖个关子讲两个注意事项。

描述符的原型与默认值

一般情况,我们会创建一个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):

  1. get
    它是一个函数,访问该属性时会自动调用,函数的返回值即为该属性的value。默认为undefined。

你可能会想,既有value又有get函数,那么属性的值是什么呢?那你就想多了,这种情况在定义的时候就直接报错了,本身逻辑就矛盾嘛。

  1. 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的方法我教给你了。


发表评论

电子邮件地址不会被公开。 必填项已用*标注

您可以使用这些HTML标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>