【你不知道的JavaScript】对象属性描述符

979 阅读6分钟

前言

本系列为学习笔记,我将在此记录我从【你不知道的JavaScript】中获取到的知识,如果你也有兴趣,可以跟我一起学习。

属性描述符

从ES5开始,JS具备了对象属性的属性描述符,我们来看这样一段代码

let obj={name:"qiuyanxi"}
Object.getOwnPropertyDescriptor(obj,'name')
{
configurable: true  //可配置
enumerable: true  //可枚举
value: "qiuyanxi"
writable: true  //可写
}

在上面代码中,我们使用Object.getOwnPropertyDescriptor()这个方法来查看属性的属性描述符,其中writable控制是否可以修改value值,enumerable控制是否可以枚举,configurable控制对象是否可以进行配置。

如果我们希望修改这个属性的属性描述符,那么就可以使用Object.defineProperty进行修改,同时这个方法也可以用来新增属性,不过一般我们不会直接用来新增,除非我们想要在新增的同时修改默认的属性描述符。

修改属性描述符

Object.defineProperty(obj,"name",{
writable:false,
configurable:false,
enumerable: false
})

上面代码中我将objname属性全部改成了false,这时候obj.name的状态是不可写、不可遍历、不可修改配置(也不可删除)

writable

现在我尝试继续对obj.name进行赋值修改,会发现修改无效

obj.name=2
obj.name //'qiuyanxi'

不过如果你的属性值为复杂数据类型,那么此时依然可以修改内容,只是obj.name的指向地址不能变罢了。

let obj={name:"qiuyanxi",array:[1,2,3,4]}
Object.defineProperty(obj,"array",{
writable:false,
configurable:true,
enumerable: true
})
obj.array=2
obj.array //[1,2,3,4]
obj.array.push(5)
obj.array //[1,2,3,4,5]

说明我们通过writable只是不能修改属性的内存指向。

configurable

这个属性描述符是用来设置对象是否可以配置的,从true-false产生了以下变化:

  • 不能再修改其他属性描述符,相当于这个属性描述符被管控了(但writable可以从true可以改成false)
  • 不能通过delete删除属性
  • 不能从false修改成true,这是一次单向操作
let obj={name:"qiuyanxi"}
// 设置成不可配置
Object.defineProperty(obj,"name",{
writable:true,
configurable:false,//不可配置
enumerable: true
})

//第一次修改
Object.defineProperty(obj,"name",{
writable:false, //修改了这里
})

//第二次修改
Object.defineProperty(obj,"name",{
enumerable: false //修改了这里
}) // Uncaught TypeError

// 第三次修改
Object.defineProperty(obj,"name",{
writable:true, //修改了这里
}) // Uncaught TypeError

// 第四次修改
Object.defineProperty(obj,"name",{
configurable:true, //修改了这里
}) // Uncaught TypeError

// 最后的结果
Object.getOwnPropertyDescriptor(obj,'name')
{
configurable: false
enumerable: true
writable: false
}

> delete obj.name
< false
obj.name //"qiuyanxi"

在第一次修改时,我把writable改成false,此时没有问题

第二次修改时,我把enumerable修改成true,此时报错,从最后的结果来看,也没有成功,说明当配置了configurable后,不能修改enumerable

第三次修改时,是想将writable改成true,此时报错,说明只允许将writable改成false

第四次是想将configurable改成true,同样报错。

最后通过delete希望删除属性,会发现禁止删除,因为属性是不可配置的。

enumerable

这个属性描述符控制的是是否可枚举出来,当我们将它设置成false之后,使用枚举就查不到该属性

let obj={name:"qiuyanxi",age:11}
Object.defineProperty(obj,"age",{
enumerable: false //修改了这里
})
for(let k in obj){console.log(k)}
// name

上面的代码使用for in来循环obj,会发现age没有被枚举出来

不变性

通过属性描述符我们可以设置属性的不可变性,但是这些只针对属性值为简单数据类型或者复杂数据类型的引用指向,如果属性值为复杂数据类型,那么在引用指向不变的情况下是可以修改其值的。这个就叫做浅不变性。

var obj={}
Object.defineProperty(obj,"age",{
value:[1,2,3],
writable:false,
enumerable:false,
configurable:false
})
obj.age=1 //无效
obj.age.push(4)
obj.age //[1,2,3,4]

对象常量

上面的代码中,当我们将obj.age设置成了writable:false,configurable:false时,这个对象的属性就变成了常量,不可修改(如果属性值不是复杂数据类型),不可删除、重新定义

禁止扩展

如果我们不希望这个对象添加更多的属性,那么可以使用Object.prevent Extensions(..)方法禁止扩展

var obj={a:1}
Object.preventExtensions(obj)
obj.b=2
obj // {a:1}

密封

密封就是禁止扩展的升级版,对这个对象做prevent Extensions操作后将属性的configurable全改成false,主要通过Object.seal(..)实现

var obj={a:1}
Object.seal(obj)
obj.b=2 //无效
delete obj.a //无效
obj  //{a:1}

密封后不但不能扩展,而且不能配置(可以修改)或者删除

