一. 前言
一转眼,发现自己大半年没有更新文章了。最近过的太安逸,没有什么危机感。感觉自己越来越躺平,技术上面也开始退步。思来想去还没到可以躺平的时候(穷,没钱😭😭
),还是认认真真的更新文章,多多巩固前端基础和学习新的技术。
考虑的马上也是面试的好时候了(我说的是以前哈
)。写一点js相关的基础文章吧,也是自己的一些沉淀。欢迎各位jym来查缺补漏。

二. 对象的几种创建方式
2.1 字面量创建
这个方式是我们平时用的最多的,通过{}
创建。
js
const p = {
name: 'zztj',
age: 20
}
内部原理:
- 分配内存,创建一个普通对象,然后继承
object.prototype
。 - 直接继承
object.prototype
上的原始属性。 - 创建的属性会直接挂载到对象本身。

2.2 Object.create
基于现有的对象作为原型,然后创建对象。
内部原理:
- 创建一个新对象,将该对象的prototype指向传入的第一个参数(此为必传)
js
Object.create(proto, propertiesObject)
1. 参数1: proto创建该对象的原型。(必须为object | null)
2. 参数2: 定义新对象的自身属性。(要满足Object.defineProperty()的参数格式)
下面用几个例子来看一下用法。
2.2.1 创建对象(无原型继承)
php
const obj = Object.create(null, { name: { value: "Test", writable: true } })

可以看出他的原型上啥都没。
2.2.2 创建对象(继承)
php
const obj = Object.create(null, { name: { value: "11111", writable: true } })
const obj2 = Object.create(obj, { age: { value: 20, writable: true } })

可以看出他的原型上有name属性。
2.3 构造函数
通过new
关键字,调用构造函数,创建对象。
内部原理:
创建对象
:在内存中创建一个新的空对象 {}继承原型
:将该对象的__proto__指向创建该对象构造函数的protoType初始化属性
:将构造函数的this指向,指向该对象。(初始化构造函数内部属性)返回结果
:若该构造函数没有返回值,则返回该对象。
js
function Person(name, age) {
this.name = name
this.age = age
}
const obj1 = new Person('ztmj', 20)

这种构造函数创建对象的原型链结构是这样的:
js
obj1.__proto__ === Person.prototype
Person.prototype.__proto__ === Object.prototype
Object.prototype.__proto__ === null
2.4 工厂函数
通过封装一个函数,用来创建对象,然后直接返回该对象。
内部原理:
- 手动创建对象并返回,不依赖
new
or原型链
- 每次调用该函数,都会产生新的对象,所以属性和方法,都是该对象本身的。
js
function createObjectFactory(name, age) {
const obj = {}
obj.name = name
obj.age = age
}
三. 对象属性修饰符
在js中,通过属性的修饰符可以精准控制
对象属性的行为
。 通过Object.defineProperty
可以修改修饰符。 主要通过以下几个修饰符来控制。
3.1 value
属性的值。
js
const obj1 = { name: 'ztmj' }
Object.getOwnPropertyDescriptor(obj1, 'name') // { value: "ztmj"}
3.2 writable
该属性是否可以被修改。默认是true。如果手动修改为false,则该属性不可修改。
js
const obj1 = { name: 'ztmj' }
Object.getOwnPropertyDescriptor(obj1, 'name') // { writable: true}
3.3 enumerable
该属性是否可以被枚举。如果为false,对象的某些操作会忽略
当前属性。
下面来列举一下,哪些操作会忽略??
先创建一个对象,把里面的一个属性设置为不可枚举
。
js
const obj = { name: 'ztmj', age: 20}
Object.defineProperty(obj, 'age', {
enumerable: false
})
Object.getOwnPropertyDescriptors(obj) // 看一下是否修改成功

我们可以看到,age的enumerable变成了false
。继续下一步:
for...in
js
for (let key in obj) {
console.log(key)
}

可以看到,age没有被遍历到。
Object.keys()
js
Object.keys(obj) // 结果只有[name]

JSON.stringify()
js
JSON.stringify(obj)

Object.assign()
js
const obj2 = { sex: 'man' }
Object.assign(obj2, obj1) // {sex: 'man', name: 'ztmj'}
以上4种方法,不可枚举属性无法处理。
PS: 这里要注意一下: ES6规定,所有Class的原型的方法都是不可枚举的
。
js
const A = class {
func() {}
}
Object.getOwnPropertyDescriptor(A.prototype, 'func').enumerable // false
四. 属性的遍历
4.1 遍历方法
既然提到了对象的属性修饰符,那就说一下属性的遍历吧。
在js中常用的属性遍历方法有5种。
for...in
遍历对象自身,和继承的可枚举
属性(不包含Symbol
属性)Object.keys(obj)
遍历对象自身(不含继承
)的可枚举的属性(不包含Symbol
属性),返回一个数组
。Object.getOwnPropertyNames(obj)
遍历对象自身(不含继承
)的所有属性(不包含Symbol
属性),返回一个数组
。Object.getOwnPropertySymbols(obj)
遍历自身的所有symbol
属性。返回一个数组
Reflect.ownKeys(obj)
遍历自身的所有
属性,啥样都行。
看起来有点混乱,其实就三点:
继承的原型是否可以遍历
不可枚举属性是否可以遍历
symbol属性是否可以遍历
看一下例子来说明一下上面的区别:
js
const instance = {
name: 'ztmj',
age: 20,
sex: 'man',
[Symbol('get')]: 'gets', // 创建symbol属性
hb: 'ball'
}
Object.defineProperty(instance, 'sex', { enumerable: false })
const sub = { other: 'other' }
instance.__proto__ = sub // 原型上加属性
for (let key in instance ) { console.log(key) } // name age hb other
Object.keys(instance) // (3) ['name', 'age', 'hb']
Object.getOwnPropertyNames(instance) // (4) ['name', 'age', 'sex', 'hb']
Object.getOwnPropertySymbols(instance) // [Symbol(get)]
Reflect.ownKeys(instance) // (5) ['name', 'age', 'sex', 'hb', Symbol(get)]
总结一下:来个非常抽象的图吧。

