万事万物皆对象
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为falseconsole.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