我们都知道vue2.0通过Object.defineProperty来实现对属性的getter和setter劫持,达到监听数据变动的目的。这也是我们理解vue实现数据双向绑定必不可少的一个知识点,可以说是前端面试必问,所以Object.defineProperty成了本文的主角。
背景:
在 ES5 之后,Object新增defineProperty方法,它会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象,对于定义的这个对象有两种描述它的状态,一种称之为数据描述符,一种被称为存取描述符,
1、 Object.defineProperty是干什么的?
数据劫持: 当访问或者设置对象的属性的时候,触发相应的函数,并且返回设置属性的值。
Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
例如:
let Person = {
name: 'jack',
}
console.log(Person) {name: "jack"}
Person.name = 'lusi' //修改Person对象name属性的值
console.log(Person) //{name: "lusi"}
Object.defineProperty(Person,'name',{value: 'xdong'}) //修改Person对象name属性的值
console.log(Person) //{name: "xdong"}
Person.age=18 //给Person对象添加新属性age
console.log(Person) //{name: "xdong", age: 18}
Object.defineProperty(Person,'sex',{value: "男"}) //给Person对象添加新属性sex
console.log(Person) //{name: "xdong", age: 18,sex: "男"}
2、 Object.defineProperty怎么用?
语法:Object.defineProperty(obj, prop, descriptor)
参数说明:
obj 要定义属性的对象。
prop 要定义或修改的属性的名称或 [Symbol] --- 【参数prop类型:String】
descriptor 属性描述 --- 添加或要修改的这个属性有什么样的特性【类型:Object】
对象里目前存在的属性描述符有两种主要形式:
*数据描述符*和*存取描述符*。数据描述符是一个具有值的属性,该值可以是可写的,也可以是不可写的。存取描述符是由 getter 函数和 setter 函数所描述的属性。一个描述符只能是这两者其中之一;不能同时是两者。
2.1 数据描述符可选键值: value
- 该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。
默认为undefined给Person对象添加新属性age
let Person = {
name: 'jack',
}
console.log(Person) {name: "jack"}
Object.defineProperty(Person,'age',{value: 18}) //给Person对象添加新属性age
console.log(Person) //{name: "jack", age: 18}
2.2 数据描述符可选键值: writable
- 当且仅当该属性的
writable键值为true时,属性的值,也就是上面的value,才能被[赋值运算符(en-US)]改变。
默认为false。修改Person对象的属性name
let Person = {}
console.log(Person) //{name: "jack"}
Object.defineProperty(Person,'name',{value: "xdong"})
console.log(Person) //{name: "xdong"}
Person.name = "lusi"
console.log(Person)//{name: "xdong"}
从上面例子可以看出 Person.name = "lusi"并没有修改成功,为什么呢?
原因:因为descriptor有很多属性,除了value属性还有个 writable【属性是否可以被重新赋值】,接受数据类型为 boolean(默认为false) ,true => 支持被重新赋值 false=>只读。没写默认writable为false
再看个有意思的例子
let Person = {
name: 'jack',
}
console.log(Person) //{name: "jack"}
Object.defineProperty(Person,'name',{value: "xdong"}) //修改Person对象的属性name
console.log(Person) //{name: "xdong"}
Person.name = "lusi"
console.log(Person)//{name: "lusi"}
咦,不是说没写默认writable键值为false?怎么这里Person.name = "lusi"修改成功了? 原因是这里Person对象的属性name不是通过Object.defineProperty添加的,而是在Person对象中直接声明的。
小结:只有通过
Object.defineProperty添加的对象属性,它的writable默认才为false。在Person对象中直接声明的属性,该属性的writable键值为true。
不信你看下面这个例子,通过Object.defineProperty将属性name的writable设为false后,Person.name 就不能修改了。
let Person = {
name: 'jack',
}
console.log(Person) //{name: "jack"}
Object.defineProperty(Person,'name',{
value: "xdong",
writable: false
}) //修改Person对象的属性name
console.log(Person) //{name: "xdong"}
Person.name = "lusi"
console.log(Person)//{name: "xdong"}
2.3 存取描述符可选键值: get、set
说明 注意:当使用了getter或setter方法,不允许使用writable和value这两个属性(如果使用,会直接报错滴)
get是获取值的时候的方法,类型为function,获取值的时候会被调用,不设置时为undefined
set是设置值的时候的方法,类型为function,设置值的时候会被调用,undefined
例:
let Person = {
name: 'jack'
}
let i = 18
Object.defineProperty(Person,'age',{
get: function(){
return i
},
set: function(){
i++
}
})
console.log(Person.age) //18
Person.age = 30
console.log(Person.age) //19
千万不能这么写
let Person = {
name: 'jack',
age: 18,
}
Object.defineProperty(Person,'age',{
get: function(){
return this.age
},
set: function(){
this.age++
}
})
console.log(Person.age)
Person.age = 30
console.log(Person.age)
直接上错误截图:
错误解析:
- 以上错误的意思是 "超出最大调用堆栈大小"
- 出现这种错误最常见的原因是:在代码中的某个地方,您正在调用一个函数,该函数又调用另一个函数,依此类推,直到达到调用堆栈限制。这几乎总是因为具有未满足的基本情况的递归函数。相当于this.age 自己调用自己,造成无限循环。
2.4 数据描述符和存取描述符都可选键值: configurable
configurable有两个作用:
1 属性是否可以被删除
2 属性的特性在第一次设置之后可否被重新定义特性
let Person ={
name:"狂奔的蜗牛",
age:18
} ;
//定义一个性别 可以被删除和重新定义特性
Object.defineProperty(Person,"gender",{
value:"男",
configurable:true
})
//删除前
console.log(Person); // {name: "xdong", age: 18, gender: "男"}
//删除一下
delete Person.gender;
console.log(Person); // {name: "xdong", age: 18}
//重新定义特性
Object.defineProperty(Person,"gender",{
value:"男",
configurable:false
})
//删除前
console.log(Person); // {name: "xdong", age: 18, gender: "男"}
//删除一下 删除失败
delete Person.gender;
console.log(Person); // {name: "xdong", age: 18, gender: "男"}
小结:
configurable设置为 true 则该属性可以被删除和重新定义特性;反之属性是不可以被删除和重新定义特性的,默认值为false(Ps.除了可以给新定义的属性设置特性,也可以给已有的属性设置特性哈)
2.5 数据描述符和存取描述符都可选键值: enumerable
想知道这个 user对象有哪些属性我们一般会这么做
var Person ={
name: "xdong",
age: 18
}
//es6
var keys = Object.keys(Person)
console.log(keys);// ['name','age']
//es5
var keys=[];
for(key in Person){
keys.push(key);
}
console.log(keys);// ['name','age']
如果我们使用 Object.defineProperty的方式定义属性会发生什么呢?我们来看下输出
var Person ={
name: "xdong",
age: 18
}
//定义一个性别 可以被枚举
Object.defineProperty(Person,"gender",{
value:"男",
enumerable:true
})
//定义一个出生日期 不可以被枚举
Object.defineProperty(Person,"birth",{
value:"2000-05-26",
enumerable:false
})
//es6
var keys=Object.keys(Person)
console.log(keys);
// ["name", "age", "gender"]
console.log(Person);
// {name: "xdong", age: 18, gender: "男", birth: "2000-05-26"}
console.log(user.birth);
// 1956-05-03
很明显,我们定义为 enumerable=false的birth属性并没有被遍历出来
小结:
enumerable属性取值为布尔类型 true | false默认值为false,为真属性可以被枚举;反之则不能。此设置不影响属性的调用和 查看对象的值。
3、 Object.defineProperty可以用来干嘛?
3.1 简单实现数据双向绑定
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div>
<p>你好,<span id='nickName'></span></p>
<div id="introduce"></div>
</div>
<script>
//视图控制器,设置一个数据的属性的getter和setter
var userInfo = {};
Object.defineProperty(userInfo, "nickName", {
get: function(){
return document.getElementById('nickName').innerHTML;
},
set: function(nick){
document.getElementById('nickName').innerHTML = nick;
}
});
Object.defineProperty(userInfo, "introduce", {
get: function(){
return document.getElementById('introduce').innerHTML;
},
set: function(introduce){
document.getElementById('introduce').innerHTML = introduce;
}
})
//然后就能愉快地绑定数据交互了。
userInfo.nickName = "china";
userInfo.introduce = "我是xdong,我来自重庆,..."
</script>
</body>
</html>
但是,上面这个例子只是数据和dom节点的绑定,而vue.js更为复杂一点,它在网页dom和accessor之间会有两层,一层是Wacher,一层是Directive,比如以下代码。
var a = { b: 1 }
var vm = new Vue({
data: data
})
把一个普通对象(a={b:1})传给 Vue 实例作为它的 data 选项,Vue.js 将遍历它的属性,用Object.defineProperty 将它们转为 getter/setter,如图绿色的部分所示。
每次用户更改data里的数据的时候,比如a.b =1,setter就会重新通知Watcher进行变动,Watcher再通知Directive对dom节点进行更改。\
官网图片
3.2 解决JS奇葩问题:如何让 (a==1 && a==2 && a==3)返回true
当我们访问一个被设置了存取描述符的元素时,如果在get方法里面做一些操作,就能巧妙的使得最终的结果达到预期:
var i = 1
Object.defineProperty(window, 'a', {
get() { return i++ }
})
if (a === 1 && a === 2 && a === 3) {
console.log('Hello World!');
}
这种劫持getter和setter的方法本质上是执行了一个函数,内部除了用自增变量,如果我们想得到其他结果,只需在getter函数内部返回我们需要的结果即可。
这道题的其他解法 请看这里
本文参考链接
vue.js关于Object.defineProperty的利用原理
js中Object.defineProperty()方法的解释
JS 奇葩知识点,如何让 (a == 1 && a == 2 && a == 3) 返回 true