1、什么是原型对象
无论什么时候,只要创建了一个函数,就会根据特定的规则为该函数创建一个prototype属性,该属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个constructor(构造函数)属性,这个属性包含一个指向prototype属性所在函数的指针。当调用构造函数创建了一个实例后,该实例的内部将包含一个指针( __proto__ ),指向构造函数的原型对象,注意的是这个连接只存在于实例和构造函数的原型对象之间。
以上摘自【JavaScript高级程序设计】,下面用一个例子来加以理解。
function Person (name, age) {
this.name = name
this.age = age
}
Person.prototype.sayName = function () {
console.log(this.name)
}
let person1 = new Person('li', 24) //li
let person2 = new Person('pan', 25) //pan
Person.prototype指向了原型对象,Person.prototype.construction又指回了Person构造函数,而person1和person2都是构造函数Person的实例,因此person1和person2的__proto__属性都指向了原型对象。

2、继承
说到继承,就不得不先提一下原型链,上面例子中,Person构造函数创建了实例,实例的__proto__属性指向了原型对象,如果Person构造函数的原型对象是另一个构造函数的实例,那Person构造函数的原型对象的__proto__属性会指向另一个构造函数的原型对象,层层递进,这就构成了原型链。
2.1 原型链继承
function Parent () {
this.name = 'parent'
this.color = ['red', 'blue', 'green']
}
Parent.prototype.getParentName = function () {
return this.name
}
function Child () {
this.childName = 'child'
}
// 继承Parent
Child.prototype = new Parent()
Child.prototype.getChildName = function () {
return this.childName
}
let child1 = new Child()
child1.colors.push('black')
let child2 = new Child()
console.log(child1.getParentName())
console.log(child1.getChildName())
console.log(child1.colors) // ["red", "blue", "green", "black"]
console.log(child2.colors) // ["red", "blue", "green", "black"]
以上代码的继承是通过创建Parent实例,并将实例赋值给Child.prototype实现的。本质是重写原型对象,也就是原本属于Parent实例的属性和方法现在也在Child.prototype中了。

在以上例子中,Parent构造函数定义了一个引用类型colors属性,这会使得所有实例都共享该属性,因此无法避免原型中包含引用类型值所带来的问题。
题外话,typeof 和 instanceof
都知道typeof运算符用于判断对象的类型,但是对于一些创建的对象,它们都会返回'object',所以我们需要判断一个实例是否为某个对象的实例的话,就要用到instanceof。instanceof运算符用来判断一个构造函数的原型对象是否存在另外一个要检测对象的原型链上。
console.log(child instanceof Object) //true
console.log(child instanceof Child) //true
console.log(child instanceof Parent) //true
手写instanceof函数
function instanceofFun (obj, cons) {
if (typeof cons !== 'function') throw Error('instance error')
if (!obj || (typeof obj !== 'object' && typeof obj !== 'function')) return false
let left = obj.__proto__
let right = cons.prototype
while (true) {
if (left === null) return false
if (left === right) return true
left = left.__proto__
}
}
Q:typeof 和 instanceof 可以判断出对象的真正类型吗?
A:不能,typeof []和{}都是object;[]继承于Array,也继承于Object。用Object.prototype.toString.call(val)来判断真正的类型。
console.log(Object.prototype.toString.call('xxx')) // [object String]
console.log(Object.prototype.toString.call([])) // [object Array]
console.log(Object.prototype.toString.call(undefined)) // [object Ubdefined]
console.log(Object.prototype.toString.call(null)) // [object Null]
console.log(Object.prototype.toString.call(123)) // [object Number]
console.log(Object.prototype.toString.call({})) // [object Object]
console.log(Object.prototype.toString.call(() => {})) // [object Function]
2.2 借用构造函数继承
function Parent () {
this.name = 'parent'
this.colors = ['red', 'blue', 'green']
}
function Child () {
Parent.call(this)
this.childName = 'child'
}
let child1 = new Child()
child1.colors.push('black')
let child2 = new Child()
console.log(child1.colors)
console.log(child2.colors)
这种方法相当于把Parent构造函数中定义的所有对象方法都复制了一个副本到实例中,因此就没有函数复用的说法了。
2.3 组合继承
function Parent (name) {
this.name = 'parent'
this.colors = ['red', 'blue', 'green']
}
Parent.prototype.getName = function () {
return this.name
}
function Child (name, age) {
Parent.call(this) // 第二次调用Parent()
this.age = 'child'
}
// 继承Parent
Child.prototype = new Parent() // 第一次调用Parent()
Child.prototype.constructor = Child
Child.prototype.getAge = function () {
return this.age
}
let child1 = new Child('li', 22)
let child2 = new Child('pan', 23)
上面方法既可以避免了原型链和借用构造函数的缺点,又融合了他们的优点。不过仔细看,会发现不管怎样,都会调用两次Parent构造函数,第一次调用,Child.prototype会得到两个属性name和colors,第二次调用的时候是在新对象上创建了实例属性name和colors,只不过这两个属性屏蔽掉了原型中的两个同名属性。
2.4 寄生组合式继承
为了解决上面的问题,引入了寄生组合式继承。
function inheritPrototype (subObj, superObj) {
let prototype = Object(superObj.prototype) // 创建对象
// 弥补因重写原型而失去的默认的constructor属性
prototype.constructor = subObj // 增强对象
subObj.prototype = prototype // 指定对象
}
function Parent (name) {
this.name = 'parent'
this.colors = ['red', 'blue', 'green']
}
Parent.prototype.getName = function () {
return this.name
}
function Child (name, age) {
Parent.call(this) // 继承属性
this.age = 'child'
}
// 继承Parent
inheritPrototype(Child, Parent)
Child.prototype.getAge = function () {
return this.age
}
这个例子只调用了一次Parent构造函数,避免了在Child.prototype上创建不必要,多余的属性。寄生组合式继承的本质是:
1、使用寄生式继承超类型的原型,然后将结果指定给子类型的原型;
2、通过借用构造函数来继承超类型的属性。
参考书籍
《Javascript高级程序设计》