冻结

冻结就是密封的升级版,在密封之后顺便把所有属性的writable给关掉,此时不能修改. 冻结主要通过Object.freeze(..)实现

var obj={a:1}
Object.freeze(obj)
obj.a=2 //无效
obj // 1

属性访问和属性操作

默认get和put

默认情况下,在访问对象时,会执行get算法访问,在我写的【你不知道的JavaScript】作用域是什么中,我们得知JS引擎使用RHS或者LHS来访问变量,这跟访问对象属性不太一样。

当LHS算法找不到变量时,会往上找作用域,当顶级作用域也没有这个变量时,抛出ReferenceError。

get算法会遍历对象的原型链,如果在对象内部找不到则遍历它的原型链,如果全部找不到那么返回一个undefined。

这里注意一个细节,请看以下代码

obj={a:undefined}
obj.a //undefined
obj.b //undefined

上面的代码中,看起来两者的引用是一样的,但是b却并不存在。如何才能判断某属性又在对象中,又是undefined呢?这里埋一个坑,由于答案太多所以请到本章最后面查看。

put操作则是对对象属性进行设置,它执行的顺序大概是这样的:

  • 首先查看属性名是否存在,如果存在则检查属性是否为访问描述符getter或者setter,如果是并存在setter则调用setter。

  • 如果不是访问描述符则检查writable是否为false,是则不进行操作,如果不是则设置属性值

  • 如果属性名不存在,则设置默认的属性描述符,并且设置属性值。

以上则是对象属性的默认操作,如果我不想用默认操作呢,那么就可以使用上面提到的getter和setter。

getter和setter

getter和setter会对默认的属性操作进行覆盖,如果都设置了,那么访问对象时,会使得writablevalue都无效,转而使用getter和setter方法来返回对应的值

getter写法:

var obj={get a(){return 123}}
obj.a //123

或者

var obj=Object.defineProperty({},"b",{
get (){return 456}
})
obj.b //456
obj.b=333
obj.b //456

当只有getter属性时,你对对象做的任何操作都是无效的,通过上面的get章节我们可以知道当做get访问时,会先去查看是不是具有访问描述符(getter),如果有且具有setter,那么就调用setter。如果没有,那么就会调用getter取得返回值

所以gettersetter一般是同时存在的 setter写法:

var obj={
a:7,
get b(){return this.a},
set b(val){this.a=val*2} //这个val是什么??
}
obj.b //7
obj.b=4  //val就是这里的4
obj.b //8 被val(4)*2计算了
obj.a //8

getter和setter同时存在需要借助第三方属性。

我们完全可以把这个称之为虚拟属性,因为其存在的原因只不过是通过getter和setter函数的互相计算罢了。

对于热门框架Vue中的computed和双向数据绑定,就是采用了gettersetter的方式,如果有兴趣可以参考我这篇文章【Vue-data原理】存取器与Object.defineProperty

存在性

上面还留了一个大坑,那就是

obj={a:undefined}
obj.a //undefined
obj.b //undefined

如何判断属性是存在的且为undefined呢?

我们可以采取以下方式:

  • in 会顺着原型链往上查找 【原型链】
  • hasOwnProperty 只会找对象自身具有的属性 【非原型链】
  • Object.keys 会查自身可枚举的属性,返回数组 【非原型链】【只能读可枚举属性】
  • Object.getOwnPropertyNames 【非原型链】
  • 枚举 for..in循环

in

in操作符可以顺着原型链查找对象的属性是否存在,我们通过它就可以判断属性是否存在

let obj={a:2,b:undefined}
'a' in obj  //true
"toString" in obj //true
"b" in obj && obj.b===undefined
"c" in obj && obj.c===undefined //false

hasOwnProperty

in操作符和hasOwnProperty的区别就是是否会查原型链。

let obj={a:2,b:undefined}
obj.hasOwnProperty('a') //true
obj.hasOwnProperty('toString') //false

Object.keys

这个方法也不会受原型链影响,它只会将可枚举的属性通过一个数组返回出来

let obj={name:"qiuyanxi",age:11}
Object.defineProperty(obj,'age',{enumerable:false})
Object.keys(obj) // ["name"]

getOwnPropertyNames

这个方法不管是否可枚举,都会通过数组返回出来,所以上面的代码通过这个方法就会是这样的

Object.getOwnPropertyNames(obj)
// ["name", "age"]

枚举循环

一般对象我们使用for..in循环,这个同样受属性描述符的影响,这里就不贴代码了,注意,for in会循环出原型链

小结

| 操作符/方法 | 遍历原型链 | 受枚举影响 |返回 | |------|------------|------------||------------| | in | √ | X | bool | | hasOwnProperty | X | X |bool| | Object.keys | X | √ |Array| | getOwnPropertyNames | X | X |Array| | for in | √ | √ |any|

总结

今天学习到的内容为属性描述符,包含

  • configurable //可配置
  • enumerable //可枚举
  • writable //可写
  • get和put默认操作
  • setter和getter访问描述符(存取器)
  • 查属性的存在性的方法 我们下期再见!!

参考文档

你不知道的Javascript