跳到主要内容

7. 对象

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:自动装箱、自动拆箱

基本数据类型 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 开始新增了属性描述符,也就是属性的特性。对象属性分为 数据属性访问器属性,其 [[内部特性]] (或者说属性描述符)分别有(2):

数据属性描述符 Data Properties Descriptor:

  • [[Configurable]]:属性是否可以:1. 通过 delete删除、或直接重定义;2. 修改它的 [[Configurable]];3. 修改为访问器属性。
  • [[Enumberable]]:属性是否可以通过 for-in 遍历。console.log()
  • [[Writable]]:属性的值是否可修改。
  • [[Value]]:属性的值。

访问器属性 Accessor Properties Descriptor:

  • [[Configurable]]:属性是否可以:1. 通过 delete删除、或直接重定义;2. 修改它的 [[Configurable]];3. 修改为数据属性。
  • [[Enumberable]]:属性是否可以通过 for-in 遍历。
  • [[Get]]:getter 函数,读取该属性时调用。默认值为 undefined。
  • [[Set]]:setter 函数,写入该属性时调用。默认值为 undefined。

❗️:vue2 中响应式的实现就是利用了 getter 和 setter。在组件 return 后,vue 会把 return 的对象全部遍历一边。对属性设置额外的 getter 和 setter,实现当某个属性值产生变化时,通过 setter 就会让其他有依赖的属性值一起发生改变,从而实现响应式。

❗️:console.log() 无法打印访问器属性。

❗️:当 enumberable 属性设置为 false 时, chrome 在console.log 时会把该属性的颜色变淡,以提示该属性不可遍历:

var obj = {
name: "ninjee",
age: 18
}

Object.defineProperty(obj, "address", {
value: "北京市"
})

console.log(obj); // 下图可以看到,address 颜色变淡了

截屏2022-07-25 21.42.13

2.3.1 定义属性的特性

当我们只在对象上定义某个属性时,

  • [[Configurable]][[Enumberable]][[Writable]] 会默认为 true;
  • [[value]][[get]][[set]]:默认为 undefined;
  • 可删改 Configurable、可遍历、可改值;

当我们通过属性描述符(下面的方法)定义属性时,

  • [[Configurable]][[Enumberable]][[Writable]] 会默认为 false,需要手动修改。
