JS高级之Object类型的深入了解

570 阅读10分钟

这是我参与11月更文挑战的第18天,活动详情查看:2021最后一次更文挑战

大家好,我是一碗周,一个不想被喝(内卷)的前端。如果写的文章有幸可以得到你的青睐,万分有幸~

概述

Object是JavaScript中所有对象的父级对象,这意味着我们创建的所有对象都继承于此。

在JavaScript中,几乎所有的对象都是Object类型的实例,它们都会从Object.prototype继承属性和方法。Object构造函数为给定值创建一个对象包装器。Object构造函数,会根据给定的参数创建对象,具体有以下情况:

  • 如果给定值是nullundefined,将会创建并返回一个空对象
  • 如果传进去的是一个基本类型的值,则会构造其包装类型的对象
  • 如果传进去的是引用类型的值,仍然会返回这个值,经他们复制的变量保有和源对象相同的引用地址

当以非构造函数形式被调用时,Object的行为等同于new Object()。创建对象的语法结构如下所示:

// 通过字面量方式
var 对象名 = Object
// 通过 new 关键字
var 对象名 = new Object()

如下代码展示了如何创建一个对象,代码如下:

// 1. 创建空对象
var obj1 = new Object(undefined)
var obj2 = new Object(null)
console.log(obj1) // {}
console.log(obj2) // {}

// 2. 如果传进去的是一个基本类型的值,则会构造其包装类型的对象
var obj3 = new Object(100)
var obj4 = new Number(100)
console.log(obj3) // [Number: 100]
console.log(obj4) // [Number: 100]

属性描述符

JavaScript提供了一个内部数据结构,用于描述对象的值,控制其行为,例如该属性是否可写、可读、可配置、是否可修改以及是否可枚举等。这个内部数据结构被称为属性描述符。

每个属性都有自己对应的属性描述符,保存该属性的元信息。

对象里目前存在的属性描述符有两种主要形式:数据描述符存储描述符

数据描述符是一个具有属性的值,该值是可写的,也可能是不可写的。数据描述符具有以下可选键值:

  • value:该属性对应的值。可以是任何有效的JavaScript值(数值,对象,函数等)。默认为undefined
  • writable:当且仅该属性的值为true时,value才能被赋值运算符所改变。默认为false
  • configurable:当且仅当该属性的值为true时,该属性描述符才能够被改变,同时该属性也能从对应的对象上被删除。默认为false
  • enumerable:当且仅当该属性的值为true时,该属性才能够出现在对象的枚举属性中。默认为false

存取描述符是由gettersetter函数对描述的属性。存储描述符具有以下可选键值:

  • get:给属性提供getter的方法,如果没有getter则为undefined。当访问该属性时,该方法被执行,方法执行是没有参数传入,但是会传入this对象。
  • set:给属性提供setter的方法,如果没有setter则为undefined。当属性值修改时,触发执行该方法。该方法接受唯一参数,即该属性新的参数值。
  • configurable:当且仅当该属性的值为true时,该属性描述符才能够被改变,同时该属性也能从对应的对象上被删除。默认为false。
  • enumerable:当且仅当该属性的值为true时,该属性才能够出现在对象的枚举属性中。默认为false

获取属性描述符

Object对象提供的**getOwnPropertyDescriptor()**方法返回指定对象上一个自有属性对应的属性描述符。

语法结构如下所示:

Object.getOwnPropertyDescriptor(obj, prop)

其中的obj参数表示要查找的对象,prop参数表示要查找的对象内属性的名称,该方法的返回值是具体属性的描述符对象,如果没有找到返回undefined

示例代码如下所示:

var obj = new Object({
  name: '一碗周',
})
var result = Object.getOwnPropertyDescriptor(obj, 'name')
console.log(result)

代码执行结果如下:

{
  value: '一碗周',
  writable: true,
  enumerable: true,
  configurable: true
}

既然返回的是一个对象,是对象就存在属性,可以通过其属性获取其相应的值,示例代码如下:

// * 获取该属性的值
console.log(result.value) // 一碗周
// * 返回该属性是否可枚举
console.log(result.enumerable) // true

设置属性描述符

设置属性描述符可以Object提供的defineProperty()方法或者defineProperties()方法来设置具体的属性描述符,具体代码如下:

/*
 * 通过 Object.defineProperties(obj, props) 为 obj 对象增加属性。
 * obj: 在其上定义或修改属性的对象。
 * props: 要定义其可枚举属性或修改的属性描述符的对象。
 */
let person = {}

Object.defineProperties(person, {
  name: {
    // 属性值
    value: '一碗周',
    // 可修改可删除
    configurable: true,
    // 不可枚举
    enumerable: false,
    /*
    我们知道循环遍历语句遍历对象的属性有三种方式,如下所示:
    1. for...in 语句:遍历该对象的可枚举属性
    2. Object.keys() 方法:该方法返回一个数组,该数组包含了该对象中所有自有可枚举的属性名称。
    3. Object.getOwnPropertyNames() 方法:该方法返回一个数组,该数组包含了该对象中所有属性名称。
    */
    // 设置不可枚举后就不可以通过 for...in 语句遍历到,也不可通过 Object.keys() 获取到
  },
  age: {
    value: 18,
    // 其他项不写默认为false
  },
})

/* 
* 调用Object.defineProperty(obj, prop, desc)方法
  * 作用
    * 用于定义目标对象的新属性
    * 用于修改目标对象的已存在属性
  * 参数
    * obj - 表示目标对象
    * prop - 表示目标对象的目标属性名称
    * desc - 表示属性描述符,必须是对象格式
      {
          value : 值
      }
  * 返回值 - 返回传递的对象
*/

Object.defineProperty(person, 'hobby', {
  value: 'coding',
  // 可枚举
  enumerable: true,
})
Object.defineProperty(person, 'sayMe', {
  value: function () {
    console.log('一碗周')
  },
  // 可枚举
  enumerable: true,
})
console.log(person) // { hobby: 'coding', sayMe: [Function: value] }
// 最后结果之后一个属性一个方法的原因是前面几项都不可枚举
person.sayMe() // 一碗周

属性描述符存储器

对象的属性除了可以直接定义以外,还可以使用存取器进行定义。其中setter为存值函数,使用属性描述符中的set;getter为取值函数,使用属性描述符中的get

存取描述符中的get()类似于类似于数据描述符中的valueget()方法在被调用时,不能传递任何参数,但是可以传递this关键字(表示当前目标对象,不过不能调用对象的当前目标属性。)

存取描述符中的set()用于实现当前目标属性的的修改作用,该方法接受一个唯一参数,作为当前目标属性的新值。

由此我们可以通过一个辅助变量来帮助我们对其进行赋值和获取操作,示例代码如下:

var obj = {}
// 辅助变量,用于赋值和获取操作
var value = '一碗粥'

Object.defineProperty(obj, 'name', {
  get: function () {
    // 通过辅助变量获取属性值
    return value
  },
  set: function (newValue) {
    // 通过修改辅助变量,完成赋值操作
    value = newValue
  },
})
console.log(obj.name) // 一碗粥
obj.name = '一碗周'
console.log(obj.name) // 一碗周

JavaScript中除了提供上述写法,还提供了另一种写法,如下所示

// 辅助变量,用于赋值和获取操作
var value = '一碗粥'

var obj = {
  // 存取描述符中的get
  get attr() {
    // 表示当前目标属性名称
    return value
  },
  // 存取描述符中的set
  set attr(newValue) {
    // 表示当前目标属性名称
    value = newValue
  },
}
console.log(obj.attr) // 一碗粥
obj.attr = '一碗周'
console.log(obj.attr) // 一碗周

防篡改对象

防篡改是什么

定义的对象默认在任何时候、任何位置,无论有意义的还是无意义的都可以修改对象的属性或方法。而这些篡改可能会影响对象的内置属性或方法,从而导致对象的正常功能可能无法使用。JavaScript在 ECMAScript5版本中新增了放置篡改对象的属性或方法的机制,共提供了以下三级保护方式:

  1. 禁止扩展:禁止为对象扩展新的属性或方法
  2. 密封对象:禁止扩展新的属性或方法,禁止配置现有的属性或方法的描述符,仅允许读写属性的值。
  3. 冻结对象:禁止对对象执行任何修改操作。

