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:自动装箱、自动拆箱
基本数据类型 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 开始新增了属性描述符,也就是属性的特性。对象属性分为 数据属性 和 访问器属性,其 [[内部特性]] (或者说属性描述符)分别有(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 颜色变淡了
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 判断:自有属性 / 继承属性
in
操作符 /for in
遍历
in
操作符会判断属性是否在对象(自有属性)及其 [[prototype]] 原型链中(继承属性),返回布尔值。
- 该属性只判断 可枚举属性;
- 这相当于
for ... in
遍历出的内容。
hasOwnProperty("name")
Person.hasOwnProperty("name")
会判断属性是否在对象内部(自有属性)。返回布尔值。
该方法不会检查原型链上的属性,只会在对象自身去寻找是否有参数名相同的属性。
- 不论是否可枚举,都可以判断。
所有普通对象的原型链最终都会指向 Object.prototype
,这也就继承了 hasOwnProperty()
方法。但通过 Object.creat(null)
创建的、或者手动重定义原型链指向,则无法使用 Object
原型的方法,可以通过 this 的显式绑定: myObj.prototype.hasOwnProperty.call(myObj, "name")
。
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]]
操作的流程是:
- 在对象中查找是否有名称相同的属性,如果找到返回这个属性值;
- 没有找到,则按照原型链
[[Prototype]]
向上寻找,如果找到返回这个属性值; - 没有找到,则返回
undefined
。
4.2 写入属性
该小节与原型链章节中,“属性的设置和屏蔽”内容相同。
person.name = "Moxy"
会触发 [[Put]]
,操作的完整过程是:
首先会判断对象中是否已存在该属性值,如果存在,则会执行 1;不存在,则会执行 2。
- 判断自有属性。如果 person 对象中存在一个同名的 数据属性,则该语句发生赋值行为,修改已有的这个属性。
- 判断继承属性。遍历 person 对象的原型链。
- 找不到。如果在原型链上找不到同名的 name 属性,则该语句发生创建行为,在 person 上创建新属性 name。否则执行 2.2;
- 找得到。如果在原型链上找得到同名的 name 属性,则又分为 3 种情况:
- 可写。如果找到的同名属性是 数据属性,且可写
writable:false
。则发生创建行为,在 person 上 创建新属性 name; - 不可写。如果找到的同名属性是 数据属性,但不可写
writable:true
。则该语句会被 静默忽略,在严格模式下报错:TypeError
; - setter。如果找到的同名属性是一个有 setter 函数 访问器属性。则该语句会发生 setter 函数的调用。
- 可写。如果找到的同名属性是 数据属性,且可写
总结:
- 所谓属性的设置,就是修改了一个已存在的属性值; 所谓属性的屏蔽,就是已知目标对象的原型链上存在一个同名属性,依然在目标对象上新建一个同名属性,则原型链上的同名属性被屏蔽。
- 所有 “数据属性” 都适用与该规则中,包括 基本数据类型 和 引用属性类型(方法、数组、等等各种对象)。如果父类存在一个同名的 name 数组,执行
person.name = "Moxy"
,在 person 对象中创建的同名属性name ,此时不再是一个引用属性类型数组,而是一个基本数据类型字符串。 - 规则 2.2.2 “不可写” 的情况可以考虑为:如果继承属性在父类不允许写,则其继承的子类也不允许写。
- 规则 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 )
,这相当于:
- 希望禁止该对象添加新属性:调用
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对象中。也就是说,目标对象通过混入源对象的属性得到增强。
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 对象的复制
放在一个新文章中阐述:浅拷贝、深拷贝。
深拷贝注意的点是什么,循环引用。
什么是 JSON安全?
方法
深拷贝方法:
let newObj = JSON.parse(JSON.stringify(newObj));
浅拷贝方法:
let newObj = Object.assign({}, myObject}
;
疑问:
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 的定义:
有两种方式:
defineProperties
API,该方法定义的访问器属性特性 configurable、enumberable默认为true。- 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}
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, "上海市");
优点:可以批量创建属性值相同、方法相同的对象。
缺点:
- 获取不到对象最真实的类型,所有对象都是 Object 对象。看起来都是一个字面量类型,不是面向对象。
- 将方法绑定到对象自身的属性上,没有实现方法的公有(绑定到原型链上),没有实现方法服用。这导致内存空间被浪费,每个对象上都创建了一个完全相同的对象。
4.1.3 构造函数方式
构造函数又称 构造器(constructor),在创建对象时会调用的函数。在其他面向的编程语言中,构造函数是存在于类中的一个方法,称之为 构造方法; 在 JavaScript 中的构造函数不同:
- 构造函数也是普通函数,和其他函数没有任何区别。
- 当一个普通函数,通过
new
关键字调用时,这个函数就是一个构造函数。
new 一个新对象的过程,发生了什么?
let person1 = new Person("Moxy", 15);
要创建 Person 的新实例,必须使用 new 操作符。以这种方式调用构造函数实际上会经历以下 5 个步骤:
- 创建对象。创建一个 新对象
{}
; - 绑定原型链。为新对象绑定 原型链:
{}.__proto__ = Person.prototype
; - 绑定
this
。将构造函数的作用域this
赋给新对象{}
; - 执行函数。执行构造函数中的代码(为
{}
添加属性:Person.call(this)
)。 - 返回对象。
- 如果构造函数最终会返回一个对象,就返回 构造函数中的对象。
- 如果构造函数没有返回其他对象,就会返回 新对象。
最终,代码中左侧的 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
优点:
- 工厂模式的优点,可以批量创建类似结构的对象(例子中都是 Person 类);
- 解决了工厂模式中的缺点1,无法确定返回的对象是什么类型的问题(工厂模式是通过 Object 创建的);
缺点:
- 将方法绑定到对象自身的属性上,没有实现方法的公有(绑定到原型链上),没有实现方法服用。这导致内存空间被浪费,每个对象上都创建了一个完全相同的对象。
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 方法变成公有
优点:
- 工厂模式优点:可以方便创造结构相似的实例对象;
- 构造函数优点:可以让实例对象有可识别的类型;
- 新增优点:让方法变成公有,所有实例对象公用一个对象,而不是对每个实例都创建一个完全相同的备份。