// 定义单个属性的特性
Object.defineProperty(person, "name", {
writable: false, // 不必写,默认就是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)。
    • 修改任何特性都会 直接报错[[Configurable]][[Enumberable]][[Writable]]
    • delete 删除属性会失败,返回 false 提示删除失败,不报错。
  • [[Writable]]:设置为 false 后,修改属性值,会 静默失败。严格模式下会 报错
// configurable = false, 不可以重新修改 Configurable 特性:
Object.defineProperty(person, "name", {
configurable: true
});
// 报错 Uncaught TypeError: Cannot redefine property: name


// Configurable = false, 不可以 delete 删除属性
delete person.name; // false
// 返回 false 提示删除失败,但不报错。


// writable = false, 不可以重新赋值
person.name = "ninjee" // 静默失败,严格模式下报错


// 不可以转化为访问器属性
// 可以修改 enumberable、writable、value 等特性

2.3.2 读取属性的特性

getOwnPropertyDescriptors 可以返回对象中全部自有属性,不论是否可枚举,同时 symbol 名称的属性也能找到。

// 读取单个属性的特性
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 可遍历到。

不可枚举属性:不可以通过 for...in 遍历的属性。

3.1 自有属性 / 继承属性

自由属性:定义在对象本身的属性;

继承属性:对象通过查找自己的原型链可访问到的属性。

3.1.1 判断:自有属性 / 继承属性

  1. in 操作符 / for in 遍历

in 操作符会判断属性是否在对象(自有属性)及其 [[prototype]] 原型链中(继承属性),返回布尔值。

  • 该属性只判断 可枚举属性
  • 这相当于 for ... in 遍历出的内容。
  1. hasOwnProperty("name")

Person.hasOwnProperty("name") 会判断属性是否在对象内部(自有属性)。返回布尔值。 该方法不会检查原型链上的属性,只会在对象自身去寻找是否有参数名相同的属性。

  • 不论是否可枚举,都可以判断。

所有普通对象的原型链最终都会指向 Object.prototype,这也就继承了 hasOwnProperty() 方法。但通过 Object.creat(null) 创建的、或者手动重定义原型链指向,则无法使用 Object 原型的方法,可以通过 this 的显式绑定: myObj.prototype.hasOwnProperty.call(myObj, "name")

  1. getOwnPropertyNames(person)

Object.getOwnPropertyNames(person) 可以获取全部自有属性。该方法会返回一个数组,成员是所有属性名。

  • 不论是否可枚举,都可以判断。

3.1.2 复习:类型判断

类型判断的方法,更多内容见:类型篇章的 “类型判断”:

  • typeof 操作符。可以判断基本数据类型值。

  • instance1 instenceof Father 操作符。判断 instance1 的原型链上是否有 Father.prototype 原型对象。

    • 不好用
  • Father.protoype.isProtoypeOf(instance1)。是 instanceof 的替代品,表意更明确

  • Object.getPrototypeOf(instance1)。获取 Instance1 的构造函数的原型对象

    • 相当于:instance1.__proto__
  • Object.prototype.toString()函数。获取 instance1 的引用类型名称,替代 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可枚举的属性(包括原型链上的属性)。

3.2.3 另: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]] 操作的流程是:

  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. 可写。如果找到的同名属性是 数据属性,且可写 writable:false 。则发生创建行为,在 person 上 创建新属性 name;
      2. 不可写。如果找到的同名属性是 数据属性,但不可写 writable:true。则该语句会被 静默忽略,在严格模式下报错:TypeError
      3. setter。如果找到的同名属性是一个有 setter 函数 访问器属性。则该语句会发生 setter 函数的调用。

总结:

  1. 所谓属性的设置,就是修改了一个已存在的属性值; 所谓属性的屏蔽,就是已知目标对象的原型链上存在一个同名属性,依然在目标对象上新建一个同名属性,则原型链上的同名属性被屏蔽。
  2. 所有 “数据属性” 都适用与该规则中,包括 基本数据类型引用属性类型(方法、数组、等等各种对象)。如果父类存在一个同名的 name 数组,执行 person.name = "Moxy" ,在 person 对象中创建的同名属性name ,此时不再是一个引用属性类型数组,而是一个基本数据类型字符串。
  3. 规则 2.2.2 “不可写” 的情况可以考虑为:如果继承属性在父类不允许写,则其继承的子类也不允许写。
  4. 规则 2.2.3 “setter” 的情况可以考虑为:如果继承属性在父类是有setter,则子类的赋值也要调用这个 setter。

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对象中。也就是说,目标对象通过混入源对象的属性得到增强。

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 类型的属性都会被拷贝。

7 对象的复制

放在一个新文章中阐述:浅拷贝、深拷贝。

  1. 深拷贝注意的点是什么,循环引用。

  2. 什么是 JSON安全?

  3. 方法

    • 深拷贝方法:let newObj = JSON.parse(JSON.stringify(newObj));

    • 浅拷贝方法:let newObj = Object.assign({}, myObject}

  4. 疑问:let newObj = Object.create({myObject}) 是一个浅拷贝方法吗?

8 Object API

同一常用的 Object API 共有19个

Object.is()

// 1 对象的定义与继承
Object.assign()
Object.create()
Object.defineProperty()
Object.defineProperties()
Object.getOwnPropertyDescriptor()
Object.getPrototypeOf()
Object.setPrototypeOf()

// 2 对象属性的遍历
Object.keys()
Object.values()
Object.entries()
Object.getOwnPropertyNames()
Object.getOwnPropertySymbols()

