一个对象有很多属性,这些属性包含了属性名和属性值,但除此之外,一个属性其实还包含了很多东西,一个属性中,除了属性名和属性值之外的东西,我们就称为属性描述符。所以,其实任何一个属性都是由一个属性描述符来描述他的。
由于属性本身就是一个数据,而属性描述符是描述这个属性的数据,也就是描述一个数据的数据,这种描述一个数据的数据我们也称为元数据
从本质上来讲,属性描述符也是一个对象,也就是说,任何一个属性,都有一个对象用来描述他的一些信息。
在以前我们学习的所有属性,其实都可以称为数据属性,除此之外,还有一种属性,叫做存取器属性,对于存取器属性而言,进行读操作会自动运行一个函数,进行写操作又会自动运行一个函数。所以,对于存取器属性,是需要两个函数的,一个函数在读取的时候运行,一个函数在写入的时候运行,我们称读取的时候运行的函数为 getter,写入的时候运行的函数为 setter,这两个函数是要在给属性赋值的时候就定义好的,要定义一个存取器属性的 getter 和 setter 函数,就要用属性描述符。
我们可以使用 Object.defineProperty(),语法为:Object.defineProperty(对象名,"属性名",属性描述符)。运行这个函数后,就会给指定对象添加一个属性,该属性通过属性描述符来定义。
而属性描述符就是一个对象,这个对象里就可以配置和这个属性的相关信息,主要有以下几个:
- value:配置属性值,默认是 undefined,对于数值型属性,对其读写操作本质上就是对这个 value 属性进行读写操作,实际上,我们获取一个未赋值的属性时,默认返回的 undefined,本质上就是因为属性描述符的 value 属性默认是 undefined。
- writable:配置这个属性是否可写,如果设置为 false,那么这个属性将不会被赋值运算符修改,我们前面见到的一些只读属性,其实就是通过这个属性描述符配置的。默认为 false。
- get:配置 getter 函数,当这个属性被读取的时候,会自动运行该函数,并返回其返回值
- set:配置 setter 函数,当这个属性被写入的时候,会自动运行该函数,并返回其返回值
- configurable:属性描述符是否可配置,或者说这个属性的属性描述符是否可修改,或者说的重新定义,如果设置为 true,表示你可以重新设置这个属性描述符,并且这个属性可被删除。默认为 false。
- enumerable:是否可遍历,默认为 false。
一旦配置了 get 和 set,那么 value 值就失效了,此时赋值和读取就会执行指定函数,而不再是获取和修改 value 值。如果你在配置了 get 和 set 函数的同时配置了 value 或者 writable 的值,甚至还会报错。
js
var obj = {};
Object.defineProperty(obj, "x", {
get: function () {
//读取属性时自动运行get函数,且get函数的返回值为读取操作获得的值
//实际上,读操作获取到的值是get函数的返回值,所以我们可以认为get函数的返回值就是这个属性的属性值
console.log("读取了x属性");
return 1;
},
set: function (val) {
//给属性赋值的时候自动运行set函数,赋的值将作为参数传入
console.log("写入了x属性" + val);
},
});
console.log(obj.x);
console.log((obj.x = 2));get 和 set 可以在给对象赋值的时候作出一些判断,避免用户乱赋值
js
function User(name,age){
this.name = name;
if(age < 0)
age = 0;
else if(age >120)
age = 120
this.age = age;
}
var user1 = new User("yl",-5);
console.log(user1.age);//0
user1.age = -5;
console.log(user1.age);//-5 在构造函数中进行判断,无法防止后期的乱赋值
function User2(name, age) {
this.name = name;
var _age;
Object.defineProperty(this, "age", {
get: function () {
return _age;
},
set: function (val) {
if (val < 0) val = 0;
else if (val > 120) val = 120;
_age = val;
},
});
this.age = age;
}
var user2 = new User2("yl", -5);
console.log(user2.age); //0
user2.age = -5;
console.log(user2.age); //0 可以防止用户乱赋值get 和 set 函数中的 this 指向当前对象
这里我们使用属性描述符来做一个 BOM 的小球联系,读者会发现这个逻辑非常清晰
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
* {
/* 清除所有元素的默认内外边距 */
margin: 0;
padding: 0;
}
.box {
position: fixed;
}
</style>
</head>
<body>
<div class="box"></div>
<script>
// 获取页面中class为box的元素
var box = document.querySelector('.box');
// 定义配置对象,包含盒子的初始属性
var config = {
width: 100, // 宽度
height: 100, // 高度
background: 'red', // 背景颜色
borderRadius: "50%", // 圆角半径
_left: 0, // 初始化左边位置
_top: 0, // 初始化上边位置
leftDis: 2, // 水平移动距离
topDis: 2 // 垂直移动距离
};
// 使用Object.defineProperty定义config对象的left属性,包含getter和setter
Object.defineProperty(config, "left", {
get: function () {
return this._left;
},
set: function (val) {
// 设置left值时,防止超出屏幕边界
if (val >= document.documentElement.clientWidth - this.width) {
val = document.documentElement.clientWidth - this.width
}
if (val <= 0) {
val = 0;
}
this._left = val;
box.style.left = val + 'px'; // 更新元素的left样式
}
});
// 使用Object.defineProperty定义config对象的top属性,包含getter和setter
Object.defineProperty(config, "top", {
get: function () {
return this._top;
},
set: function (val) {
// 设置top值时,防止超出屏幕边界
if (val >= document.documentElement.clientHeight - this.height) {
val = document.documentElement.clientHeight - this.height
}
if (val <= 0) {
val = 0;
}
this._top = val;
box.style.top = val + 'px'; // 更新元素的top样式
}
});
// 初始化盒子的位置和样式
config.left = 0;
config.top = 0;
box.style.borderRadius = config.borderRadius;
box.style.height = config.height + 'px';
box.style.width = config.width + 'px';
box.style.backgroundColor = config.background;
// 定期更新盒子的位置,实现移动效果
setInterval(function () {
config.left += config.leftDis;
// 当盒子到达边界时,改变移动方向
if (config.left == document.documentElement.clientWidth - config.width || config.left == 0) {
config.leftDis = -config.leftDis;
}
config.top += config.topDis;
// 当盒子到达边界时,改变移动方向
if (config.top == document.documentElement.clientHeight - config.height || config.top == 0) {
config.topDis = -config.topDis;
}
}, 16);
</script>
</body>
</html>在这个案例中,当我给 config.left 和 config.top 修改值的时候,就会自动将 box 盒子放到指定位置,同时也保证每次读取改属性得到的都是正确的。所以,当我们要修改盒子位置的时候,只需要修改 config.left 即可,不需要再每次去操作 DOM 元素。即在定时器中,我们只需要关注 config.left 和 config.top 的值,而不需关注真实的 DOM。此时 config.left 和 config.top 就像是可以驱动视图一样,这种数据驱动视图的思想非常重要。
实际上,我们在前面学到的很多东西,比如给元素的 innerHTML 属性赋值后会立刻修改视图,都是通过类似的方式实现的。
除此之外,get 和 set 函数还可以用于实现只读属性,只写一个 get 函数,返回一个固定值,即可实现只读属性,除此之外,还可以使用 writable 这个属性描述符,将其赋值为 false,即可实现只读属性。
实际上,只要配置了属性描述符,writable 默认就为 false,即不可写。
接下来是最后一个 enumerable 描述符,我们知道,for in 循环可以访问一个对象的所有属性,但实际上,我们可以将一个属性的 enumerable 描述符设置为 false,此时 for in 循环就不会再访问到这个属性了。这其实就可以解释为什么 Object.prototype 中有非常多的属性,但是我们 for in 循环都遍历不到,就是因为他们的 enumerable 描述符被设置为了 false。
以上就是属性描述符的全部知识,下面一个问题是,我们如何获取一个属性的属性描述符,JS 给我们提供了一个方法:Object.getOwnPropertyDescriptor,这个静态方法接受两个参数,第一个是对象,第二个是属性名字符串,该方法就可以获取一个对象中某个属性的属性描述符对象。注意,该方法只能获取到自己的属性,无法获取原型链上的属性。