我们常听说vue是用getter与setter实现数据监控的,那么getter与setter到底是什么东西,它与defineProperty是什么关系,平时有哪些用处呢?本文将为大家一一道来。注:Vue.js 3.0不再使用Object.defineProperty而是原生Proxy。
对象的属性
我们知道对象有自身的属性以及原型上的属性,它们都可以通过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)。描述符必须是这两种形式之一,不能同时是两者。默认情况下,使用 Object.defineProperty() 添加的属性值是不可修改的。
这两类描述符有两个必选项:
configurable
从字面意思看它表示“可配置”,含义是:当它为true时,该属性的描述符可被修改,并且该属性可被delete删除。同理,当它为false时,我们无法再次调用defineProperty去修改描述符,也不可通过delete删除。默认为false。
var o = {};
Object.defineProperty(o, "a", {
get : function(){return 1;},
configurable : false
}
);
// throws a TypeError
Object.defineProperty(o, "a", {configurable : true});
// throws a TypeError
Object.defineProperty(o, "a", {enumerable : true});
// throws a TypeError (set was undefined previously)
Object.defineProperty(o, "a", {set : function(){}});
// throws a TypeError (even though the new get does exactly the same thing)
Object.defineProperty(o, "a", {get : function(){return 1;}});
// throws a TypeError
Object.defineProperty(o, "a", {value : 12});
console.log(o.a); // logs 1
delete o.a; // Nothing happens
console.log(o.a); // logs 1
如果o.a的configurable属性为true,则不会抛出任何错误,并且该属性将在最后被删除。
enumerable
从字面意思看它表示“可枚举”,含义是:当它为true时,该属性可被迭代器枚举出来。比如使用for in或者是Object.keys。默认为false。
var o = {};
Object.defineProperty(o, "a", {
value : 1,
enumerable:true
});
Object.defineProperty(o, "b", {
value : 2,
enumerable:false
});
Object.defineProperty(o, "c", {
value : 3
}); // enumerable defaults to false
o.d = 4; // 如果使用直接赋值的方式创建对象的属性,则这个属性的enumerable为true
for(var i in o){
console.log(i);
}
// 打印 'a' 和 'd' (in undefined order)
Object.keys(o); // ["a", "d"]
o.propertyIsEnumerable('a'); // true
o.propertyIsEnumerable('b'); // false
o.propertyIsEnumerable('c'); // false
数据描述符同时具有以下可选键值:
value
这个就是该属性的值啦,即通过obj.key访问时返回。任何js数据类型都可以使用(number,string,object,function等)。默认为undefined。
writable
这个也很好理解,表示该属性是否可写。当它为false时,属性不可被任何赋值语句重写。然而,此时还可以调用defineProperty来修改value,当然前提是configurable为true啦。默认为false。
var o = {}; // Creates a new object
Object.defineProperty(o, 'a', {
value: 37,
writable: false
});
console.log(o.a); // logs 37
o.a = 25; // No error thrown
// (it would throw in strict mode,
// even if the value had been the same)
console.log(o.a); // logs 37. The assignment didn't work.
// strict mode
(function() {
'use strict';
var o = {};
Object.defineProperty(o, 'b', {
value: 2,
writable: false
});
o.b = 3; // throws TypeError: "b" is read-only
return o.b; // returns 2 without the line above
}());
如示例所示,试图写入非可写属性不会改变它,也不会引发错误。
访问描述符同时具有以下可选键值:
get
一个给属性提供getter的方法,如果没有getter则为undefined。当访问该属性时,该方法会被执行,方法执行时没有参数传入,但是会传入this对象(由于继承关系,这里的this并不一定是定义该属性的对象)。默认为undefined。
set
一个给属性提供setter的方法,如果没有setter则为undefined。当属性值修改时,触发执行该方法。该方法将接受唯一参数,即该属性新的参数值。默认为undefined。
描述符可同时具有的键值
configurable | enumerable | value | writable | get | set | |
数据描述符 | Yes | Yes | Yes | Yes | No | No |
存取描述符 | Yes | Yes | No | No | Yes | Yes |
如果一个描述符不具有value,writable,get 和 set 任意一个关键字,那么它将被认为是一个数据描述符。如果一个描述符同时有(value或writable)和(get或set)关键字,将会产生一个异常。
描述符的原型与默认值
一般情况,我们会创建一个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)将__proto__属性指向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
所以这里还是要注意下的,使用defineProperty定义属性的时候把描述符写全,免得默认都成false了。
创建属性
var o = {}; // 创建一个新对象
// 在对象中添加一个属性与数据描述符的示例
Object.defineProperty(o, "a", {
value : 37,
writable : true,
enumerable : true,
configurable : true
});
// 对象o拥有了属性a,值为37
// 在对象中添加一个属性与存取描述符的示例
var bValue;
Object.defineProperty(o, "b", {
get : function(){
return bValue;
},
set : function(newValue){
bValue = newValue;
},
enumerable : true,
configurable : true
});
o.b = 38;
// 对象o拥有了属性b,值为38
// o.b的值现在总是与bValue相同,除非重新定义o.b
// 数据描述符和存取描述符不能混合使用
Object.defineProperty(o, "conflict", {
value: 0x9f91102,
get: function() {
return 0xdeadbeef;
}
});
// 抛出TypeError:value只能出现在数据描述符中,get只能出现在访问器描述符中
添加多个属性和默认值
考虑特性被赋予的默认特性值非常重要,通常,使用点运算符和Object.defineProperty()为对象的属性赋值时,数据描述符中的属性默认值是不同的,如下例所示。
var o = {};
o.a = 1;
// 等同于 :
Object.defineProperty(o, "a", {
value : 1,
writable : true,
configurable : true,
enumerable : true
});
// 另一方面,
Object.defineProperty(o, "a", { value : 1 });
// 等同于 :
Object.defineProperty(o, "a", {
value : 1,
writable : false,
configurable : false,
enumerable : false
});
getter与setter
所谓getter与setter其实是两个概念,并没有这样的属性。与之对应的是两个访问描述符(access descriptor):
get
它是一个函数,访问该属性时会自动调用,函数的返回值即为该属性的value。默认为undefined。
你可能会想,既有value又有get函数,那么属性的值是什么呢?那你就想多了,这种情况在定义的时候就直接报错了,本身逻辑就矛盾嘛。
set
它是一个函数,为该属性赋值时会自动调用,并且新值会被当做参数传入。
看到这里你可能就眼前一亮了,为属性赋值的时候会自动执行一个函数,那岂不是就能监控到数据的变化,从而实现mvvm的双向绑定?其实vue的数据监控用到的核心原理也就是这个啦。如果你用过knockout可能感受会更深,knockout能做到在IE6都支持双向绑定,就是强制让属性值为函数类型,必须手动执行函数才能拿到值。
还好现在有了浏览器的默认支持,ES5开始就支持gettter、setter了,现在移动端基本完全可用,pc端需要IE9+。
下面的例子展示了如何实现一个自存档对象。 当设置temperature 属性时,archive 数组会获取日志条目。
function Archiver() {
var temperature = null;
var archive = [];
Object.defineProperty(this, 'temperature', {
get: function() {
console.log('get!');
return temperature;
},
set: function(value) {
temperature = value;
archive.push({ val: temperature });
}
});
this.getArchive = function() { return archive; };
}
var arc = new Archiver();
arc.temperature; // 'get!'
arc.temperature = 11;
arc.temperature = 13;
arc.getArchive(); // [{ val: 11 }, { val: 13 }]
或
var pattern = {
get: function () {
return 'I alway return this string,whatever you have assigned';
},
set: function () {
this.myname = 'this is my name string';
}
};
function TestDefineSetAndGet() {
Object.defineProperty(this, 'myproperty', pattern);
}
var instance = new TestDefineSetAndGet();
instance.myproperty = 'test';
// 'I alway return this string,whatever you have assigned'
console.log(instance.myproperty);
// 'this is my name string'
console.log(instance.myname);//继承属性
继承属性
如果访问者的属性是被继承的,它的get和set方法会在子对象的属性被访问或者修改时被调用。如果这些方法用一个变量存值,该值会被所有对象共享。
function myclass() {
}
var value;
Object.defineProperty(myclass.prototype, "x", {
get() {
return value;
},
set(x) {
value = x;
}
});
var a = new myclass();
var b = new myclass();
a.x = 1;
console.log(b.x); // 1
这可以通过将值存储在另一个属性中固定。在get和set方法中,this指向某个被访问和修改属性的对象。
function myclass() {
}
Object.defineProperty(myclass.prototype, "x", {
get() {
return this.stored_x;
},
set(x) {
this.stored_x = x;
}
});
var a = new myclass();
var b = new myclass();
a.x = 1;
console.log(b.x); // undefined
不像访问者属性,值属性始终在对象自身上设置,而不是一个原型。然而,如果一个不可写的属性被继承,它仍然可以防止修改对象的属性。
function myclass() {
}
myclass.prototype.x = 1;
Object.defineProperty(myclass.prototype, "y", {
writable: false,
value: 1
});
var a = new myclass();
a.x = 2;
console.log(a.x); // 2
console.log(myclass.prototype.x); // 1
a.y = 2; // Ignored, throws in strict mode
console.log(a.y); // 1
console.log(myclass.prototype.y); // 1
示例
实现数据的双向绑定
<input type="text" id="demo">
<p id="display"></p>
var obj={};
var bind=[];
//触发obj对象set和get方法的时候
Object.defineProperty(obj,'s',{
set:function(val){
//修改bind数组的内容
bind['s']=val;
//修改文本框、p标签值
display.innerHTML=bind['s'];
demo.value=bind['s'];
},
get:function(){
return bind['s'];
}
})
var demo=document.querySelector('#demo');
var display=document.querySelector('#display');
//#demo绑定onkeyup事件
demo.onkeyup=function(){
//触发了obj的set方法,等于#demo的value值赋值给bind['s']。
obj['s'] = demo.value;
}
配置对象属性不可写、不可配置
例如往window上挂一些全局属性,并且你不希望别人在其他地方不小心覆盖这个属性,那就可以用defineProperty让该属性不可写、不可配置。
//向全局挂载通用方法
for(let key in methods){
if(methods.hasOwnProperty(key)){
Object.defineProperty(WIN, key, {
value: methods[key]
});
}
}
篡改浏览器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
参考:
https://www.cnblogs.com/bydzhangxiaowei/p/8089127.html
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty