温故而知新系列(四)深入浅出认识对象第一篇

667 阅读10分钟

万事万物皆对象

ECMA-262 将对象定义为一组属性的无序集合

我们在定义对象的时候,常常使用对象字面量来定义,这种方式的定义只是简单的给了键值关系,不能对属性进行深一层的操作和控制,例如控制属性是否可以被删除,属性是否能在for...in中遍历中来,属性是否可写等等,默认以上操作都是可以的,但如果你不想让某个属性拥有这些操作权限该怎么办呢?JavaScript为我们提供了精细化操作对象属性的方法,也就是Object.defineProperty()Object.defineProperties()

// 对象字面量
let obj = {
  name: 'jack'
}

一、对象由浅入深

1. 属性的分类

对象的属性分为两种:数据属性和访问器属性

数据的属性也是有特性的,通过属性描述符定义

1.1 数据属性

数据属性有4个数据属性描述符:configurable、enumerable、writable、value

let obj = {
  name: 'jack',
  age: 18
}

Object.defineProperty(obj, 'address', {
  value: 'Bei Jing',
  configurable: false,
  enumerable: true,
  writable: true
})

/**
 * 1.configurable
 * 表示属性是否可以通过delete删除属性,是否可以修改他的特性,或者是否可以将它修改为存取属性描述符
 * 直接在对象上定义默认为true,通过属性描述符定义默认为false
 */
//因为configurable: false,不可配置,无效的配置
Object.defineProperty(obj, 'adress', {
  value: 'tt',
  configurable: true
})
//无法删除address属性
delete obj.address
console.log(obj.address) //BeiJjing

/**
 * 2.enumerable
 * 表示属性是否可以通过for-in或者Object.keys()返回该属性
 * 直接在对象上定义默认为true,通过属性描述符定义默认为false
 */
console.log(obj) //{ name: 'jack', age: 18, address: 'Bei Jing' }
console.log(Object.keys(obj)) //[ 'name', 'age', 'address' ]

/**
 * 3.writable
 * 表示是否可以修改属性的值
 * 直接在对象上定义默认为true,通过属性描述符定义默认为false
 */
obj.address = 'Tian Jin'
console.log(obj.address) //Tian Jin
/**
 * 4.value
 * 属性的value值,读取属性时会返回该值,修改属性时会对其进行修改
 * 默认值: undefined
 */
Object.defineProperty(obj, 'friend', {})
console.log(obj.friend) //undefined

对象字面量的定义方式,实际上定义的是数据属性,值就是value描述符存的值,其它三个描述符全部为true

1.2 访问器属性

访问器属性有也有4个属性描述符:configurable、enumerable、get、set

/**
 * 存储属性描述符的作用:
 * ①隐藏某一个私有属性不希望直接被外界使用和赋值
 * ②如果我们希望截获某一个属性他访问和设置的过程,也可以使用存储属性描述符
 * vue2 的响应式就是利用的存储属性描述符来实现的
 */
var obj = {
  name: 'li',
  age: 18,
  //私有属性(社区默认以_开头的是私有属性),没有严格意义的私有属性,Typescript中有
  _address: '昆明市'
}

Object.defineProperty(obj, 'address', {
  configurable: true,
  enumerable: true,
  get() { //获取属性的时候执行
    foo()
    return this._address
  },
  set(v) { //设置属性的时候执行
    bar()
    this._address = v
  }
})
console.log(obj.address)
obj.address = '曲靖市'
console.log(obj.address)

function foo() {
  console.log('获取了一次值')
}

function bar() {
  console.log('修改了一次值')
}

除此之外,每个对象其实都有一个隐式原型对象[[prototype]],如果是函数还有一个显示原型对象prototype,后面也会开专题专门讲解这两个属性,原型原型链可谓是JavaScript的一个核心了

2. 同时定义多个属性

使用Object.defineProperties

Object.defineProperties(obj, {
  name: {
    configurable: true,
    enumerable: true,
    writable: true,
    value: 'li',
  },
  age: {
    configurable: false,
    enumerable: false,
    get() {
      return this._age
    },
    set(v) {
      this._age = v
    }
  }
})

实际使用中感觉其实很少会这样来定义属性,在写一些工具类库的时候可能会用到吧

3. 对属性的限制方法

// 测试对象
let obj = {
  name: 'jack',
  age: 18
}
3.1 禁止扩展

​ 👉Object.preventExtensions()用于禁止对象扩展新属性

​ 👉Object.isExtensible()用于判断对象是否可扩展属性

Object.preventExtensions(obj)
obj.address = '北京'
console.log(obj.address) //undefined
console.log(Object.isExtensible(obj)) //false
3.2 密封

👉Object.seal()用于禁止配置属性特性(无法删除属性),实际调用的是Object.preventExtensions(),并且将现有的属性configurable特性置为false

👉Object.isSealed()用于判断对象是否被密封

Object.seal(obj)
delete obj.name
console.log(obj.name) //jack
console.log(Object.isSealed(obj)) //true
3.3 冻结

👉 Object.freeze()用于禁止修改属性,实际调用的是Object.seal(),并将现有属性的writable置为false

👉Object.isFrozen()用于判断对象是否被冻结

Object.freeze(obj)
obj.name = 'jj'
console.log(obj.name) //jack
console.log(Object.isFrozen(obj)) //true

4. 获取对象的属性描述符

  • Object.getOwnPropertyDescriptor()

    对于访问器属性包含 configurable、enumerable、get 和 set 属性,对于数据属性包含 configurable、enumerable、 writable 和 value 属性。

  • Object.getOwnPropertyDescriptors()

    实际上 会在每个自有属性上调用 Object.getOwnPropertyDescriptor()并在一个新对象中返回它们

console.log(Object.getOwnPropertyDescriptor(obj, 'age')) //获取age属性的属性描述符
console.log(Object.getOwnPropertyDescriptors(obj)) //获取所有属性描述符

5. 获取对象的属性

// 5、6、7小结均使用该对象进行测试
const obj = {
  name: 'jack',
  age: 19,
  foo() {},
  [Symbol('symAttr')]: 'Symbol Attribute'
}
Object.defineProperty(obj, 'info', {
  value: 'hello',
  enumerable: false
})
Object.defineProperty(obj, Symbol('s2'), {
  value: 'Symbol 2',
  enumerable: false
})

//原型链上的属性
obj.__proto__.protoAttr1 = 'proto Attr1'
obj.__proto__[Symbol('protoSym')] = 'proto Attribute'
Object.defineProperty(obj.__proto__, 'protoAttr2', {
  value: 'proto Attr2',
  enumerable: false
})
5.1 Object.keys()

返回一个包含所有给定对象自身可枚举属性名称的数组,原型链上的属性不会获取

console.log(Object.keys(obj))  //[ 'name', 'age', 'foo' ]
5.2 Object.getOwnPropertyNames()

Object.keys()稍有不同,不可枚举属性也能获取

console.log(Object.getOwnPropertyNames(obj)) //[ 'name', 'age', 'foo', 'info' ]
5.3 Object.getOwnPropertySymbols()

返回一个数组,它包含了指定对象自身所有的符号属性包括不可枚举属性

console.log(Object.getOwnPropertySymbols(obj))  //[ Symbol(s1), Symbol(s2) ]
5.4 for...in

使用for...in遍历对象时,会采用原型链查找方式,即任何可以通过原型链访问到的可枚举属性都会被遍历到

const arr = []
for (const item in obj) {
  arr.push(item)
}
console.log(arr) //[ 'name', 'age', 'foo', 'protoAttr1' ]
5.5 in

使用in操作符来检查属性在对象中是否存在时,同样会查找整个原型链,无论是否可枚举

console.log('protoAttr2' in obj) //true

6. 获取对象的值

6.1 Object.values()

返回给定对象自身可枚举属性值的数组,不包括符号属性

console.log(Object.values(obj)) // [ 'jack', 19, [Function: foo] ]
6.2 for...of

需要对可迭代对象才能使用,后面还会对迭代器和生成器单独进行复习,暂时以String这个内置包装对象类型为例

const str = 'abcde'
const res = []
for (const item of str) {
  res.push(item)
}
console.log(res) //[ 'a', 'b', 'c', 'd', 'e' ]

其实第一步创建的只是一个原始值类型的字符串,当进行for...of操作时,实际上是将str隐式包装为对象,获取对象中的迭代器str[Symbol.iterator](),调用其next()方法进行遍历。

const iterator = str[Symbol.iterator]()
console.log(iterator.next()) //{ value: 'a', done: false }
console.log(iterator.next()) //{ value: 'b', done: false }
console.log(iterator.next()) //{ value: 'c', done: false }
console.log(iterator.next()) //{ value: 'd', done: false }
console.log(iterator.next()) //{ value: 'e', done: false }
console.log(iterator.next()) //{ value: undefined, done: true }

7. 获取对象的entries

返回给定对象自身可枚举属性的 [key, value] 数组

console.log(Object.entries(obj))
// [ [ 'name', 'jack' ], [ 'age', 19 ], [ 'foo', [Function: foo] ] ]

补充一个:Object.fromEntries()还可以通过entries获取一个全新对象,这是ECMAScript2019的新语法

let arr = [
  ['name', 'jack'],
  ['age', 19],
  [
    'foo',
    function () {
      console.log('foo: ', this.name)
    }
  ]
]
let newObj = Object.fromEntries(arr)
newObj.foo() //foo: jack
console.log(newObj) //{ name: 'jack', age: 19, foo: [Function (anonymous)] }

运用场景:将url中的query转换为对象使用

const queryStr = 'name=jack&age=18'
const queryParams = new URLSearchParams(queryStr)
console.log(queryParams) //URLSearchParams { 'name' => 'jack', 'age' => '18' }
const paramObj = Object.fromEntries(queryParams)
console.log(paramObj) //{ name: 'jack', age: '18' }

二、合并对象

ES6提供了 Object.assign(dest, ...source)方法,接收一个目标对象和一个 1 或多个源对象作为参数,然后将每个源对象中可枚举(Object.propertyIsEnumerable()返回 true) 和自有(Object.hasOwnProperty()返回 true)属性复制到目标对象,该函数返回目标对象。以字符串和符号为键的属性会被复制。对每个符合条件的属性,这个方法会使用源对象上的[[Get]]取得属性的值,然后使用目标 对象上的[[Set]]设置属性的值。

1. 简单复制

let source = {
  name: 'lucy'
}
let dest = {}
let obj2 = Object.assign(dest, source)
console.log(dest) //{name: 'lucy'}
console.log(obj2 === dest) //true

2. 获取函数与设置函数

从源对象访问器属性取得的值,比如获取函数,会作为一个静态值赋给目标对象。换句话说,不能在两个对象间转移获取函数和设置函数,只是将源对象中的getter函数获取到的值传递到目标对象的setter函数的参数上。

let dest = {
  set name(v) {
    console.log('dest中执行setter', v)
  }
}
let s1 = {
  get name() {
    return 'jack'
  }
}
console.log(s1.name)
console.log(Object.assign(dest, s1))
//jack
// dest中执行setter jack
// { name: [Setter] }

console.log(Object.getOwnPropertyDescriptor(dest, 'name'))
//{
//   get: undefined,
//   set: [Function: set name],
//   enumerable: true,
//   configurable: true
// }

可以看出,s1中的getter函数并没有复制到dest中,dest中name属性的get依然是undefined

3. 多源复制

如果有相同的属性,后面复制的属性覆盖前面复制的属性

let dest = {
  set name(v) {
    console.log(v)
  }
}
let s1 = {
  name: 's1 name',
  age: 10
}
let s2 = {
  name: 's2 name'
}
let s3 = {
  name: 's3 name'
}
Object.assign(dest, s1, s2, s3)
// s1 name
// s2 name
// s3 name
// 不过上面的复制并未成功,dest中的name属性依然undefined

4. 非回滚复制

如果赋值期间出错,则操作会中止并退出,同时抛出错误。Object.assign()不会回滚已经进行了的赋值操作,因此它是一个尽力而为、可能只会完成部分复制的方法。

let dest = {
  name: 'lucy'
}
let s1 = {
  name: 'jack',
  get age() {
    throw new Error('this is a error')
  },
  sex: 'male'
}
try {
  Object.assign(dest, s1)
} catch (e) {}
console.log(dest) //{ name: 'jack' }

name属性依然被复制到了dest中,并将其覆盖

5. 对象展开运算符

展开运算符其实也是一个浅拷贝,构建对象时,对于相同的属性,后添加的会覆盖先添加的,数组会转换为索引-值得对象形式

const obj = {
  name: 'jack',
  age: 19
}
const arr = ['a', 'b', 'c']
const newObj = {
  name: 'lucy',
  ...obj,
  ...arr,
  1: 1
}
console.log(newObj) 
//{ '0': 'a', '1': 1, '2': 'c', name: 'jack', age: 19 }

对于对象的拷贝,后面开专题讲对象的浅拷贝、深拷贝怎么实现

三、对象相等判定

区别一下===Object.is()两点不同

  • 对于0,-0,+0的比较,===全为true,Object.is()判断-0和+0为false

    console.log(0 === +0) // true
    console.log(0 === -0) // true
    console.log(-0 === +0) // true
    console.log(Object.is(-0, +0)) // false
    
  • 对于NaN,===全为false,Object.is()判断为true

console.log(NaN === NaN) // false
console.log(Object.is(NaN, NaN)) // true

补充:要检查超过两个值递归的利用相等性传递即可

function checkEqual(x, ...rest) {
  return Object.is(x, rest[0]) && (rest.length < 2 || checkEqual(...rest))
}

四、对象的增强语法

实则是一些语法糖,简化对象字面量的写法,主要包括①属性值简写、②可计算属性、③方法简写

/**
 * Enhanced object literals
 */
let name = 'why'
let age = 18
let foo2 = 'foo2'

//old style
let obj = {
  name: name,
  age: age
}

let obj1 = {
  //属性简写 属性会被解释为同名的属性键
  name,
  age,
  
  foo: function () {
  },
  //method简写 method shorthand
  foo1() {
  },
  
  //计算属性名 computed property names
  [name + '__']: 'jack',
  [Symbol('sym')]:'symbol value',
  //方法简写和计算属性配合使用
  [foo2](){}
}

对于可计算属性,需要注意,中括号内的表达式在执行时可能会涉及闭包带来的副作用,如果抛出任何错误都会终止对象创建,但是抛出错误前的副作用不会回滚,也就可能修改其他数据造成不必要的麻烦,所以在使用时一定要小心!

🌰举个栗子

let num = 10
let addNum = function () {
  try {
    num = num * 10
    throw new Error('this is a error')
    return 'info'
  } catch (e) {}
}

let obj = {
  [addNum()]: 'hello world'
}
console.log(num) //100
console.log(obj) //{ undefined: 'hello world' }

可以看出:对象obj的键没创建成功,还是undefined,但是外层作用域num的值已经变成了100,危险!‼️

五、对象解构

1. 基本使用

可以起别名,设置默认值

const obj = {
  name: 'jack',
  age: 19
}
const { name: nickName, age, sex = 'female', other } = obj
console.log(nickName) //jack
console.log(age) //19
console.log(sex) //sex
console.log(other) //undefined

2. 嵌套解构

属性也是一个对象,可以继续嵌套解构,如果外层属性未定义是不能使用嵌套解构的会报错

const obj = {
  info: {
    name: 'hello'
  }
}
const {
  info: { name: nickName },
  //foo: { bar } TypeError: Cannot read properties of undefined (reading 'bar')
} = obj
console.log(nickName) //hello

3. 部分解构

const obj = {
  name: 'jack',
  age: 19
}
const { name } = obj

4. 参数上下文匹配

在函数参数列表中也可以进行解构赋值,对参数的解构赋值不会影响arguments对象,但可以在函数签名中声明在函数体内使用局部变量

const obj = {
  name: 'jack',
  age: 19
}
function foo(param1, { name, age, sex }, param2) {
  console.log(arguments)
  console.log(name, age, sex)
}
foo('first', obj, 'third')
//[Arguments] {
//   '0': 'first',
//   '1': { name: 'jack', age: 19 },
//   '2': 'third'
// }
// jack 19 undefined

创作不易,求个关注或点赞😄😄🤗