本文会介绍 JavaScript 中,对象的基础知识。
1 对象的定义
对象有两种定义方式:声明式和构造式:
// 声明式
let myObj = {
name: "Moxy",
}
// 构造式
let myObj = new Object()
myObj.name = "Moxy"
复制代码
2 对象的类型
复习 1 :数据类型
JavaScript 一共有 8 个基本数据类型:
- 原始类型:Null,Undefined,Boolean,Number,BigInt,String,Symbol;
- 对象类型:Object
复习 2 :引用类型
引用类型都继承自对象类型 Object,换句话说,引用类型的原型对象,其原型链最终都指向 Object.prototype
以下是引用类型:
- 基本包装类型:Boolean,Number,BigInt,String,Symbol;
- Array
- Date
- RegExp
- Function
- Error
- Map
- …
这些引用类型也可以称为 内置对象,其是由 JavaScript 引擎在运行开头,就通过 object
创建好的一系列类型。所以这些类型的原型链都指向 object.prototype
。
复习 3 :自动装箱与类型判断
// 控制台如下输出:
let str1 = "Moxy"
str1.__proto__ // String {"", constructor: ƒ, anchor: ƒ, big: ƒ, blink: ƒ, …}
str1 instanceof String // false
let str2 = new String("Moxy")
str2.__proto__ // String {"", constructor: ƒ, anchor: ƒ, big: ƒ, blink: ƒ, …}
str2 instanceof String // true
复制代码
要点 1:定义的两种方式
str1
是一个字面量,通过字面量的形式直接定义的基本数据类型 string。str2
是一个对象,通过 new 运算符创建的一个 String 类型的对象。
所以,instanceof
运算符在判断 str1
是否属于 String
的时候,返回了 false
。因为它本就不是通过 包装类型 String 创建的。
要点 2:自动装箱、自动拆箱
基本数据类型 String
,Number
,BigInt
,Symbol
,Boolean
在 必要 时,JavaScript 会自动把字面量转换成一个对应的包装类型对象。在完成对应操作后,就自动还原成原本的字面量形式。这就是自动装箱 / 拆箱。
- ”必要时“ 这个时机的触发,通常是调用相关方法时触发,比如对字符串的基本操作:
toString()
、length()
、charAt()
,等等都会触发。
所以,文中对 str1.__proto__
触发了自动装箱,先把 str1 装箱为 String 类型的实例化对象,然后通过原型链查找 __proto__
自然可以找到 String.prototype
,即 String 的原型对象。
2 对象的属性
2.1 对象是属性的集合
对象的内容时由一些存储在特定命名位置的、任意类型的值组成的,我们称之为 属性。
JavaScript 中对象独有的特色是:
对象具有高度的动态性,这是因为 JavaScript 赋予了使用者在运行时为对象添改状态和行为的能力。
实际上 JavaScript 对象的运行时是一个 “属性的集合” 。
属性以字符串或者 Symbol 为 key,以数据属性特征值或者访问器属性特征值为 value。
JavaScript 中,其实没有 属性 和 方法 的区分。JavaScript 内部只有一个东西,就是属性 —— 一个 key/value。
-
如果 value 保存了一个字面量,直接保存在栈内存中。那么保存了一个基本数据类型值(boolean、number、string 等)。
-
如果 value 保存了一个地址值,指向了堆内存中的某个地方。
- 如果指向了一个 function,那么这个属性也是一个 方法;
- 如果指向了一个 array,那么这个属性也是一个 数组;
- ….
这就是为什么在 对象的继承 中,我们分别分析 基本类型属性、引用类型属性 和 方法,继承后是否可以复用了。从根本上,是在分析是直接字面量还是地址值。
2.2 属性的访问
属性访问方式有两种:
- 属性访问:
instance1.name
- 键访问:
instance1["name"]
两者的区别:
- 属性访问:代码写起来更便捷,但是
.
操作符要求属性名满足标识符的命名规范; - 键访问:支持更强大。
["..."]
可以接受任意 UTF-8/Unicode 字符串作为属性名,比如带有叹号的变量名,只能通过键访问instance["Super-fun!"]
;- 支持 表达式 访问名,可以把变量名放进去。比如定义
var a = "name"
,那么p1[a]
在运行时转化为p1.name
。- 也可以是:可计算属性名,比如
instance1[ 1 + 2 ]
。
- 也可以是:可计算属性名,比如
在对象中,属性名永远都是字符串,即使通过变量传递过来 number 等其他值,也会被自动的转换为字符串。
2.3 属性的特性
对象的属性,在 ES5 开始新增了属性描述符,也就是属性的特性。对象属性分为 数据属性 和 访问器属性,其 [[内部特性]] (或者说属性描述符)分别有:
数据属性:
[[Configurable]]
:属性是否可以:1. 通过delete
删除、或直接重定义;2. 修改它的特性;3. 修改为访问器属性。[[Enumberable]]
:属性是否可以通过 for-in 遍历。[[Writable]]
:属性的值是否可修改。[[Value]]
:属性的值。
访问器属性:
[[Configurable]]
:属性是否可以:1. 通过delete
删除、或直接重定义;2. 修改它的特性;3. 修改为数据属性。[[Enumberable]]
:属性是否可以通过 for-in 遍历。[[Get]]
:getter 函数,读取该属性时调用。默认值为undefined
。[[Set]]
:setter 函数,写入该属性时调用。默认值为undefined
。
2.3.1 定义属性的特性
// 定义单个属性的特性
Object.defineProperty(person, "name", {
writable: false,
value: "Moxy"
});
// 定义多个属性的特性
Object.defineProperties(person, {
age: {
get() {
return this.age_
}
set(newValue) {
if (newValue > 0) this.age_ = newValue
}
},
age_: {
value: 12,
}
})
复制代码
数据属性:
[[Configurable]]
:设置为false
后,修改任何特性,都会报错。是单向操作,无法撤销(无法再重新设置为 true)。[[Enumberable]]
:属性是否可以通过 for-in 遍历。[[Writable]]
:如果设置为false
后,修改属性值,会静默失败。严格模式下会报错。[[Value]]
:属性的值。
2.3.2 读取属性的特性
// 读取单个属性的特性
let descriptor = Object.getOwnPropertyDescriptor(person, "name");
console.log(descriptor.configurable) // true
console.log(descriptor.enumberable) // true
console.log(descriptor.writable) // true
console.log(descriptor.value) // "Moxy"
// 读取多个属性的特性(ES2017)
let descriptors = Object.getOwnPropertyDescriptors(person);
console.log(descriptors)
// 会输入如下内容:
{
name: {
configurable: true,
enumerable: true,
value: "Moxy",
writable: false
},
age: {
configurable: false,
enumerable: false,
get: f(),
set: f(newValue)
},
age_: {
configurable: false,
enumerable: false,
value: 12,
writable: false
}
}
复制代码
3 判断对象的属性
关于对象的属性,通常有以下 4 个名词,注意区分其含义:自有属性、继承属性、枚举属性、不可枚举属性。
自有属性:定义在对象内部的属性;
继承属性:通过对象原型链继承而来的属性;
可枚举属性:可枚举属性分为下面两种。通常来讲,指的是属性描述符 writable: true
的属性。
- 对象自有的可枚举属性:仅包括对象自有的可枚举属性
- 对象所有的可枚举属性:包括对象自有的、继承的可枚举属性,用
for...in
可遍历到。
不可枚举属性:属性描述符 writable: false
的属性,不可以通过 for...in
遍历的属性。
3.1 自有属性 / 继承属性
自有属性:定义在对象本身的属性;
继承属性:对象通过查找自己的原型链可访问到的属性。
3.1.1 判断:自有属性 / 继承属性
1. in
操作符
in
操作符会判断属性是否在对象(自有属性)及其 [[prototype]]
原型链中(继承属性),返回布尔值。
- 该属性不论是否可枚举,都会被判断。
2. hasOwnProperty("name")
Person.hasOwnProperty("name")
会判断属性是否在对象内部(自有属性)。返回布尔值。该方法不会检查原型链上的属性,只会在对象自身去寻找是否有参数名相同的属性。
所有普通对象的原型链最终都会指向 Object.prototype
,这也就继承了 hasOwnProperty()
方法。但通过 Object.creat(null)
创建的,或者手动重定义原型链指向则无法使用 Object
原型的方法,可以通过 this 的显式绑定:Object.prototype.hasOwnProperty.call(myObj, "name")
。
3. getOwnPropertyNames(person)
Object.getOwnPropertyNames(person)
可以获取全部自有属性。该方法会返回一个数组,成员是所有属性名。
3.1.2 复习:类型判断
类型判断的方法,更多内容见:类型篇章的 “类型判断”:
typeof
操作符。可以判断基本数据类型值。instenceof
操作符。可以判断引用类型值,但不好用。Object.prototype.toString()
函数。可以判断引用类型值,替代instanceof
操作符。
3.2 可枚举属性 / 不可枚举属性
可枚举属性:通常来讲,指的是属性描述符 writable: true
的属性;
不可枚举属性:指的是属性描述符 writable: false
的属性。
3.2.1 判断:对象自身可枚举属性
判断对象 自身的可枚举 属性:person.propertyIsEnumerable("name")
通过 person.propertyIsEnumerable("name")
返回的布尔值来判断。以下两种情况,都会返回 false
:
- 该属性是 不可枚举的;
- 该属性是原型链上的 继承属性 。
3.2.2 获取:所有可枚举属性
- 获取 所有可枚举 属性:
for...in
遍历
for...in
经常被用来遍历一个 object,它可以遍历一个对象的所有 非Symbol、可枚举 的属性(包括原型链上的属性)。
- 另:
for...of
遍历
es6 引入 Iterator 遍历器,来赋予多种数据结构统一的遍历接口。只要一个目标具有 Symbol.iterator
属性,那么就认为它具备这样的一个遍历器,也就可以被 for...of
遍历。
-
获取对象 自身的所有可枚举 属性名:
Object.keys(Person)
-
获取对象 自身的所有可枚举 属性值:
Object.values(Person)
-
获取对象 自身的所有可枚举 属性 K/V:
Object.entries(Person)
与in
操作符不同的是:
- 这三个方法 不检查原型链 上的那些属性;
- 这三个方法都 遍历 Symbol 类型。
三个方法的返回值均是一个数组。
4 [[Get]]
和 [[Put]]
注意:本小节中 [[Get]]
和 [[Put]]
并不是访问器描述符中的 getter 函数 和 setter 函数。而是对象内置的默认访问 / 读取函数的规则。注意区分。
读取属性和写入属性,通常和这两点息息相关:
- 属性值的类型,是基本数据类型还是引用数据类型;
- 对象的原型链,在原型链上的原型对象,是否存在同名的属性。
关于原型链的更多知识,参考原型链章节。
4.1 读取属性
let person = {name: "Moxy"}
person.name // "Moxy"
复制代码
在进行依次属性访问 peson.name
时,实际上是完成了一次 [[Get]]
操作,类似函数调用:[[Get]]()
。
一次 [[Get]]
操作的流程是:
- 在对象中查找是否有名称相同的属性,如果找到返回这个属性值;
- 没有找到,则按照原型链
[[Prototype]]
向上寻找,如果找到返回这个属性值; - 没有找到,则返回
undefined
。
4.2 写入属性
该小节与原型链章节中,“属性的设置和屏蔽”内容相同。
person.name = "Moxy"
会触发 [[Put]]
,操作的完整过程是:
首先会判断对象中是否已存在该属性值,如果存在,则会执行 1;不存在,则会执行 2。
- 判断自有属性。如果 person 对象中存在一个同名的 数据属性,则该语句发生赋值行为,修改已有的这个属性。
- 判断继承属性。遍历 person 对象的原型链。
- 找不到。如果在原型链上找不到同名的 name 属性,则该语句发生创建行为,在 person 上创建新属性 name。否则执行 2.2;
- 找得到。如果在原型链上找得到同名的 name 属性,则又分为 3 种情况:
- 有setter。如果找到的同名属性是一个有 setter 函数 访问器属性。则该语句会发生 setter 函数的调用。
- 可写。如果找到的同名属性是 数据属性,且可写
writable:false
。则发生创建行为,在 person 上 创建新属性 name; - 不可写。如果找到的同名属性是 数据属性,但不可写
writable:true
。则该语句会被 静默忽略,在严格模式下报错:TypeError
;
总结:
-
所谓属性的设置,就是修改了一个已存在的属性值; 所谓属性的屏蔽,就是已知目标对象的原型链上存在一个同名属性,依然在目标对象上新建一个同名属性。按照访问顺序,会访问目标对象上的属性,则原型链上的同名属性被屏蔽。
-
所有 “数据属性” 都适用与该规则中,包括 基本数据类型 和 引用属性类型(方法、数组、等等各种对象)。如果父类存在一个同名的
name
数组,执行person.name = "Moxy"
,在person
对象中创建的同名属性name
,此时不再是一个引用属性类型数组,而是一个基本数据类型字符串。 -
规则 2.2.1 “有setter” 的情况可以考虑为:如果继承属性在父类是有 setter,则子类的赋值也要调用这个 setter。
-
规则 2.2.3 “不可写” 的情况可以考虑为:如果继承属性在父类不允许写,则其继承的子类也不允许写。
5 对象的不变性
不变性:希望属性或对象的值不会发生改变。
浅不变性:属性或对象的值是不可改变的。但如果保存的值是一个地址值,指向了堆内存中的地址。那虽然 “地址值” 本身不会改变,指向的那个地址中的内容(数组、对象、函数等)却有可能发生变化。
下面介绍的 4 种方法,按照不变性级别逐次增高,共有 4 种不变性:
5.1 对象常量
定义一个常量属性:不可修改、重定义、删除:
- 把
[[Configurable]]
和[[Enumberable]]
都设置为false
即可。
let obj = {};
Object.defineProperty( obj, "NAME" {
value: "Moxy",
writable: false,
configurable: false
})
复制代码
5.2 禁止扩展
希望禁止一个对象添加新属性,并且保留自己的已有属性,使用 Object.preventExtensions(obj);
let obj = { name: "Moxy" };
Object.preventExtensions(obj);
obj.age = 18
obj.age // undefined
复制代码
非严格模式下,额外创建属性会静默失败;严格模式下,会报错 TypeError
。
5.3 密封
密封一个属性,调用 Object.seal(obj)
,这相当于:
- 希望禁止该对象添加新属性:调用
Object.preventExtensions(obj)
; - 不能重新配置 / 删除 任何现有的属性:所有现有属性设置特性
Configurable:false
。
let obj = { name: "Moxy" };
Object.seal(obj);
复制代码
5.4 冻结
冻结一个属性,调用 Object.freeze(obj)
,这相当于:
- 密封一个属性,调用
Object.seal(obj)
。禁止对象扩展新属性,禁止对象删改现有属性。 - 现在属性不可修改:所有现有属性设置特性
writable:false
。
6 对象的合并
合并(merge)两个对象,指的是把 A 对象和 B 对象合并为一个新的对象。
混入(mixin)某个对象,指的是把 B、C、D 对象的属性,混入 A 对象中。也就是说,目标对象(A)通过混入源对象(其他)的属性得到增强。
Object.assign()
:ES6 提供的混入对象的方法。接收一个目标对象,和多个源对象作为参数,然后将每个源对象中 可枚举的自有属性 都复制到目标对象中。
- 自有属性:
Object.hasOwnProperty()
返回true
; - 自有的可枚举的属性:
Object.propertyIsEnumberable()
返回true
;
// person, animal, building 都是对象.
Object.assign(person, {sex: "male"}, animal, building)
复制代码
注:
-
这是一个浅拷贝,
- 只混入自身的可枚举属性,不涉及继承而来的属性;
- 源值是一个对象的引用,它仅仅会复制其引用值,而不是复制整个对象。
-
如果目标对象和接受混入的源对象具有相同的属性名,则源对象属性名的具体值,会覆盖目标对象之前的值。
-
如果多个源对象都在相同的属性,则最终使用最后一个源对象的值。
-
该方法的获取值(从源对象中)和赋值(到目标对象中),会使用源对象的
[[Get]]
和目标对象的[[Set]]
,所以它会调用相关 getter 和 setter。也就是说,该方法不会复制源对象的 getter 和 setter ,而是把访问器属性获取的值,转化为一个普通的数据属性。 -
String 类型和 Symbol 类型的属性都会被拷贝。