对象(一)--对象的创建
JavaScript创建对象的几种模式
前言
我们经常听到js中一切皆对象
(其实并不,还存在基本数据类型的值), 可以知道对象在javascript中的普遍性和重要性, 但其实上面那句话中的对象侧重点的更像是一个整体引用类型
, 而我们在这里说的是自定义对象, 创建一个自定义对象可以是字面量{}
直接创建, 也可以使用new Object()
来创建, 都很方便
但是当我们要大量创建同一种类型的对象时, 就需要编写大量的重复代码, 辟如创建一个有名字和年龄的人let p1 = { name: 'p1', age: 21 }
, 再编写一个一样有名字和年龄的人, let p2 = { name: 'p2', age: 22 }
, 那如果要再创建100个人或者这个人还要有身高,体重等特征呢,我们会发现代码编写的工作量会十分的庞大。其实有其他编程语言的人会很容易地发现,其实这个就是类需要完成的工作,只要人抽象成一个类
,而这些人其实就是类的实例化对象
, 可惜的是js中并没有类这个概念, 因此就要介绍之后的几种模式来达到类这样的效果
工厂模式
创建一个函数来抽象创建具体对象的过程
function createPerson(name, age) {
var p = new Object()
p.name = name
p.age = age
p.sayHello = function() {
console.log(`Hello, my name is ${this.name}`)
}
return p
}
let p1 = createPerson('p1', 21)
console.log(p1.name) // p1
console.log(p1.age) // 21
console.log(p1.sayHello()) // Hello, my name is p1
let p2 = createPerson('p2', 22)
console.log(p2.name) // p2
console.log(p2.age) // 22
console.log(p2.sayHello()) // Hello, my name is p2
我们可以看到用工厂模式利用函数可以大大简化人
这个对象的实例化操作, 但是有个问题注意到没有, 就是这些对象无法归类, 它们到底属于哪个类呢?从内部代码中也能容易地看出它只是一个Object
类型的实例化对象, p1 instanceof Object // true
构造函数模式
虽然工厂模式做到了抽象实例化对象的操作,但是它无法归类。构造函数很好地解决了这个问题
什么是构造函数呢, 其实吧构造函数就是一个函数, JavaScript并没有指定语法规则来区分构造函数与普通函数。它们的唯一区别仅在于它们的调用方式, 我们在会构造函数前面加new
操作符来表示这是一次构造函数的调用而不是普通函数, 另外既然js无法区分这是构造函数还是普通函数, 那规则就取决于我们开发者, 一般我们会将构造函数名首字母大写加以区分
还是实现上面的person的例子
function Person(name, age) {
this.name = name
this.age = age
this.sayHello = function() {
console.log(`Hello, my name is ${this.name}`)
}
}
let p1 = new Person('p1', 21)
console.log(p1.name) // p1
console.log(p1.age) // 21
console.log(p1.sayHello()) // Hello, my name is p1
同样的, 我们利用构造函数的模式完成对象的创建, 并且与工厂模式不同的是利用构造函数的方式我们做到了对象的归类, 如创建好的p1
其实就是Person
这个构造函数的实例化对象, 测试p1 instanceof Person // true
, 可以说是工厂模式的bug修复版了
现在我们来说说这个构造函数, 第一点可以看出来我们用首字母大写的方式来命名函数名, 其次我们在函数内部看到this
, 这个this
有什么作用呢。
- 当函数用
new
进行调用时(执行函数内部的[[Constructor]]
方法),函数内部会创建一个对象,this
即指向这个对象, 把属性和方法都定义在这个对象上最后return返回 - 未使用
new
直接调用的话(执行内部的[[Call]]
方法), 这个this
指向的是全局, 浏览器的就是window
, 即会给window
添加属性和方法
需要注意的地方
- 当作为构造函数的时候内部不应该写
return
, 否则返回是return
后面跟着的对象而非this
指向的实例对象 - 箭头函数内部没有
[[Constructor]]
方法, 因此它无法作为构造函数
看似构造函数模式已经实现地挺欧克了, 但是!但是把方法直接写入到一个构造函数内部是有问题的, 尽管实例也都有了这个方法, 它的主要问题是每个对象在实例化过程都重新创建了这个sayHello
, 而各个sayHello
指向的又不是同一个函数对象, 这就造成了内存和性能上的浪费, 明明使用同一个方法就行了
let p1 = new Person('person1', 21)
let p2 = new Person('person2', 22)
p1.sayHello === p2.sayHello // false
那把这个sayHello
提取出来怎么样
function sayHello() {
console.log(`Hello, my name is ${this.name}`)
}
function Person(name, age) {
this.name = name
this.age = age
this.sayHello = sayHello
}
这单纯解决了上面的问题, 但又有个问题就是:把方法定义在全局中而不是相关的类中, 这样就没什么封装性可言。而且要是有很多构造函数,很多方法都是采取这种形式的话, 那全局环境该成什么样。为了解决构造函数方法定义的问题, 原型模式登场了
原型模式
- 构造函数中有一个
prototype
属性,它指向函数的原型对象 - 原型对象有一个
constructor
属性,它指回构造函数 - 由构造函数
new
实例化的对象,都有一个__proto__
属性(或者Object.getPrototypeOf()
), 它也指向函数的原型对象
综上所述, 逻辑结构参照如下
原型模式的特点就在于, 实例对象可以访问其属性, 比如访问实例对象p1
的name
属性
- 首先会在p1自身中查询是否有
name
这个属性, 有的话则返回 - 若自身没有这个属性, 就会去构造函数的原型对象中查询, 有
name
则返回 - 若原型对象中也没有
name
这个属性呢, 其实原型对象也是一个对象啊,它也会有它的构造函数的原型对象, 因此继续按照以上步骤查询,终点是null
(Object.prototype.__proto__
), 最后还是没法访问到的话即返回undefiend
Person = function() {}
Person.prototype = {
name: 'person',
age: '20',
testArr: [1, 2, 3],
sayHello() {
console.log(`Hello, my name is ${this.name}`)
}
}
p1 = new Person()
p2 = new Person()
console.log(p1.testArr) // [1, 2, 3]
console.log(p2.testArr) // [1, 2, 3]
p1.testArr.push(4)
console.log(p1.testArr) // [1, 2, 3, 4]
console.log(p2.testArr) // [1, 2, 3, 5]
可以看到通过原型模式有两大问题:
- 无法传参, 所有对象访问的属性其实同一个属性值
- 基于问题1所有访问的属性都是同一个值,若属性值为引用类型
Array
或者Object
, 往里添加或者删除都是会影响所有对象的访问结果
值的需要注意的是原型对象的重写
Person = function() {}
p1 = new Person()
Person.prototype = {
name: 'p',
age: '21'
}
console.log(p1.name) // undefined
这段代码看上去只是修改了下实例对象和原型对象的位置, 但是结果截然不同
因为原型对象已经重新赋值了, 在p1
实例化后它指向的原先的原型对象(此时我们并没有对它添加属性, 它只有默认construcotr
还有其他从Object
继承来的方法), 但是Person.prototyoe
却指向了另外一个对象
因此原型对象的赋值应该要小心, 还有若赋值的话也要重写constructor
指回构造函数(默认enumerable
为false
)
function Person() {}
descriptor = Object.getOwnPropertyDescriptor(Person.prototype, 'constructor')
// {value: ƒ, writable: true, enumerable: false, configurable: true}
Person.prototype = {
constructor: Person
}
descriptor = Object.getOwnPropertyDescriptor(Person.prototype, 'constructor')
// {value: ƒ, writable: true, enumerable: true, configurable: true}
构造函数和原型组合模式
结合构造函数和原型模式的特点, 新的组合模式诞生
简单来说就是:
- 把属性写到构造函数内部
- 把方法添加到原型对象属性上
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.sayHello = function() {
console.log(`Hello, my name is ${this.name}`)
}
let p1 = new Person('person1', 21)
let p2 = new Person('person2', 22)
console.log(p1 instanceof Person && p2 instanceof Person) // true
console.log(p1.sayHello()) // Hello, my name is person1
console.log(p2.sayHello()) // Hello, my name is person2
console.log(p1.sayHello === p2.sayHello) // true
Perfect!其实ES6添加的class就是基于这种构造函数和原型组合的模式的语法糖, 而非引入类
好吧, 先总结到这, 之后再讲下继承和class的继承