值得注意的是,一旦定义了防篡改对象,就无法撤销了,也就是说无法改回了。

禁止扩展

所谓的禁止扩展,就是禁止为对象扩展新的属性或方法。

Object 对象提供的preventExtensions(obj)方法让一个对象变的不可扩展,也就是永远不能再添加新的属性,该方法会返回已经不可扩展的对象。

如果一个对象可以添加新的属性,则这个对象是可扩展的。Object.preventExtensions()将对象标记为不再可扩展,这样它将永远不会具有它被标记为不可扩展时持有的属性之外的属性。注意,一般来说,不可扩展对象的属性可能仍然可被删除。尝试将新属性添加到不可扩展对象将静默失败或抛出TypeError异常。

示例代码如下:

var obj = {}

// 将对象设置为禁止扩展
Object.preventExtensions(obj)

// 新增属性或方法无效,但是语法并不报错
obj.name = '一碗周'
console.log(obj)
/*
  通过 Object.defineProperty() 方法新增属性
   * 抛出异常, 提示信息: TypeError: Cannot define property: name, object is not extensible.
*/
Object.defineProperty(obj, 'name', {
  value: '一碗粥',
})

Object对象还提供了isExtensible(obj)方法,此方法用于判断一个对象是否可扩展,返回一个Boolean值,可扩展为true,不可扩展为false

密封对象

所谓密封对象,就是指禁止扩展新的属性或方法,禁止配置现有的属性或方法的描述符(包括两种,一种是writable修改此描述符不会报错,但是会修改失败,另一种包括configurableenumerable两个描述符,修改此描述符会报错)。

Object对象提供的seal(obj)方法用于封闭对象。也提供了isSealed(obj)方法用于检测一个对象是否是密封对象。

示例代码如下:

var obj = {
  name: '一碗周',
}
// 封闭对象
Object.seal(obj)
// 新增属性
obj.age = 18
console.log(obj) // 无效,但是并不报错
console.log(Object.getOwnPropertyDescriptor(obj, 'name'))
// 修改属性描述符
Object.defineProperty(obj, 'name', {
  configurable: true, // 抛出异常 TypeError: Cannot redefine property: name
})

console.log(Object.isSealed()) // true

冻结对象

所谓的冻结对象就是指禁止对对象进行任何操作。

Object.freeze(obj)方法可以冻结一个对象。一个被冻结的对象再也不能被修改;冻结了一个对象则不能向这个对象添加新的属性,不能删除已有属性,不能修改该对象已有属性的可枚举性、可配置性、可写性,以及不能修改已有属性的值。此外,冻结一个对象后该对象的原型也不能被修改。

同样的,Object.isFrozen(obj)方法用于检测是否为冻结对象。

这里需要提一下的是,一个冻结的对象既是一个密封对象,也是一个不可扩展对象。

示例代码如下所示:

var obj = {
  name: '一碗粥',
}

// 冻结对象
Object.freeze(obj)
// 修改属性
obj.name = '一碗周'
// 新增属性
obj.age = 18

console.log(obj) // 无效,但是并不报错
// 修改属性描述符
Object.defineProperty(obj, 'name', {
  value: '掘金', // 抛出异常 TypeError: Cannot redefine property: name
})

// 一个冻结的对象既是一个密封对象,也是一个不可扩展对象。
console.log(Object.isFrozen()) // true
console.log(Object.isExtensible()) // false
console.log(Object.isSealed()) // true

总结

本篇文章介绍了如何通过Object去创建对象,还介绍了属性描述符以及防篡改对象是什么。

写在最后

这是【从头学前端】系列文章的第四十八篇-《Object的扩展》,如果你喜欢这个专栏,可以给我或者专栏一个关注~

本系列文章在掘金首发,编写不易转载请获得允许

往期推荐