快来加入我们吧!
"小和山的菜鸟们",为前端开发者提供技术相关资讯以及系列基础文章。为更好的用户体验,请您移至我们官网小和山的菜鸟们 ( https://xhs-rookies.com/ ) 进行学习,及时获取最新文章。
"Code tailor" ,如果您对我们文章感兴趣、或是想提一些建议,微信关注 “小和山的菜鸟们” 公众号,与我们取的联系,您也可以在微信上观看我们的文章。每一个建议或是赞同都是对我们极大的鼓励!
对象
前言
在开始学习之前,我们想要告诉您的是,本文章是对JavaScript
语言知识中 “对象、类与面向对象编程” 部分的总结,如果您已掌握下面知识事项,则可跳过此环节直接进入题目练习
- 对象的基本构造
- 对象声明及使用
- 类
- 对象的结构赋值
- 继承
- 包装对象
如果您对某些部分有些遗忘,👇🏻 已经为您准备好了!
汇总总结
ECMA-262
将对象定义为一组属性的无序集合。严格来说,这意味着对象就是一组没有特定顺序的值。对象的每个属性或方法都由一个名称来标识,这个名称映射到一个值。正因为如此(以及其他还未讨论的原因),可以把ECMAScript
的对象想象成一张散列表,其中的内容就是一组名/值对,值可以是数据或者函数。
对象的基本构造
创建自定义对象的通常方式是创建 Object 的一个新实例,然后再给它添加属性和方法,如下例 所示:
let person = new Object()
person.name = 'XHS-rookies'
person.age = 18
person.job = 'Software Engineer'
person.sayName = function () {
console.log(this.name)
}
这个例子创建了一个名为 person
的对象,而且有三个属性(name
、age
和 job
)和一个方法(sayName()
)。sayName()
方法会显示 this.name
的值,这个属性会解析为 person.name
。早期JavaScript
开发者频繁使用这种方式创建新对象。几年后,对象字面量变成了更流行的方式。前面的例子如果使用对象字面量则可以这样写:
let person = {
name: 'XHS-rookies',
age: 18,
job: 'Software Engineer',
sayName() {
console.log(this.name)
},
}
这个例子中的 person
对象跟前面例子中的 person
对象是等价的,它们的属性和方法都一样。这些属性都有自己的特征,而这些特征决定了它们在 JavaScript
中的行为。
对象声明及使用
综观 ECMAScript
规范的历次发布,每个版本的特性似乎都出人意料。ECMAScript 5.1
并没有正式 支持面向对象的结构,比如类或继承。但是,正如接下来几节会介绍的,巧妙地运用原型式继承可以成 功地模拟同样的行为。ECMAScript 6
开始正式支持类和继承。ES6
的类旨在完全涵盖之前规范设计的基于原型的继承模式。不过,无论从哪方面看,ES6
的类都仅仅是封装了ES5.1
构造函数加原型继承的语法糖而已。
工厂模式
工厂模式是一种众所周知的设计模式,广泛应用于软件工程领域,用于抽象创建特定对象的过程。下面的例子展示了一种按照特定接口创建对象的方式:
function createPerson(name, age, job) {
let o = new Object()
o.name = name
o.age = age
o.job = job
o.sayName = function () {
console.log(this.name)
}
return o
}
let person1 = createPerson('XHS-rookies', 18, 'Software Engineer')
let person2 = createPerson('XHS-boos', 18, 'Teacher')
这里,函数 createPerson()
接收 3 个参数,根据这几个参数构建了一个包含 Person
信息的对象。可以用不同的参数多次调用这个函数,每次都会返回包含 3 个属性和 1 个方法的对象。这种工厂模式虽然可以解决创建多个类似对象的问题,但没有解决对象标识问题(即新创建的对象是什么类型)。
构造函数模式
ECMAScript
中的构造函数是用于创建特定类型对象的。像 Object
和 Array
这 样的原生构造函数,运行时可以直接在执行环境中使用。当然也可以自定义构造函数,以函数的形式为 自己的对象类型定义属性和方法。 比如,前面的例子使用构造函数模式可以这样写:
function Person(name, age, job) {
this.name = name
this.age = age
this.job = job
this.sayName = function () {
console.log(this.name)
}
}
let person1 = new Person('XHS-rookies', 18, 'Software Engineer')
let person2 = new Person('XHS-boos', 18, 'Teacher')
person1.sayName() // XHS-rookies
person2.sayName() // XHS-boos
在这个例子中,Person()
构造函数代替了createPerson()
工厂函数。实际上,Person()
内部 的代码跟 createPerson()
基本是一样的,只是有如下区别。
-
没有显式地创建对象。
-
属性和方法直接赋值给了
this
。 -
没有
return
。
另外,要注意函数名 Person
的首字母大写了。按照惯例,构造函数名称的首字母都是要大写的, 非构造函数则以小写字母开头。这是从面向对象编程语言那里借鉴的,有助于在 ECMAScript
中区分构 造函数和普通函数。毕竟 ECMAScript
的构造函数就是能创建对象的函数。
要创建 Person
的实例,应使用 new
操作符。以这种方式调用构造函数会执行如下操作。
(1)在内存中创建一个新对象。
(2)这个新对象内部的 [[Prototype]]
特性被赋值为构造函数的 prototype
属性。
(3)构造函数内部的 this
被赋值为这个新对象(即 this
指向新对象)。
(4)执行构造函数内部的代码(给新对象添加属性)。
(5)如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。
上一个例子的最后,person1
和 person2
分别保存着 Person
的不同实例。这两个对象都有一个 constructor
属性指向 Person
,如下所示:
console.log(person1.constructor == Person) // true
console.log(person2.constructor == Person) // true
constructor
本来是用于标识对象类型的。不过,一般认为 instanceof
操作符是确定对象类型更可靠的方式。前面例子中的每个对象都是 Object
的实例,同时也是 Person
的实例,如下面调用 instanceof
操作符的结果所示:
console.log(person1 instanceof Object) // true
console.log(person1 instanceof Person) // true
console.log(person2 instanceof Object) // true
console.log(person2 instanceof Person) // true
定义自定义构造函数可以确保实例被标识为特定类型,相比于工厂模式,这是一个很大的好处。在 这个例子中,person1
和 person2
之所以也被认为是 Object
的实例,是因为所有自定义对象都继承自 Object
(后面再详细讨论这一点)。构造函数不一定要写成函数声明的形式。赋值给变量的函数表达式也可以表示构造函数:
let Person = function (name, age, job) {
this.name = name
this.age = age
this.job = job
this.sayName = function () {
console.log(this.name)
}
}
let person1 = new Person('XHS-rookies', 18, 'Software Engineer')
let person2 = new Person('XHS-boos', 18, 'Teacher')
person1.sayName() // XHS-rookies
person2.sayName() // XHS-boos
console.log(person1 instanceof Object) // true
console.log(person1 instanceof Person) // true
console.log(person2 instanceof Object) // true
console.log(person2 instanceof Person) // true
在实例化时,如果不想传参数,那么构造函数后面的括号可加可不加。只要有 new
操作符,就可以调用相应的构造函数:
function Person() {
this.name = 'rookies'
this.sayName = function () {
console.log(this.name)
}
}
let person1 = new Person()
let person2 = new Person()
person1.sayName() // rookies
person2.sayName() // rookies
console.log(person1 instanceof Object) // true
console.log(person1 instanceof Person) // true
console.log(person2 instanceof Object) // true
console.log(person2 instanceof Person) // true
1. 构造函数也是函数
构造函数与普通函数唯一的区别就是调用方式不同。除此之外,构造函数也是函数。并没有把某个函数定义为构造函数的特殊语法。任何函数只要使用 new
操作符调用就是构造函数,而不使用 new
操作符调用的函数就是普通函数。比如,前面的例子中定义的 Person()
可以像下面这样调用:
// 作为构造函数
let person = new Person('XHS-rookies', 18, 'Software Engineer')
person.sayName() // "XHS-rookies"
// 作为函数调用
Person('XHS-boos', 18, 'Teacher') // 添加到 window 对象
window.sayName() // "XHS-boos"
// 在另一个对象的作用域中调用
let o = new Object()
Person.call(o, 'XHS-sunshineboy', 25, 'Nurse')
o.sayName() // "XHS-sunshineboy"
这个例子一开始展示了典型的构造函数调用方式,即使用 new
操作符创建一个新对象。然后是普通函数的调用方式,这时候没有使用 new
操作符调用 Person()
,结果会将属性和方法添加到 window
对象。这里要记住,在调用一个函数而没有明确设置 this
值的情况下(即没有作为对象的方法调用,或 者没有使用 call()/apply()
调用),this
始终指向 Global
对象(在浏览器中就是 window
对象)。 因此在上面的调用之后,window
对象上就有了一个 sayName()
方法,调用它会返回 "Greg"
。最后展示的调用方式是通过 call()
(或apply()
)调用函数,同时将特定对象指定为作用域。这里的调用将 对象 o
指定为 Person()
内部的 this
值,因此执行完函数代码后,所有属性和 sayName()
方法都会添加到对象 o
上面。
2. 构造函数的问题
构造函数虽然有用,但也不是没有问题。构造函数的主要问题在于,其定义的方法会在每个实例上都创建一遍。因此对前面的例子而言,person1
和 person2
为sayName()
的方法,但这两个方法不是同一个 Function
实例。我们知道,ECMAScript
中的函数是对象,因此每次定义函数时,都会初始化一个对象。逻辑上讲,这个构造函数实际上是这样的:
function Person(name, age, job) {
this.name = name
this.age = age
this.job = job
this.sayName = new Function('console.log(this.name)') // 逻辑等价
}
这样理解这个构造函数可以更清楚地知道,每个 Person
实例都会有自己的 Function
实例用于显 示 name
属性。当然了,以这种方式创建函数会带来不同的作用域链和标识符解析。但创建新 Function
实例的机制是一样的。因此不同实例上的函数虽然同名却不相等,如下所示:
console.log(person1.sayName == p