引用类型-对象
# 前言
JavaScript对象本质就是使用{}
包裹的键值对数据结构
# 对象的基础知识
# 1.创建对象的方式
let obj = new Object() // 构造器的方式
let anotherObj = {} // 字面量的方式
2
3
实际应用中,我们会利用对象存储键值对:
let obj = {
name: '小新',
age: 5,
"love palying": "small car" // 这里注意 多字词语必须用引号包裹
do: function () {
console.log("今天没有认真写作业,被妈妈打屁股了!!")
}
}
2
3
4
5
6
7
8
# 2.对象进行读取的操作
读取对象的属性可以这样做:
obj.name // 小新
obj["love playing"] // small car
2
3
使用[]
访问对象属性的好处是 [key]
具有计算属性,即使key动态变化,也可以获取正确的key
// 这里是浏览器环境
let obj = {
name: '小新',
age: 5,
"love palying": 'small car', // 这里注意 多字词语必须用引号包裹
do: function () {
console.log("今天没有认真写作业,被妈妈打屁股了!!")
}
}
let a = prompt("输入你需要查询的属性值","name") // 加入在弹出的输入框中我们输入name
console.log(obj[a]) // small car
2
3
4
5
6
7
8
9
10
11
在对象中定义属性时我们也可以使用计算属性:
// 这里也是浏览器环境
let property = prompt("请输入你想定义的属性名","property")
const obj = {
[property]: "我是被定义的属性的值"
}
console.log(obj) // {property: "我是被定义的属性的值"}
2
3
4
5
6
我们还可以使用+
在定义属性的同时进行一些字符的拼接操作:
// 这里也是浏览器环境
let property = prompt("请输入你想定义的属性名","name")
const obj = {
["my" + property]: "我是被定义的属性的值"
}
console.log(obj) // {myname: "我是被定义的属性的值"}
2
3
4
5
6
# 3.对象没有属性名称限制
前面提到变量名的命名存在限制,保留字和关键字不能用做变量名,但是对象的属性命名其实没有做限制:
let obj = {
for: 1,
return: 2,
while: 3
}
let sum = 0;
for (let item in obj) {
sum += obj[item]
}
console.log(sum) // 正常输出6 没有任何问题
2
3
4
5
6
7
8
9
10
但是强烈不建议这样做,会造成很多令人困惑的地方。
很多内置的属性是禁止开发者定义的,比如_proto_
是禁止用作属性名的
obj._proto_是所有对象内置的一个属性,指向该对象的原型,重新指定属性值会破坏原型链,所以这是不允许的
# 4.使用in操作符检测属性是否存在
我们用undefined
判断属性是否存在:
obj.name === undefined // 注意是三个等于
也可以使用操作赋in
let obj = {
name: '叮当猫'
}
console.log("name" in obj) // true
console.log("age" in obj) //false
2
3
4
5
使用操作符in
时需注意:
- in左侧字符加了引号表示这是一个属性名
- in左侧字符若是没有加引号,则表示这是一个变量,它应该包含需要的属性名
给对象设置属性值是undefined
是不推荐的:
let obj = {
name: undefined
}
console.log(obj.name === undefined) // true 表示属性不存在,但是其实属性是存在的,只不过其值是undefined而已
console.log("name" in undefined) // true 这种情况下in操作符则不会出错
2
3
4
5
6
不要使用undefined作为属性值,想表示空,无的属性值,可以使用null
# 5.使用for...in对对象进行遍历
let obj = {
name: '小新',
age: 3,
isGoodStudent: false
}
for (let item in obj) {
console.log(obj[item]) // 依次打印:小新、3、false
}
2
3
4
5
6
7
8
9
那么使用for...in
遍历对象的属性,属性有顺序吗?答案是得分情况
- 整数属性会进行排序
- 其他类型的属性按照创建的顺序依次显示
测试下属性都是整数属性的情况:
let obj = {
"4": '我是第四',
"1": '我是第一',
"3": '我是第三',
"2": '我是第二'
}
for (let item in obj) {
console.log(obj[item]) // 依次打印我是第一 我是第二 我是第三 我是第四
}
2
3
4
5
6
7
8
9
10
注意:所谓的整数属性是指不做任何修改的情况下与一个整数进行相互转换的字符串
比如+4
就不是整数属性,利用这个特点我们可以让属性名为整数的对象和属性值为非整数的对象在遍历上行为一致:
let obj = {
"+4": '我是第四',
"+1": '我是第一',
"+3": '我是第三',
"+2": '我是第二'
}
for (let item in obj) {
console.log(obj[item]) // 依次打印我是第四 我是第一 我是第三 我是第二
}
2
3
4
5
6
7
8
9
10
此时遍历即是按照属性创建的的顺序打印
# 对象的拷贝、克隆
原始类型:将值进行整体赋值/拷贝,赋值或者拷贝后的新值与旧值各行其事,互不影响
引用类型:只是将引用赋值/拷贝给新值,新值和旧值还是指向堆中同一个内存地址,任意其中一个值的修改,都影响另外一个值
# 1.对象是按引用赋值,基本类型是按值赋值
第一个例子:
let name = "小新"
let newName = name;
newName = "小新的女朋友"
console.log(name, newName) // 小新 小新的女朋友
2
3
4
当name
赋值给newName
,相当于复制了一份值给newName
,内存中会重新开辟一个空间存贮newName
,所以修改name
的值并不会影响anotherName
的值
第二个例子:
let obj = {
name: '小新'
}
let anotherObj = obj
anotherObj.name = "小新的女朋友"
console.log(obj.name, anotherObj.name) // 小新的女朋友 小新的女朋友
2
3
4
5
6
当obj
赋值给anotherObj
,其实是复制了一份引用给anotherObj
,由于是同一个引用,所以obj
和anotherObj
指向同一个地址,当修改anotherObj
中的name
属性,就会更新obj
和anotherObj
公用地址中的值,因此obj
的name
属性也改变了。
# 2.对象的比较方式
仅当两对象是同一对象时两者才相等,对于对象来说
==
和===
作用时一样的
let a = {};
let b = a; // 拷贝引用
console.log( a == b ); // true,都引用同一对象
console.log( a === b ); // true
2
3
4
5
但是两个独立的对象并不想等,即使拥有相同的属性
// 都是空对象
let a = {}
let b = {}
console.log(a === b) // false
// 或者拥有相同的属性
let a = {name: '小新'}
let b = {name: '小新'}
console.log(a === b) // false
2
3
4
5
6
7
8
9
# 构造器和操作符new
构造器是实现可重复代码的实现方式,所有的函数都能被构造。判断一个实例是否是通过new
关键字构造的,内置的方法有instanceof
和Object.getPrototypeOf
,其实new
自己也有一个属性可以判断,但是使用很少:
function isUseNew () {
console.log(new.target) // 返回new关键字构造的实例的原型
}
isUseNew() // undefiend
new isUseNew() // [Function: isUseNew]
2
3
4
5
6
所以其实这里我们可以使得普通函数也具有new关键字的行为,不过只是为了理解更加全面,绝不推荐这样使用
function isUseNew () {
if (!new.target) {
return new isUseNew()
}
}
console.log(new isUseNew() instanceof isUseNew); // true
2
3
4
5
6
7
# 可选链?.的用法
这是一种读取预存属性防止错误的预处理操作符
我们读取一个对象的未定义的属性,是开发中经常会出现的场景:
let obj = {}
console.log(obj.address.town); // Error
2
3
这样访问是会报错的,因为并没有在obj
中定义address
属性,所以读取town
属性就会报错
之前的处理办法基本是使用&&
解决这个问题:
console.log(obj && obj.address && obj.address.town)
使用?.
用法可以规避这个问题:
// 谷歌浏览器环境
console.log(obj?.address?.town) // 不会报错 而是undefiend
2
注意?.
刚被加入到标准中,在node环境中只支持node 14+的版本,所以目前不推荐使用,还是使用&&
# Symbol类型
在对象这里提到Symbol
类型是因为,Symbol
是用来定义唯一标识符的基本类型,而属性的唯一在对象的使用中显得尤为重要,所以在这一章我们聊一下Symbol
这种基本类型。
首先看看来自MDN上对Symbol
的定义:
Symbol 是一种基本数据类型 ,
Symbol()
函数会返回symbol类型的值,该类型具有静态属性和静态方法。它的静态属性会暴露几个内建的成员对象;它的静态方法会暴露全局的symbol注册,且类似于内建对象类,但作为构造函数来说它并不完整,因为它不支持语法:"new Symbol()
"。
基础的使用方法是:
// 只有一个参数,表示对定义的symbol标识符的描述
let mySymbol = Symbol("这是我自定义的Symbol");
console.log( typeof mySymbol); // symbol
2
3
# 1.Symbol是独立的,即使描述相同
let mySymbol1 = Symbol("这是我自定义的Symbol");
let mySymbol2 = Symbol("这是我自定义的Symbol");
console.log(mySymbol1 === mySymbol2) // false
2
3
4
# 2.注册全局的Symbol
// 从全局注册表中读取,没有就会注册一个全局symbol
let id = Symbol.for("我自定义的全局symbol");
// 再次读取,可能是在全局注册表中读取
let idAgain = Symbol.for("我自定义的全局symbol")
console.log(id === idAgain); // true
2
3
4
5
6
7
8
可以这样使用,可以在MDN (opens new window)上看到更多细节。
# 3.对象-原始值的转换
我们可能都这样使用对象进行加减法,如obj1+obj2
,或者obj1-obj2
,或者是打印某个对象,console.log(obj)
,这种情况下,对象自动被转换成了原始值:
- to-Boolean: 所有对象都会被转换成true,所以对于对象来说,不存在转换成布尔类型。
- to-Number: 转换成数值的情况,例如在日期对象中,日期可以相加减
- to-String: 比如打印某个对象时默认就会转换成基本数值类型
基本只会存在两种转换成to-Number
和to-String
,我们来讨论下:
语言内部在将对象转换成基本类型的时候,将可能会遇到的情况的处理方式称为"hint":
number和string两种hint自不用说,但是还有第三种hint就是default,主要是存在将对象和其他类型进行比较是无法判断是进行哪种hint,其次,+运算符除了运算功能外,还有连接字符串的作用,所以这也是不确定,default就是存在来处理这样的情况。
语言内部执行的机制是这样的,我们直接参考现代javaScript教程-将对象转换成基本类型 (opens new window)的说法:
转换时,解析器会尝试查找并调用三个对象方法:
- 首先调用obj[Symbol.toPrimitive](hint)假如这个方法存在的话
- 否则,如果hint是"string",尝试调用obj.toString或者obj.valueOf,谁存在调谁,两者都存在,先调用obj.toString,再调用obj.valueOf
- 否则,如果hint是"number"或者"default",尝试调用obj.valueOf和obj.tostring,谁存在调谁。两者都存在,先调用obj.valueOf,再调用obj.toString
内部的算法基本是按照这样的三步走战略。
第一步,寻找obj[Symbol.toPrimitive](hint)
const obj = {}
obj[Symbol.toPrimitive] = function (hint) {
// 这里的hint可能是string、number、default
}
2
3
4
比如我们在对象中定义这个方法:
const obj = {
name: '小新',
num: 520,
[Symbol.toPrimitive]: function (hint) {
return hint == 'string' ? this.name : hint == 'number' || hint == 'default' ? this.num : ''
}
}
// 触发我们自己定义的Symbol.toPrimitive方法
console.log(obj); // obj这个对象,alert方法是返回{name: '小新'} hint是string
console.log(+obj); // 520 hint是number
console.log(obj + 520); // 1024 hint是default
2
3
4
5
6
7
8
9
10
11
12
可以看到,当我们尝试做转换的时候就会优先调用我们自定义的Symbol.toPrimitive
的方法
再来定义toString
和valueOf
方法:
let obj = {
name: "小新",
num: 520,
// [Symbol.toPrimitive](hint) {
// console.log(`hint: ${hint}`);
// return hint == "string" ? `{name: "${this.name}"}` : this.money;
// }
toString () {
return this.name;
},
valueOf () {
return this.num;
}
};
// 转换演示:
console.log(obj); // obj这个对象
console.log(+obj); // 520
console.log(obj + 500); // 1024
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
这里得出了之前相同的结果,可以发现,内部机制缺失如我们描述的这样运行的。
# 可迭代对象(Iterable object)
可迭代是一种数组的泛化概念,可以根据这个概念将所有的对象定制成为
for...of
中可以迭代的对象。
我们可以自己实现一个可迭代的对象,比如一个range
对象:
let range = {
from: 1,
to: 5
}
// 我们希望 for..of 这样运行:
// for(let num of range) ... num=1,2,3,4,5
2
3
4
5
6
可迭代对象内部的运行机制是如何运作的呢?
- 为了使range对象可迭代,我们需要给range对象添加一个Symbol.iterator的方法,这是语言内部定义的一个方法。Symbol.iterator方法的特点是:返回一个具有next方法的对象,for...of遍历的时候迭代的就是调用这个next方法
- next方法返回也有讲究:必须返回的是{done: Boolean, value: any}这样的形式,当done为true的时候表示迭代结束。
所以想要使得range
对象可以被迭代,我们就必须满足上面的两个要求,我们来实现一下:
let range = {
from: 1,
to: 5
}
range[Symbol.iterator] = function () {
return {
current: this.from,
to: this.to,
next () {
if (this.current <= this.to) {
return {done: false, value: this.current++}
} else {
return {done: true}
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
我们来测试下是否能被for...of
实现:
for (let key of range) {
console.log(key); // 1 2 3 4 5
}
2
3