// 3 对象的不变性
Object.preventExtensions()
Object.seal()
Object.freeze()
Object.isExtensible()
Object.isSealed()
Object.isFrozen()

Object.is()

Object.is(valueA, valueB) 是基于严格相等符号 === 的改进,主要改进了 === 的几个历史问题:

// === 的几个历史问题:
NaN === NaN // false
0 === -0 // true
-0 === +0 // true
0 === +0 // true

// Object.is() 的改进;
Object.is(NaN, NaN) // true
Object.is(0, -0) // false
Object.is(-0, +0) // false
Object.is(0, +0) // true 特别注意:这里是true!

1 对象的定义与继承

Object.assign()

对象的合并:用于将所有可枚举属性的值从一个或多个源对象分配到目标对象。它将返回目标对象。

Object.create()

对象的创建:创建一个新对象,使用现有的对象来提供新创建的对象的__proto__

  • Object.create(null),会创建一个没有原型链的对象。
  • Object.create(null, {....}),可以传入其他对象,来增强新创建的对象。

Object.defineProperty()

定义、修改对象的一个属性。

Object.defineProperty(obj, prop, descriptor) // 对象、属性名、属性描述符

// example
Object.defineProperty(obj, "key", {
enumerable: false,
configurable: false,
writable: false,
value: "static"
});

Object.defineProperties()

定义、修改对象的多个属性

Object.defineProperties(obj, props) 

// example
let obj = {};
Object.defineProperties(obj, {
'property1': {
value: true,
writable: true
},
'property2': {
value: 'Hello',
writable: false
}
});

Object.getOwnPropertyDescriptor()

获得目标对象上一个自有属性对应的属性描述符。

let person = {
name: "Moxy"
}
let descriptor = Object.getOwnPropertyDescriptor(person, "name")
descriptor
// {value: 'Moxy', writable: true, enumerable: true, configurable: true}

Object.getPrototypeOf()

获得指定对象的原型(内部[[Prototype]]属性的值)。

  • Object.getPrototypeOf(obj) 就相当于 obj,__proto__ 的标准版
// example 1
function Father() {}
function Son() {}
Son.prototype = Object.create(Father.prototype)

Object.getPrototypeOf(Son.prototype) === Father.prototype // true
Son.prototype.__proto__ === Father.prototype // true

// example 2
let father = {}
let son = Object.create(father)

Object.getPrototypeOf(son) === father // true
son.__proto__ === father // true

Object.setPrototypeOf()

把目标对象的原型(即, 内部[[Prototype]]属性)链接到另一个对象上。

  • 不推荐使用此方法。而更推荐使用 Object.create(),性能更好。
function Father() {}

function Son() {}
Object.setPrototypeOf(Son.prototype, Father.prototype)
// 效果相当于:
// Son.prototype = Object.create(Father.prototype)

Object.getPrototypeOf(Son.prototype) === Father.prototype // true
Son.prototype.__proto__ === Father.prototype // true

2 对象属性的遍历

Object.keys()

Object.values()

keys 返回一个数组,其成员是目标对象 自有的可枚举的 属性名。

values 返回一个数组,其成员是目标对象 自有的可枚举的 属性值。

  • 数组中排列顺序和正常循环遍历时的顺序一致。
let obj = {
green: "Happy",
blue: "Smile",
red: "hot"
}

console.log(Object.keys(obj)) // (3) ['green', 'blue', 'red']
console.log(Object.values(obj)) // (3) ['Happy', 'Smile', 'hot']

Object.entries()

返回一个数组,其元素一组组的键值对,是在 object 上自有的可枚举的属性和属性值。

  • 返回了一个可迭代的对象。
let obj = {
green: "Happy",
blue: "Smile",
red: "hot"
}
for (let [key, value] of Object.entries(obj)) {
console.log(`${key}: ${value}`);
}
// green: Happy
// blue: Smile
// red: hot

Object.getOwnPropertyNames()

