理解defineProperty以及getter、setter

2019-05-22165次阅读javascript

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

 

描述符可同时具有的键值

 configurableenumerablevaluewritablegetset
数据描述符YesYesYesYesNoNo
存取描述符YesYesNoNoYesYes

如果一个描述符不具有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

上一篇: inputmode输入模式了解一下  下一篇: Object.defineProperty的问题  

理解defineProperty以及getter、setter相关文章