5.1 遍历的属性顺序
对象遍历也是分先后顺序的。
step1: 先遍历属性名为数值
的属性。按照数字的大小进行遍历顺序(从小到大)
step2: 然后所有属性名为字符串
的属性。按照生成时间排序
step3: 最后是symbol
类型。按照生成时间排序
举个例子:
js
const time = {
name: 'ztmj',
age: 20,
[Symbol('get')]: 'gets',
hb: 'ball',
'2': '2222',
18: '18',
5: '55555',
sex: 'man',
[Symbol('post')]: 'posts', // 创建symbol属性
}
Object.getOwnPropertyNames(time) // ['2', '5', '18', 'name', 'age', 'hb', 'sex']
五. Object.is()
这个其实用的不多(反正俺用的很少。。。),但是还是要单独说一下。
主要是用于比较两个值是否真正意义上的全等
。 常用的 ==
和 ===
是有缺陷的。
==
会自动转换数据类型
===
无法比较 NaN 和 -0 、 +0
js
1 == '1' // true
NaN === NaN // false
-0 === +0 // true
其实在ES6提出"Same-value equality"
(同值相等)算法,用来解决这个问题。Object.is
就是部署了这个方法。 他与 ===
的区别在于可以正确的比较 NaN 和 -0 、 +0
。
js
Object.is(NaN, NaN) // true
Object.is(-0, +0) // false
六. Object.assign()
6.1 基本用法
Object.assign
方法用于对象的合并
,将源对象的所有可枚举
属性,复制到目标对象。 (这里注意不可枚举的属性,是不能合并的。
)
js
let t = { a: 1 };
let s1 = { b: 2, c: 3 };
Object.defineProperty(s1, 'c', { enumerable: false })
Object.assign(t, s1); // {a: 1, b: 2}
6.2 覆盖原则
如果在合并的过程中,出现属性的同名,则后面的属性值会覆盖前面的属性值
。
js
let t = { a: 1, test: 'test1' };
let s1 = { test: 'test2', b: 2 };
let s2 = { test: 'test3'};
Object.assign(t, s1, s2) // {a: 1, test: 'test3', b: 2}
Object.assign(t, s2, s1) // {a: 1, test: 'test2', b: 2}
6.3 转换原则
如果第一个参数接收的为非对象类型
,会先进行转换成对象
,然后在执行下一步。
css
let t = '1'
let s = { a: 'a'}
Object.assign(t, s) // String {'1', a: 'a'}
如果当前类型不能转为对象就会error
javascript
let t = null
let s = { a: 'a'}
Object.assign(t, s) // Uncaught TypeError: Cannot convert undefined or null to object
如果其他位置
,出现非对象类型
, 处理流程会有所不同
。
- 还是把
非对象类型
转为对象
。不同的是,不能转的
,直接就会跳过
。所以传入null、undefined就不会报错。
js
let t = null
let s = { a: 'a'}
Object.assign(s, t) // {a: 'a'}
- 但是,除了
字符串会
以类数组
形式,合并入对象,其他值都不会产生效果。也是直接忽略。
js
let t = null
let t2 = true
let t3 = 'abc'
let t4 = '111'
let s = {a: 'a'}
Object.assign(s, t, t2, t3, t4) // {0: '1', 1: '1', 2: '1', a: 'a'}
这里有点奇怪的地方: 为什么'abc'没有了,按照类数组的概念,结果应该是{ 0: 'a', 1: 'b', 2: 'c', 3: 1, 4: 1, 5: 1, a: 'a' }
。答案还是替换原则。相当于,已经占了位,前面的0、1、2
属性已经存在,后面的就会替换掉
。
我们可以替换一下位置看看:
js
let t = null
let t2 = true
let t3 = 'abc'
let t4 = '111'
Object.assign(s, t, t2, t4, t3) // {0: 'a', 1: 'b', 2: 'c', a: 'a'}
PS:为什么只有字符串可以??
因为只有字符串的包装对象,会产生可枚举属性

七. 总结
简单的总结一下js里面的对象。不算难,也算是对自己知识的总结。欢迎大家在评论区多多交流,有问题欢迎指出!