获得目标对象的所有 自有的 属性名。

  • 包括不可枚举属性
  • 不包括 Symbol 值作为名称的属性
  • 返回一个数组。
let person = {
name: "Moxy",
age: "18",
class: "3"
}

let res = Object.getOwnPropertyNames(person)
console.log(res) // (3) ['name', 'age', 'class']

Object.getOwnPropertySymbols()

获得目标 自有的 所有 Symbol 属性的数组。

let uid = Symbol('uid');
let x = Symbol('x');
let person = {
name: "Moxy",
age: "18",
class: "3",
[uid]: 001,
[x]: "x"
}

let res = Object.getOwnPropertySymbols(person)
console.log(res) // (2) [Symbol(uid), Symbol(x)]

3 对象的不变性

Object.preventExtensions()

  • 令对象禁止扩展

Object.seal()

  • 密封一个对象

Object.freeze()

  • 冻结一个对象

Object.isExtensible()

  • 判断一个对象是否是可扩展,如果不可扩展,则返回 false

Object.isSealed()

  • 判断一个对象是否已密封,如果已密封,则返回 true

Object.isFrozen()

  • 判断一个对象是否已冻结,如果已冻结,则返回 true

4. Js 的面向对象

JavaScript 中的对象被设计成一组 key 和 value组成的 属性无序集合,像是一个 哈希表

  • key 是一个标识符名称,value 可以是任意类型(变量、其他对象或者函数类型);
  • 如果 value 是一个函数,这个函数通常称之为对象的方法。

对象的创建

// new 创建
const obj1 = new Object();
obj1.name = "moxy";
obj1.say = function() {
console.log('hello world');
}

// 字面量创建
const obj2 = {
name: "moxy";
say: function() {
console.log('hello world');
}
}

set 和 get 的定义:

有两种方式:

  1. defineProperties API,该方法定义的访问器属性特性 configurable、enumberable默认为true。
  2. get 和 set 关键字创建,访问器属性特性 configurable、enumberable默认为true。
// 方法一:defineProperties
var obj1 = {};

Object.defineProperties(obj1, {
_age: {
configurable: false, // 默认为false,可不写
enumerable: false, // 默认为false,可不写
writable: true,
value: "ninjee"
},
age: {
configurable: true,
enumberable: true,
get: function() { return this._age; },
set: function(value) { this._age = value; }
}
});


// 方法二:字面量创建
var obj1 = {
get age() { return this._age },
set age(value) { this._age = value }
}

Object.defineProperties(obj1, {
_age: {
writable: true,
value: "ninjee"
}
});

// 测试
console.log(obj1); // {_age: 'ninjee'} 无法打印访问器属性 age
obj1.age; // ninjee
obj1.age = 18;
obj1.age; // 18
obj1._age // 18

// 使用API可以打印出所有描述符
// 批量打印
Object.getOwnPropertyDescriptors(obj1);
// age: {enumerable: true, configurable: true, get: ƒ, set: ƒ}
// _age: {value: 18, writable: true, enumerable: false, configurable: false}

// 打印一个
Object.getOwnPropertyDescriptor(obj1, "age");
// {enumerable: true, configurable: true, get: ƒ, set: ƒ}
Object.getOwnPropertyDescriptor(obj1, "_age");
// {value: 18, writable: true, enumerable: false, configurable: false}

截屏2022-07-25 17.59.57

4.1 设计模式(创建对象)

4.1.1 new 和字面量

const obj = new Object();
// or
const obj2 = {}

new 和字面量如果创建多个相似属性的对象, 会产生大量重复的代码

4.1.2 工厂模式

🏭 工厂模式的样式:

function createPerson(param..) {
// 通过入参创建一个对象并返回
}

const p1 = createPerson(param1...);
const p2 = createPerson(param2...);
const p3 = createPerson(param3...);

具体实现:

