JavaScript系列 – 对象的基础知识

本文会介绍 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  // falselet 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:自动装箱、自动拆箱

基本数据类型 StringNumberBigIntSymbolBoolean必要 时,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 获取:所有可枚举属性

  1. 获取 所有可枚举 属性:for...in 遍历

for...in 经常被用来遍历一个 object,它可以遍历一个对象的所有 非Symbol可枚举 的属性(包括原型链上的属性)。

  • 另:for...of 遍历

es6 引入 Iterator 遍历器,来赋予多种数据结构统一的遍历接口。只要一个目标具有 Symbol.iterator 属性,那么就认为它具备这样的一个遍历器,也就可以被 for...of 遍历。

  1. 获取对象 自身的所有可枚举 属性名:Object.keys(Person)

  2. 获取对象 自身的所有可枚举 属性值:Object.values(Person)

  3. 获取对象 自身的所有可枚举 属性 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]] 操作的流程是:

  1. 在对象中查找是否有名称相同的属性,如果找到返回这个属性值;
  2. 没有找到,则按照原型链 [[Prototype]] 向上寻找,如果找到返回这个属性值;
  3. 没有找到,则返回 undefined

4.2 写入属性

该小节与原型链章节中,“属性的设置和屏蔽”内容相同。

person.name = "Moxy" 会触发 [[Put]] ,操作的完整过程是:

首先会判断对象中是否已存在该属性值,如果存在,则会执行 1;不存在,则会执行 2。

  1. 判断自有属性。如果 person 对象中存在一个同名的 数据属性,则该语句发生赋值行为,修改已有的这个属性。
  2. 判断继承属性。遍历 person 对象的原型链。
    1. 找不到。如果在原型链上找不到同名的 name 属性,则该语句发生创建行为,在 person 上创建新属性 name。否则执行 2.2;
    2. 找得到。如果在原型链上找得到同名的 name 属性,则又分为 3 种情况:
      1. 有setter。如果找到的同名属性是一个有 setter 函数 访问器属性。则该语句会发生 setter 函数的调用。
      2. 可写。如果找到的同名属性是 数据属性,且可写 writable:false 。则发生创建行为,在 person 上 创建新属性 name;
      3. 不可写。如果找到的同名属性是 数据属性,但不可写 writable:true。则该语句会被 静默忽略,在严格模式下报错:TypeError

总结:

  1. 所谓属性的设置,就是修改了一个已存在的属性值; 所谓属性的屏蔽,就是已知目标对象的原型链上存在一个同名属性,依然在目标对象上新建一个同名属性。按照访问顺序,会访问目标对象上的属性,则原型链上的同名属性被屏蔽。

  2. 所有 “数据属性” 都适用与该规则中,包括 基本数据类型引用属性类型(方法、数组、等等各种对象)。如果父类存在一个同名的 name 数组,执行 person.name = "Moxy" ,在 person 对象中创建的同名属性 name ,此时不再是一个引用属性类型数组,而是一个基本数据类型字符串。

  3. 规则 2.2.1 “有setter” 的情况可以考虑为:如果继承属性在父类是有 setter,则子类的赋值也要调用这个 setter。

  4. 规则 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) ,这相当于:

  1. 希望禁止该对象添加新属性:调用 Object.preventExtensions(obj)
  2. 不能重新配置 / 删除 任何现有的属性:所有现有属性设置特性 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 类型的属性都会被拷贝。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享