创建对象
了解如何创建对象有助于我们理解如何继承,和对象的
prototype
属性
创建对象的方法有许多种,后来的方法解决了前者的缺点,了解这些递进的关系有助于帮助我们更好地了解对象的工作模式
工厂模式
最开始的模式,即通过一个函数,用户可以选择传入一些参数,然后函数返回一个新的对象完成对象的创建
function createPerson (name, age, job) {
let o = new Object()
o.name = name
o.age = age
o.job = job
o.sayName = function () {
console.log('`Hello my name is ${this.name}`')
}
return o
}
let person1 = createPerson('Nicholas', 29, 'Software Engineer')
let person2 = createPerson('Greg', 27, 'Doctor')
复制代码
显而易见的是可以多次调用该函数得到多个具有相似的对象,但是其存在一些缺点:
- 我们无法知道上述的
person1
到底是不是createPerson
的实例,因为person1
的prototype
是Object.prototype
- 我们每创建一个对象都要新建一个
sayName
方法属性,即person1.sayName === person2.sayName // false
构造函数模式
构造函数模式解决了工厂模式的第一个问题,即 可以确认一个实例是否为一个 类 的对象
function Person (name, age, job) {
this.name = name
this.age = age
this.job = job
this.sayName = function () {
console.log('`Hello my name is ${this.name}`')
}
}
let person1 = new Person('Nicholas', 29, 'Software Engineer')
let person2 = new Person('Greg', 27, 'Doctor')
复制代码
这里我们使用
new
操作符来实例化一个对象时发生了以下几件事:
- 新建一个对象
- 将构造函数的
this
指向新对象- 执行构造函数中的代码
- 返回新对象
同时
person1
的prototype
被赋值为Person.prototype
因此使用person1 instanceof Person
时其实在判断Person.prototype
是否出现在了person1
的原型链之中,即可判断person1
为Person
的实例。
但是构造函数模式依然没有解决工厂模式的第二个问题。
原型模式
原型模式的存在即解决了上述所说的 第二个问题 它使得一个类的所有实例共享一些属性。而在那之前我们需要了解什么是原型对象,即上文提到的
prototype
。
prototype
这里有几个点自己觉得比较重要的,对于我自己理解何为原型比较有帮助
- prototype是一个对象
- 实例将包含一个属性,这个属性是一个指向了构造函数的原型对象的指针
- 实例中可以获取到的属性分两种,一种 实例属性 一种是 原型属性
接下来再讨论原型对象:
- 只要创建了一个函数,就会为该函数创建一个
prototype
属性,这个属性指向一个对象即该函数的原型对象- 默认情况下所有的原型对象都会自动获得一个
constructor
属性,这个属性是一个 指向prototype
所在函数的指针。即Person.prototype.constructor
指向了Person
- 当调用构造函数创建一个实例后,该实例的内部将包含一个指针,指向了构造函数的原型对象。
代码示例如下:
function Person () {}
Person.prototype.name = 'Nichalos'
Person.prototype.age = 29
Person.prototype.job = 'Software Engineer'
Person.prototype.sayName = function () {
console.log('Hello my name is ' + this.name)
}
let person = new Person()
复制代码
以上代码表现成原型状态如下,没有上图只有文字描述,多读几遍其实文字也来得精妙啦:
Person.prototype
指向了一个对象(以下简称原型对象,这里时刻牢记是一个对象),即Person
的原型对象Person
的原型对象包含一系列属性:第一个即默认情况下被赋予的constructor
属性,该属性是一个指向构造函数Person
的指针,所以Person.prototype.constructor
值为Person
;剩下的属性即代码中定义的各个属性(name
、age
、job
、sayName
),所以这个原型对象的值为:
{ constructor: Person, name: 'Nichalos', age: 29, job: 'Software Engineer', sayName: Function } 复制代码
- 实例出
person
实例后,person
被赋予了一个内部属性,这个属性指向了第2步中的原型对象,通常这个对象是__proto__
。值得注意的是,虽然我们只是实例化person
,并没有未其添加属性和方法,但是却可以通过搜索原型链 访问__proto__
中的属性和方法。我自己将其称作 原型属性
以上描述了原型对象的由来、理清楚了 实例 和 构造函数 和 原型对象 的关系:实例和构造函数没什么关系,但是实例有一个属性指向了原型对象,而原型对象是属于构造函数的,所以三者存在了联系。而接下来我们要讨论一些关于原型对象的方法:
hasOwnProperty(propertyName: String) // 返回实例是否具有该实例属性(即实例本身的属性)
in // 操作符,'name' in person,这个操作符只要对象能够访问到该属性就返回true,不论是实例属性还是原型属性
Object.keys(obj: Object) // 该方法会返回一个包含所有可枚举属性的字符串数组
Object.getOwnPropertyNames(obj: Object) // 该方法会的到所有实例的属性不论是实例的属性还是原型属性,也不管是否可枚举
复制代码
接着是原型的特性:
- 为实例添加属性会屏蔽原型中的同名属性而不是改写
- 原型具有动态性,当我们对原型对象做出修改时,能够立刻从实例上反应出来,就算是先创建了实例,再修改了原型,之前创建的实例的
[[prototype]]
依然会反应出来- 如果先创建了实例,就不能切断本来原型和构造函数之间的联系,即不能重写构造函数的
prototype
属性例如下:
function Person () {}
Person.prototype.name = 'Nicholas'
Person.prototype.age = 22
Person.prototype.sayName = function () {
console.log(`Hello, my name is ${this.name}`)
}
let person = new Person()
person.sayName() // Hello, my name is Nicholas
person.name = 'Bob'
person.sayName() // Hello, my name is Bob
Person.prototype = {
constructor: Person,
name: 'Peter',
job: 'Software Engineer',
tellJob: function () {
console.log(`Hello, my job is ${this.job}`)
}
}
let anotherPerson = new Person()
anotherPerson.tellJob() // Hello, my job is Software Engineer
anotherPerson.sayName() // type error, no method called sayName
person.sayName() // Hello, my name is Bob
person.tellJob() // type error, no method called tellJob
复制代码
以上代码解释了实例和构造函数的原型对象之间的关系,同时阐明,当我们访问一个对象的实例的时候,会先从属于实例自身的实例的属性搜索,找到了直接返回,若找不到则取原型对象中去找。同时,由于所有的实例都共享一个原型,我们可以将公共的属性和方法都写在原型对象里,让实例共享。解决了文章一开始说的第二个问题。但是这种方法引入了新的一个问题:
function Person () {}
Person.prototype = {
constructor: Person,
name: 'Nicholas',
age: 22,
job: 'Software Engineer',
friends: ['Shelby', 'Court'],
sayName: function () {
console.log(`Hello, my name is ${this.name}`)
}
}
let person1 = new Person()
let person2 = new Person()
person1.friends // ['Shelby', 'Court']
person2.friends // ['Shelby', 'Court']
person1.friends.push('Bob')
person1.friends // ['Shelby', 'Court', 'Bob']
person2.friends // ['Shelby', 'Court', 'Bob']
person1.friends === person2.friends // true
复制代码
上述代码说明,原型对象会让实例共享属性,而实际上,我们希望实例拥有自己的属性。
组合使用构造函数模式和原型模式
这个模式时为了解决上述提出的三个问题的,最重要时解决让实例拥有自己的属性,同时所有实例又共享原型上的方法。简而言之为将属性和方法分开设置。
function Person (name, age, job) {
this.name = name
this.age = age
this.job = job
this.friends = ['Shelby', 'Court']
}
Person.prototype = {
constructor: Person,
sayName: function () {
console.log(`Hello, my name is ${this.name}`)
}
}
let person1 = new Person('Nicholas', 22, 'Software Engineer')
let person2 = new Person('Greg', 27, 'Doctor')
person1.friends.push('Bob')
person1.friends // ['Shelby', 'Court', 'Bob']
person2.friends // ['Shelby', 'Court']
person1.friends === person2.friends // false
person1.sayName === person2.sayName true
复制代码
其他
其实还剩下几种创建对象的方法:
- 动态原型模型:将对构造函数
prototype
属性书写的位置换到构造函数中来- 寄生构造函数模式和稳妥构造函数模式:这两种方法创建的对象和在构造函数外部直接创建对象没有区别,所以无法判定实例和构造函数之间的关系,这里就不做记载了,有兴趣可以翻看书本进行了解。
持续更新在github