function createPerson(name, age, height, address) {
const p = new Object();
p.name = name;
p.age = age;
p.height = height;
p.address = height;
p.eating = function() {
console.log(this.name + "在吃东西");
}
return p
}

const p1 = createPerson("张三", 18, 1.88, "北京市");
const p2 = createPerson("李四", 22, 1.68, "重庆市");
const p3 = createPerson("王五", 30, 1.80, "上海市");

优点:可以批量创建属性值相同、方法相同的对象。

缺点:

  1. 获取不到对象最真实的类型,所有对象都是 Object 对象。看起来都是一个字面量类型,不是面向对象。
  2. 将方法绑定到对象自身的属性上,没有实现方法的公有(绑定到原型链上),没有实现方法服用。这导致内存空间被浪费,每个对象上都创建了一个完全相同的对象。

4.1.3 构造函数方式

构造函数又称 构造器(constructor),在创建对象时会调用的函数。在其他面向的编程语言中,构造函数是存在于类中的一个方法,称之为 构造方法; 在 JavaScript 中的构造函数不同:

  • 构造函数也是普通函数,和其他函数没有任何区别。
  • 当一个普通函数,通过 new 关键字调用时,这个函数就是一个构造函数。

new 一个新对象的过程,发生了什么?

let person1 = new Person("Moxy", 15);

要创建 Person 的新实例,必须使用 new 操作符。以这种方式调用构造函数实际上会经历以下 5 个步骤:

  1. 创建对象。创建一个 新对象 {}
  2. 绑定原型链。为新对象绑定 原型链{}.__proto__ = Person.prototype
  3. 绑定 this。将构造函数的作用域 this 赋给新对象 {}
  4. 执行函数。执行构造函数中的代码(为 {} 添加属性:Person.call(this))。
  5. 返回对象。
    1. 如果构造函数最终会返回一个对象,就返回 构造函数中的对象
    2. 如果构造函数没有返回其他对象,就会返回 新对象

最终,代码中左侧的 person1 变量接收到了新创建的那个对象。

具体实现:

function Person(name, age, height, address) {
this.name = name;
this.age = age;
this.height = height;
this.address = height;
this.eating = function() {
console.log(this.name + "在吃东西");
}
}

const p1 = new Person("张三", 18, 1.88, "北京市");
const p2 = new Person("李四", 22, 1.68, "重庆市");
const p3 = new Person("王五", 30, 1.80, "上海市");

p1.__proto__.constructor; // 构造函数的代码:function Person(...){...}
p1.__proto__.constructor.name; // 构造函数的函数名称:Person
p1.__proto__; // 指向创建p1的构造函数:Person
p1.__proto__ === Person.prototype; // true

优点:

  1. 工厂模式的优点,可以批量创建类似结构的对象(例子中都是 Person 类);
  2. 解决了工厂模式中的缺点1,无法确定返回的对象是什么类型的问题(工厂模式是通过 Object 创建的);

缺点:

  1. 将方法绑定到对象自身的属性上,没有实现方法的公有(绑定到原型链上),没有实现方法服用。这导致内存空间被浪费,每个对象上都创建了一个完全相同的对象。

4.1.4 原型链方式

为了解决对象方法不能公有的问题,可将公共方法放在实例对象的原型对象上:

function Person(name, age, height, address) {
this.name = name;
this.age = age;
this.height = height;
this.address = height;
}

// 通过原型链绑定公有方法
Person.prototype.eating = function() {
console.log(this.name + "在吃东西");
}

const p1 = new Person("张三", 18, 1.88, "北京市");
const p2 = new Person("李四", 22, 1.68, "重庆市");
const p3 = new Person("王五", 30, 1.80, "上海市");

p1.eating === p2.eating // true 方法变成公有

优点:

  1. 工厂模式优点:可以方便创造结构相似的实例对象;
  2. 构造函数优点:可以让实例对象有可识别的类型;
  3. 新增优点:让方法变成公有,所有实例对象公用一个对象,而不是对每个实例都创建一个完全相同的备份。