JavaScript 面向对象编程-创建对象
一、介绍对象
对象是什么?
一切皆对象(Everything is object)
这句话乍一听似乎有些抽象,可以从两个方面来理解。
(1)单个事务的抽象
如一个人、一只猫、一台电脑、一张纸这些实物都可以抽象为对象, 实物之间的关系就变成了对象之间的关系,我们就可以通过用对象来模拟实物来编程。
(2)对象是一个容器,封装了属性(property)和方法(method)
属性是对象的状态,方法描述对象的行为。比如,我们可以把人抽象为people对象,使用“属性”记录名字、年龄、性别、身高等,使用“方法”表示人的行为(吃饭、睡觉、工作、运动等等)。
二、创建对象
创建对象的方式:
- 调用构造函数Object
- 以字面量的形式
- 工厂函数
- 构造函数
- 构造函数 + 原型
简单方式
方式一:调用系统的构造函数 Object
let lucy = new Object()
lucy.name = 'Lucy'
lucy.gender = '女'
lucy.eat = function () {
console.log(`I like eating banana!`)
}
每次都通过 new Object()
创建对象比较麻烦,可以通过它的简写形式对象字面量来创建。
方式二:字面量形式
let lucy = {
name: 'Lucy',
gender: 女,
show: function () {
console.log(`hello,my name is ${this.name}`)
}
}
通过上面两种方式创建对象对比:尽量使用字面量发创建对象,理由如下:
(1)字面量创建对象代码更加简洁,强调该对象仅是一个可变的hash映射,而不是从对象中提取的属性或方法。
(2) 对象字面量运行速度更快,因为它们在解析时不需要作用域解析。而当我们调用Object()的时候,解析器需要顺着作用域链从当前作用域开始查找,如果在当前作用域找到了名为Object()的函数就执行,如果没找到,就继续顺着作用域链往上照,直到找到全局Object()构造函数为止。
(3)使用构造函数Object可以接受一个参数,并且会根据传递的值的类型使用对应的内置函数来创建对象,如果传入的参数是动态的,直到运行时才确定其类型,这种行为可能会导致意想不到的结果。
let obj1 = new Object(1)
console.log(obj1.constructor === Number) // true
let obj2 = new Object('lucy')
console.log(obj2.constructor === String) // true
通过字面量创建对象代码似乎挺简洁的,但是假如我们要创建的对象数量为两个呢?
let lucy = {
name: 'Lucy',
gender: 女,
show: function () {
console.log(`hello,my name is ${this.name}`)
}
}
let benson = {
name: 'Benson',
gender: 男,
show: function () {
console.log(`hello,my name is ${this.name}`)
}
}
上面的代码给人感觉冗余,不过还能勉强接受,但是如果我们需要创建的对象为五个、十个、甚至更多,显然还是按照字面量的方式创建对象并不适合了。
总结:单个或少数量的对象,建议以字面量的形式创建,但是不建议用该方式创建多个对象。
工厂函数
工厂模式
: 封装一个函数,函数内部创建一个对象并返回,可以理解为对创建的对象进行封装,调用一次函数则创建一个对象 。
function createPerson (name, gender) {
let obj = {
name: name,
gender: gender,
show: function () {
console.log(`hello,my name is ${this.name}`)
}
}
return obj
}
let lucy = createPerson('Lucy', '女')
let benson = createPerson('Benson', '男')
在此可以做进一步简化,减少给对象赋值的过程,直接将定义的对象返回,简写代码:
function createPerson (name, gender) {
return {
name: name,
gender: gender,
show: function () {
console.log(`hello,my name is ${this.name}`)
}
}
}
let lucy = createPerson('Lucy', '女')
let benson = createPerson('Benson', '男')
这样封装确实爽太多了,解决了创建多个相似对象代码冗余的问题, 但却存在另外一个问题:
console.log(lucy instanceof Object) // true
console.log(lucy instanceof createPerson) // false
console.log(benson instanceof Object) // true
console.log(benson instanceof createPerson) // false
通过上面打印的结果我们可以发现没有办法知道一个对象具体的类型。值得注意的是,任何对象和 Object 在 instanceof 检查时都会返回 true,因为所有对象都是Object的后代 ,具体原因在后面的原型中会提到。
总结:将对象的创建封装为一个工厂函数,方便创建多个对象,但是没办法区分对象的类型
构造函数
它的用法是,先定义一个构造函数:
function CreatePerson (name, gender) {
this.name = name,
this.gender = gender
this.show = function () {
console.log(`hello,my name is ${this.name}`)
}
}
这时候你可能会疑惑,咦,这不就是一个普通函数吗?无非就是函数名首字母大写了。
没错,这确实是一个普通函数,但是又不同于普通函数,调用时需要使用new
关键字 来调用,调用new CreatePerson('Lucy', '女')
后会返回一个对象。
let lucy = new CreatePerson('Lucy', '女')
lucy.name; // 'Lucy'
lucy.show(); // hello,my name is Lucy
let benson = new CreatePerson('Benson', '男')
benson.name; // 'Benson'
benson.show(); // hello,my name is Benson
Remark:
(1)构造函数与其他函数的唯一区别,就在于调用他们的方式不同。如果不用关键字new
,这就是一个普通函数,并返回undefined
。但是,如果写了new
,它就变成了一个构造函数,它绑定的this
指向新创建的对象,并默认返回this
,也就是说,不需要在最后写return this;
。
(2)构造函数函数名首字母也可以小写,但是为了区别于普通函数,建议大写。
下面来验证构造函数有没有解决掉工厂函数无法知道一个对象类型的问题。
console.log(lucy instanceof Object) // true
console.log(lucy instanceof createPerson) // true
console.log(benson instanceof Object) // true
console.log(benson instanceof createPerson) // true
好了,通过上面的代码可以看到构造函数能够查看对象的类型,同样可以看到,创建的对象和Object在instanceof 检查时都会返回true,具体原因在后面的原型中会提到。
通过上面的一步步优化,似乎使用构造函数是我们创建多个对象时的最佳选择,其实使用构造函数也存在一些问题----那就是构造函数中的所有属性和方法都要在每个实例对象上重新创建一遍。
如何证实我的说法呢?
在前面的例子中,lucy 和 benson 都有一个 show 的方法,但这两个方法不是同一个 Function 的实例。因为js中函数也是对象,因此每声明一个新的函数,就会创建并初始化一个新的函数对象,这个函数对象会包含以下属性:length,prototype等 。从逻辑角度讲,此时的构造函数也可以这样定义。
function CreatePerson (name, gender) {
this.name = name,
this.gender = gender
this.show = new Function(console.log(`hello,my name isuu ${this.name}`)) // 与声明函数在逻辑上是等价的
}
直接打印两个实例对象 lucy 和 benson 的属性和方法进行对比:
console.log(lucy.name === benson.name) // false
console.log(lucy.show === benson.show) // false
从上面的执行结果,可以看出 lucy.show 和 benson.show 指向的不是同一个内存地址,而是存在两个不同的内存地址【在js中,引用类型比较的是地址, 函数是一种引用类型】,而 lucy 和 benson 的 name 属性的值都是根据传入的参数赋值,一般来说我们都希望每个实例对象都分别拥有自己的的属性,因此属性一般都在构造函数中定义。但是如果方法执行的都是一样的代码,就没有必要每个实例对象都复制一份,浪费内存,这就是使用构造函数创建对象的缺点 。那么如何来实现实例对象共享方法呢?
我们可以把需要共享的方法定义到全局作用域来解决这个问题:
function CreatePerson (name, gender) {
this.name = name,
this.gender = gender
this.show = show
}
function show() {
console.log(`hello,my name is ${this.name}`)
}
let lucy = new CreatePerson('Lucy', '女')
let benson = new CreatePerson('Benson', '男')
console.log(lucy1.show === benson.show) // true
在上面的代码中把构造函数中的 show 属性指向一个全局函数,从执行结果可以看到,创建的实例对象都指向同一个内存地址,解决了构造函数创建多个实例对象,方法都会在内存中开辟一个新的空间造成内存浪费的问题,但是在全局作用域定义函数又存在污染全局作用域问题,而且如果对象需要定义很多方法,那么就要定义很多个全局函数,就没有封装性可言了了,因此不推荐。
那么是不是没有更好的办法呢?当然不是,我们可以通过使用构造函数+原型模式创建对象来解决上述的问题。(PS:其实前面说了这么多创建对象的方式其实是为了做一个对比、逐步优化的过程,采用 构造函数+原型模式 创建对象才是目前在 ECMAScript 使用最广泛的创建自定义类型的方式,才属于重头戏的开场,哈哈。。。说的有点飘了)
接下来就讲讲原型 prototype,请点击下一篇链接:JavaScript 面向对象编程-原型prototype