Javascript基础之对象的属性和特性

385 阅读7分钟

对象是Javascript常用的数据类型之一,面试时提到的深拷贝、浅拷贝,属性定义、原型链等都是对象的常见考题,本文通过代码片段来说一说对象的属性定义,从而理解深浅拷贝、原型链等知识点

1.对象的属性

1.1 数值型

定义在对象上的数据,或者叫值,比如下面的 obj1 对象的 x 就是一个数据型属性:

const obj1 = { x: 1 }

1.2 访问器

定义在对象上的方法,一般有get获取方法和set设置方法,比如下面 obj2 对象的 x() 就是一个访问器属性:

const obj2 = { 
    get x() {
        return 1
    },
    set x(newX) {
        console.log(newX)
        // 注意这里不能直接 this.x = newX,因为此时obj2上面没有数值型属性x,否则会内存溢出,见下图中的obj
    }
}

image.png

2.对象属性的特性

介绍完对象的两种属性,下面说说属性的特性,也称为属性描述符

2.1 数值型属性的特性

  • value:值
  • writable:是否可写
  • enumerable:是否可以枚举,比如用for while等是否可以枚举出来
  • configurable:是否可以配置,表示当前属性的特性是否允许修改

获取 obj1 的特性如下所示,可以看出 x 属性的特性,这也是对象中数值型属性的默认特性

// 获取obj1对象x的特性
Object.getOwnPropertyDescriptor(obj1, 'x')

image.png

2.2 访问器属性的特性

  • get:获取
  • set:设置
  • enumerable:是否可以枚举
  • configurable:是否可以配置

获取 obj2 的特性如下所示,可以看出 x 属性的特性

image.png

对象中访问器属性的默认特性是 enumerable: trueconfigurable: true, get或者set没有设置就是undefined,比如下图中obj3 image.png

2.3 如何获取对象属性的特性

  • getOwnPropertyDescriptor:获取对象指定属性的特性,有两个参数,第一个是对象,第二个是对象的属性key
  • getOwnPropertyDescriptors:获取对象所有属性的特性,有一个参数,也就是对象本身

image.png

到这里我们就清楚一个对象的属性和属性所有的特性是啥含义了,下面说说利用这些属性特性有啥用处

3. 设置对象的属性和特性

通过Object.defineProperty()方法来设置对象的属性和特性,比如:

const obj = {} // 声明一个空对象
// 为obj添加x数值型属性
Object.defineProperty(obj, 'x', {
    value: 1,
    writable: true,
    enumerable: true,
    configurable: true
})
console.log(obj.x) // 1
obj.x = 2
console.log(obj.x) // 2
console.log(Object.getOwnPropertyDescriptor(obj, 'x'))
// { value: 2, writable: true, enumerable: true, configurable: true }

下面对obj设置一个y属性,可以看出,不设置特性(属性描述符)时,默认都是false

// 为obj添加y数值型属性,并且不可写
Object.defineProperty(obj, 'y', {
    value: 20
})
console.log(obj.y) // 20
// 设置失败,非严格模式直接略过,
// 严格模式报错: TypeError: Cannot assign to read only property 'y' of object '#<Object>'
obj.y = 21
console.log(obj.y) // 20
console.log(Object.getOwnPropertyDescriptor(obj, 'y'))
// { value: 20, writable: false, enumerable: false, configurable: false }

下面为obj添加一个访问器类型属性z,可以看出,不设置特性(属性描述符)时,默认也是false或者undefined

// 为obj添加z访问器属性
Object.defineProperty(obj, 'z', {
    get: () => 30
})
console.log(obj.z); // 30
console.log(Object.getOwnPropertyDescriptor(obj, 'z'))
// {
//     get: [Function: get],
//     set: undefined,
//     enumerable: false,
//     configurable: false
// }

当然,也可以通过Object.defineProperties为对象设置多个属性:

// 为obj一次性添加a、b、c三个属性
Object.defineProperties(obj, {
    a: { value: 'a' },
    b: { value: 'b', writable: true, enumerable: true, configurable: true },
    c: { get() { return 'c' }, enumerable: true }
})
console.log(obj.a, obj.b, obj.c); // a b c
// 这里暂时屏蔽了obj对象上面x、y、z的定义
console.log(Object.getOwnPropertyDescriptors(obj))
// {
//     a: {
//         value: 'a',
//         writable: false,
//         enumerable: false,
//         configurable: false
//     },
//     b: { value: 'b', writable: true, enumerable: true, configurable: true },
//     c: {
//         get: [Function: get],
//         set: undefined,
//         enumerable: true,
//         configurable: false
//     }
// }

最后一起看看对象obj遍历情况:

for (const key in obj) {
    console.log(key, ': ', obj[key])
}
// x: 2
// b: b
// c: c

当然,如果对象的某个属性特性是允许配置的,那么可以转变此属性的类型,即数值型和访问器类型之间转换,比如通过下面的操作将objx数值型属性设置为访问器型属性:

Object.defineProperty(obj, 'x', { 
    get() {
        return 10
    } 
})

到这里,可能有小伙伴问了,那我一个对象,能不能不允许其他伙伴定义属性或者改动呢?查一查MDN必须有,下面我们就来聊一聊对象的可扩展性

4. 对象的可扩展性

简单说,就是如果对象是不允许修改或者扩展的,那么调用Object.definePropertie或者Object.defineProperties时就会报错抛出TypeError。可以理解为如果对象的属性特性是不可写或者不可配置、没有set访问,或者对象本身处于不可扩展状态时就无法定义新属性或者修改属性特性了。

