一、原型
原型的作用是什么?
在理解原型的作用之前,我们需要先知道一个例子
function Person(name) {
this.name = name
this.sayName = function () {
console.log(this.name)
}
}
const p1 = new Person('张三')
const p2 = new Person('李四')
console.log(p1.sayName === p2.sayName) //false
这里我们声明一个构造函数并在这个构造函数内部定义了相关的属性和方法,我们可以看到,该构造函数在声明实例的时候,sayName这个方法每个实例都重新声明创建了一遍,但是不同实例sayName的内容是完全一样的,这就导致了不必要的浪费。而原型最重要的作用,就是为了让实例去共享相关的属性和方法。如下所示:
function Person(name){
this.name = name
}
//将sayName声明在原型对象上
Person.prototype.sayName = function(){
console.log(this.name)
}
const p1 = new Person('张三')
const p2 = new Person('李四')
console.log(p1.sayName === p2.sayName) //true
实例、构造函数、原型对象的关系
每个构造函数都会创建一个prototype的属性,这个属性的值是一个对象,包含应该由特定引用类型的实例共享的属性和方法。这个对象就是我们所说的原型对象。默认情况下,所有原型对象自动获得一个名为constructor的属性,指回与之关联的构造函数。而通过构造函数来实例化的对象,被称之为实例,每个实例都有一个__proto__的属性,来指向实例的原型对象。如下图所示:
function Person(){}
console.log(typeof Person.prototype) //object
console.log(Person.prototype) //{}
console.log(Person.prototype.constructor === Person) //true
console.log(Person.prototype.__proto__ === Object.prototype) //true
console.log(Person.prototype.__proto__.constructor === Object) //true
console.log(Person.prototype.__proto__.__proto__ === null) //true
console.log(Person.prototype.__proto__)
let person1 = new Person()
person2 = new Person()
// 构造函数和实例以及原型对象是三个完全不同的对象
console.log(person1 !== Person)//true
console.log(person1 !== Person.prototype)//true
console.log(Person.prototype !== Person)//true
// 实例的__proto__指向原型对象
console.log(person1.__proto__ === Person.prototype)//true
console.log(person2.__proto__.constructor === Person)//true
console.log(person1.__proto__ === person2.__proto__) //true
// instanceof检查实例的原型链中是否包含指定构造函数的原型
console.log(person1 instanceof Person) //true
console.log(person2 instanceof Object) //true
console.log(Person.prototype instanceof Object) //true
原型相关的方法(以Person构造函数,实例person1为例)
isPrototypeOf():原型对象的方法,可以用来判断person1是否为Person的实例
Object.getPrototypeOf():返回参数的内部特性[[Prototype]]的值
Person.prototype.isPrototypeOf(person1) //true
Object.getPrototypeOf(person1) 返回person1的原型对象Person.prototype
原型层级及属性访问
通过对象访问属性时,会按照属性名称开始搜索,优先搜索实例本身,如果实例本身不存在,则去实例的原型对象上找是否存在该属性。如果实例上已经存在了同名属性,然后还想访问原型对象上的同名属性,需要使用delete操作符删除实例上的同名属性(只能删除,设置为null无效)。
hasOwnProperty方法用于确定某个属性是在实例上还是在原型对象上。
in操作符可以用于判断某个属性是否在实例属性上或者原型上。
for-in循环中使用in操作符时,可以通过对象访问且可以被枚举的属性都会返回,包括实例属性和原型属性。
function Person(name) {
this.name = name
}
function hasPrototypeProperty(obj, property) {
return !Object.hasOwnProperty(property) && (property in obj)
}
Person.prototype.name = "李四"
Person.prototype.work = "developer"
Person.prototype.language = ["英语"]
let p = new Person("张三")
console.log(p.name)
console.log(p.work)
console.log("还没删除name", "name" in p)
console.log(hasPrototypeProperty(p, "name"))
delete p.name
console.log("删除了name", "name" in p)
console.log(hasPrototypeProperty(p, "name"))
console.log(p.name)
p.language = ["英语", "中文"]
// p.language.push('中文')
let p2 = new Person("王五")
console.log(p.language)
console.log(p2.language)
console.log(Object.hasOwnProperty("name"))
输出结果
张三
developer
还没删除name true
false
删除了name true
false
李四
[ '英语', '中文' ]
[ '英语' ]
true
注意:因为从原型上搜索值的过程是动态的,所以即使实例在修改原型之前已经存在,任何时候对原型对象所做的修改也会在实例上反映出来
原型的问题
1.构造函数的弱化
原型弱化了向构造函数传递初始化参数的能力,会导致所有实例默认都取得相同的属性值
2.数据共享性在引用值属性上的问题
举个例子:当一个原型对象的属性是一个数组时,我们在实例中如果访问这个数组并调用数组的push,那就会导致原型上数组内容也发生改变,如果是就是想要修改原型数组内容还好,如果不是,则可能导致所有引用到这个数组地地方可能带来意想不到的问题。
二、原型链
每个构造函数都有一个原型对象,原型有一个属性指回构造函数,而实例有一个内部指针指向原型。如果原型是另一个类型的实例呢?那就意味着这个原型本身有一个内部指针指向另一个原型,相应地另一个原型也有一个指针指向另一个构造函数。这样就在实例和原型之间构造了一条原型链。基本上所有的函数,都包含Object原型。而Object的prototype指向的是null。
function SuperType() {
this.property = true
}
SuperType.prototype.getSuperValue = function () {
return this.property
}
function SubType() {
this.subproperty = false
}
SubType.prototype = new SuperType()
SubType.prototype.getSubValue = function () {
return this.subproperty
}
let instance = new SubType()
console.log(instance.getSuperValue()) // true
完整的原型链如下:
原型链实现继承的问题
1.原型中存在引用值是可能导致修改原型引用值的问题
详细描述参考上文原型的问题,第二点
2.子类型在实例化时不能给父类型的构造函数传参
三、其他继承实现
1.盗用构造函数
在子构造函数中通过直接调用父类构造函数并采用call/apply方法将参数传递给父构造函数。
缺点:无法实现公共函数在不同实例的共享问题,以及无法访问父类的原型对象中的方法。
优点:子类创建的每个实例,都各自声明了各自的父类属性,不会出现原型链中的引用值问题。
function SuperType(name){
this.name = name
}
SuperType.prototype.getName = function(){
return this.name
}
function SubType(){
SuperType.call(this,"张三")
this.age = 29
}
var instance = new SubType()
console.log(instance.name) //张三
console.log(instance.age) //29
console.log(instance.getName()) //报错
2.组合继承(经典继承)
组合继承即同时结合盗用构造函数和继承原型的方式。
优点:采取盗用构造函数继承实例属性,避免了引用值的问题,同时使用原型链继承原型上的属性和方法。
缺点:父类构造函数始终会被调用两次,一次在是创建子类原型时调用,另一次是在子类构造函数中调用。
function SuperType(name) {
this.name = name
this.colors = ["red", "blue", "green"]
}
SuperType.prototype.sayName = function () {
console.log(this.name)
}
function SubType(name, age) {
SuperType.call(this, name)
this.age = age
}
SubType.prototype = new SuperType()
SubType.prototype.sayAge = function(){
console.log(this.age)
}
let instance1 = new SubType("张三",24)
instance1.colors.push('yellow')
instance1.sayAge()
instance1.sayName()
console.log(instance1.colors)
let instance2 = new SubType("李四",35)
console.log(instance2.colors)
instance2.sayAge()
instance2.sayName()
输出结果:
24
张三
[ 'red', 'blue', 'green', 'yellow' ]
[ 'red', 'blue', 'green' ]
35
李四
3.寄生式组合继承
寄生式组合继承就是在组合继承的基础上进行的改良,子类的prototype直接指向一个父类的prototype的副本,这样就保证了仅有子类存在同名的属性,以及在处理原型的时候不触发副本的构造函数执行。
优点:即实现了原型链的继承也解决了引用值的问题,同时避免了调用两次父类的构造函数。
function object(o) {
function F() {}
F.prototype = o
return new F()
}
function inheritPrototype(subType, superType) {
// 创建父类原型的副本,免去了一次构造函数的调用,直接继承superType的原型链副本
let prototype = object(superType.prototype)
// 解决由于重写原型导致默认constructor丢失的问题
prototype.constructor = subType
//
subType.prototype = prototype
}
function SuperType(name) {
console.log("SuperType")
this.name = name
this.colors = ["red", "blue", "green"]
}
SuperType.prototype.sayName = function () {
console.log(this.name)
}
function SubType(name, age) {
SuperType.call(this, name) //第二次调用
this.age = age
}
inheritPrototype(SubType, SuperType)
SubType.prototype.sayAge = function () {
console.log(this.age)
}
var instance1 = new SubType("张三", 24)
instance1.colors.push("yellow")
instance1.sayAge()
instance1.sayName()
console.log(instance1.colors)
var instance2 = new SubType("李四", 35)
console.log(instance2.colors)
instance2.sayAge()
instance2.sayName()
输出结果:
SuperType
SuperType
24
张三
[ 'red', 'blue', 'green', 'yellow' ]
SuperType
[ 'red', 'blue', 'green' ]
35
李四