对象提供了三个方法来处理对象本身的可扩展性,三个方法对对象的可扩展性限制依次增强,不过需要注意的是,这些方法只修改对象本身,不会影响此对象的原型,也就是说只改变传给这三个方法的对象,而不影响这个对象的父类(原型)。

4.1 extensible是否可扩展

  • 是否允许对象新增属性
  • Object.isExtensible(obj)判断对象obj是否可以扩展
  • Object.preventExtensions(obj)设置对象obj不可以扩展,ps:此设置不可逆,即设置不可扩展后不能设置为可以扩展
  • 对象默认是可以扩展的
  • 即使对象不可以扩展,如果对象的属性特性允许修改配置,那么这个属性还是可以修改的
Object.preventExtensions(obj)
Object.defineProperty(obj, 'newKey', { value: 'newValue' })
// TypeError: Cannot define property newKey, object is not extensible

4.2 seal是否封存

  • 是否允许对象新增、删除和配置属性
  • Object.seal(obj)设置对象obj封存,ps:此设置也不可逆
  • Object.isSealed(obj)判断对象obj是否处于封存状态
  • 对象封存后不允许新增、删除和配置属性
Object.seal(obj)
Object.defineProperty(obj, 'newKey', { value: 'newValue' })
// TypeError: Cannot define property newKey, object is not extensible
Object.defineProperty(obj, 'x', { value: 'newX', writable: true })
console.log(Object.getOwnPropertyDescriptor(obj, 'x'))
// {
//     value: 'newX',
//     writable: true,
//     enumerable: true,
//     configurable: false  // 通过seal设置后可配置变成了false
// }
Object.defineProperty(obj, 'x', { value: 'newX', writable: true, configurable: true })
// TypeError: Cannot redefine property: x
// 设置seal后对象的x属性不可修改配置,也就无法重新定义

如果对象属性设置的可写(writable:false)可遍历(configurable:false)则不影响

Object.seal(obj)
Object.defineProperty(obj, 'x', { value: 'newX', enumerable: true })
for (const key in obj) {
    console.log(key, ': ', obj[key])
}
// x :  newX

4.3 freeze是否冻结

  • 是否允许对象新增、修改、删除和配置属性
  • Object.isFrozen(obj)判断对象obj是否处于冻结状态
  • Object.freeze(obj)设置对象obj冻结,ps:此设置也不可逆
  • 冻结对象后,不可新增属性,对象的数值型属性变成只读状态(即writable:falseconfigurable:false
console.log('Object.isFrozen(obj): ', Object.isFrozen(obj)) // false
Object.freeze(obj)
Object.defineProperty(obj, 'x', { value: 'newX' })
// TypeError: Cannot redefine property: x

但是如果对象有访问器型属性,那么此属性已经设置的getset不受影响

Object.defineProperty(obj, 'd', {
    get() {
        return 'ddd'
    },
    set(value) {
        console.log('set value: ', value)
    }
})
console.log(obj.d) // ddd
obj.d = 'newDDD' // set value:  newDDD
Object.freeze(obj)
obj.d = 'newDDD123' // set value:  newDDD123

到此,对象的属性和特性就聊完了,最后复习一道面试题:

Object.assign是做什么的?实现一个assignDescriptor方法拷贝对象的属性特性

我们都知道assign实现对象的拷贝,并且是浅拷贝,如果深拷贝肯定会想到JSON.parse方式,但是assign方法有个缺陷,就是只拷贝可枚举属性和值,并且不拷贝属性的特性,这就意味这如果对象有访问器型属性,那么拷贝的只是这个属性的值,而非属性的get或者set函数。既然这样,我们可以给Object定义一个assignDescriptor方法,实现对象的属性特性拷贝

首先我们给obj对象添加一个xx属性,返回x属性的递增数值:

Object.defineProperty(obj, 'xx', {
    get() {
        return this.x++ // x属性前面已经定义
    }
})
console.log(obj.x, obj.xx) // 2 2 之后`x`的值是3
const assignX = Object.assign({}, obj)
console.log(assignX.x, assignX.xx) // 3 undefined
console.log(assignX.x, assignX.xx) // 3 undefined 没有递增,保持3不变

下面我们定义assignDescriptor方法来实现递增拷贝:

Object.defineProperty(Object, 'assignDescriptor', {
    writable: true,
    enumerable: false,
    configurable: true,
    value: (target, ...sources) => {
        for (const source of sources) {
            for (const key of Object.getOwnPropertyNames(source)) {
                const descriptor = Object.getOwnPropertyDescriptor(source, key)
                Object.defineProperty(target, key, descriptor)
            }
            for (const key of Object.getOwnPropertySymbols(source)) {
                const descriptor = Object.getOwnPropertyDescriptor(source, key)
                Object.defineProperty(target, key, descriptor)
            }
        }
        return target
    }
})

最后我们通过自定义方法来看看拷贝结果:

Object.defineProperty(obj, 'xx', {
    get() {
        return this.x++
    }
})
console.log(obj.x, obj.xx) // 2 2
const assignX = Object.assignDescriptor({}, obj)
console.log(assignX.x, assignX.xx) // 3 3
console.log(assignX.x, assignX.xx) // 4 4

可以看到对属性特性的拷贝是成功的,不过assin满足大部分需要了。好了,到这里关于对象的属性和特性就先聊到这里,如果有问题,欢迎留言"~"