在面试中,原型、原型链以及继承是一个高频的问点,大家其实一直在使用,比如数组的方法forEach,map等等,只是可能未曾概念化,阿树在此整理了一些东西,希望对大家有所帮助!
一、构造函数和原型
1.1 构造函数
构造函数,就是提供一个生成对象的模板,主要用来初始化对象,即为对象成员变量赋初始值。 使用构造函数初始化对象的时候,需要使用到 new 关键字。new在执行的时候分为以下几步:
- 在内存中创建一个空的对象;
- 让this指向这个新对象;
- 执行构造函数里面的代码,给这个新对象添加属性和方法;
- 返回这个新对象。
1.1.1 构造函数中的属性和方法
在这里,我们将构造函数中的属性和方法称为成员,我们可以有两类成员:实例成员 和 静态成员
function Person(name, age, love) {
this.name = name;
this.age = age;
this.love = love;
this.loving = function() {
console.log('世界这么大,我还在搬砖!')
}
}
实例成员
- 实例成员就是构造函数内部通过this添加的成员,只能通过实化的对象来访问:
let james = new Person('James', 36, '篮球')
console.log(james.names) // James
james.loving() // 世界这么大,我还在搬砖!
console.log(Person.names) // undefined
Person.loving() // Person.loving is not a function
- 从下面的打印结果来看,实例化后的变量 James 可以访问到相对应的属性方法,而构造函数本身并不能够访问到相应的属性方法:
静态成员
- 静态成员就是给构造函数本身添加的属性和方法,只能够通过构造函数本身来访问:
let james = new Person('James', 36, '篮球')
Person.like = '奔向自由'
Person.doing = function () {
console.log('构造函数本身的方法')
}
console.log(Person.like) // 奔向自由
console.log(james.like) // undefined
Person.doing() // 构造函数本身的方法
james.doing() // james.doing is not a function
- 从下面的打印结果来看,给构造函数本身添加的方法和属性其本身能够访问,但是实例化后的变量 James 并不能够访问:
1.1.2 构造函数存在的问题
频繁使用构造函数实例化对象,造成内存的浪费
function Person(name, age, love) {
this.names = name;
this.age = age;
this.love = love;
this.loving = function() {
console.log('世界这么大,我还在搬砖!')
}
}
let james = new Person('James', 36, '篮球')
let kobe = new Person('kobe', 41, '篮球')
- 从下面创建的对象占用的内存资源来看,每次实例化,均会开辟新的内存空间去存储相应的方法,但是方法对于每个实例来说完全可以共用,这里就造成了内存的大量浪费:
1.1.3 构造函数的原型对象 prototype
除了Object.create(null)创建出来的对象是没有原型的之外, 基本上其余 JavaScript 对象都会从一个 prototype(原型对象)中继承属性和方法, 且 JavaScript 中的对象都是位于原型链顶端的 Object 的实例。
JavaScript规定,每一个构造函数都有一个prototype属性,我们可以把一些公用的方法,直接定义在prototype对象上,这样所有的实例化对象都可以从同一个存储中获取方法,极大的节省了内存。(如下图所示)
function Person(name, age, love) {
this.names = name;
this.age = age;
this.love = love;
}
Person.prototype.loving = function() {
console.log('世界这么大,我还在搬砖!')
}
let james = new Person('James', 36, '篮球')
let kobe = new Person('kobe', 41, '篮球')
james.loving() // 世界这么大,我还在搬砖!
kobe.loving() // 世界这么大,我还在搬砖!
console.log(james.loving === kobe.loving) // true
- 在我们对构造函数进行实例化的时候,实例化对象并没创建方法属性,我们在通过实例化对象访问方法的时候,会在原型prototype中去寻找:
读到这里大家可能会有疑惑,实例对象是怎样访问到 prototype 原型对象里边的属性的呢?我们接着往下看!
1.1.4 对象的原型 __proto__
对象都会有一个属性__proto__指向构造函数的prototype原型对象。 JavaScript 对象有一个指向一个原型对象的链。当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。
这里我们继续分析上述例子james.loving() 的查找规则:
- __proto__ 对象原型 和 原型对象 prototype是等价的;
- __proto__ 对象原型 的存在为对象的查找机制提供了一个线路方向,但要注意的是它是内部指向原型对象的,开发中不能使用这个属性
1.1.5 constructor 构造函数
constructor指向的是构造函数本身,在对象原型 __proto__ 和 原型对象 prototype中均有其存在, 主要好似用于记录该对象引用于哪个构造函数,他可以让原型对象 重新指向原来的构造函数:
console.log('Person.prototype------------', Person.prototype)
console.log('james.__proto__', james.__proto__)
案例场景:
当我们要给原型对象添加多个方法的时候,可以使用 Person.prototype = {}来添加,但是这样会造成对象原型指向错误
Person.prototype = {
loving: function() {
console.log('世界这么大,我还在搬砖!')
},
dream: function() {
console.log('不想一直搬砖!')
}
}
let james = new Person('James', 36, '篮球')
console.log('Person.prototype------------', Person.prototype)
console.log('james.__proto__-------------', james.__proto__)
console.log('Person.prototype.constructor------------', Person.prototype.constructor)
console.log('james.__proto__.constructor-------------', james.__proto__.constructor)
- 如下图所示,我们可以发现对象原型 和 原型对象中都没有了constructor 属性,以至于 对象原型指向不再是 Person 而是 Object,这是因为直接使用 {} 赋值给 Person.prototype将原先的地址覆盖,导致 constructor丢失:
- 我们可以给其中直接添加 constructor来解决这一问题:
Person.prototype = {
constructor: Person,
loving: function() {
console.log('世界这么大,我还在搬砖!')
},
dream: function() {
console.log('不想一直搬砖!')
}
}
- 这时的原型指向便没有问题了,这也是 constructor 经常被使用的场景:
1.1.6 构造函数、实例、原型对象之间的关系
- 首先,每一个都早函数中都有一个原型对象,构造函数通过 prototype 属性指向 原型对象;
- 原型对象中有constructor属性,通过其指向 构造函数;
- 构造函数通过 new 关键字 产生 实例对象
- 实例对象 通过 对象原型 __proto__ 指向原型对象 prototype 从而指向 构造函数
二、原型指向问题
在 JavaScript 中,在对象实例和它的构造器之间建立一个链接——它是 __proto__属性,是从构造函数的prototype属性派生的——之后通过上溯原型链,在构造器中找到这些属性和方法。 __proto__ 的指向取决与对象创建时的实现方式。
2.1 字面量方式
let objA = {}
console.log(objA.__proto__)
console.log(objA.__proto__ === objA.constructor.prototype)
- 我们在这里通过字面量方式创建的对象 objA, objA的原型 __proto__ 指向 objA的构造器 constructor的原型 prototype
2.2 构造函数的方式
let ObjB = function () {}
let objC = new ObjB()
let objD = new ObjB()
console.dir(objC)
console.dir(objD)
console.dir(ObjB)
console.log(objC.__proto__ === objC.constructor.prototype)
console.log(objC.__proto__ === ObjB.prototype)
console.log(objC.__proto__ === objD.__proto__)
- objC的对象原型 与 objD的对象原型 相同,而且二者的对象原型也全等于 构造函数 objB 的原型对象,可见二者的原型对象指向均是构造函数的原型对象。(如下图)
2.3 Object.create()方式创建
let objE = {
name: '哈哈'
}
let objF = Object.create(objE)
console.dir(objF)
console.dir(objE)
console.log(objF.__proto__ === objF.constructor.prototype) // false
console.log(objF.__proto__ === objE) // true
- 在使用Object.create(xx)方法创建的对象时,被创建的对象原型直接指向 创建源头xx对象上,在该创建方式下 objF.__proto__ === objF.constructor.prototype* 是不成立的。(如下图)
三、原型链
- 万物皆对象,只要是对象,就有 __proto__ 原型,指向原型对象;
- james对象的原型指向 Person构造函数的原型对象prototype
- Person构造函数的原型对象prototype 即 Person.prototype也拥有__proto__原型 并且其指向 Object构造函数的原型对象
- Object 是官方声明的对象最顶层的 构造函数,即 Object.prototype 不再 拥有对象原型 __proto__ 因此 Object.prototype.__proto__ 指向 null, 到达最顶层
- 由此可知,对象根据对象原型__proto__ 一层层向上检索,寻找原型对象prototype,这样链式寻找的检索路径就被称为原型链。
- 下面,我们来打印一下相应的原型对象是否相等:
console.log('Person.prototype.__proto__------------', Person.prototype.__proto__)
console.log('Obeject.prototype------------', Object.prototype)
console.log(Object.prototype === Person.prototype.__proto__)
注意:this的指向问题
在构造函数中,this的指向是 实例化 的对象
在原型对象中,this指向的是调用者,谁调用指向谁
let james = new Person('James', 36, '篮球')
let kobe = new Person('kobe', 41, '篮球')
james.loving()
console.log(that)
kobe.loving()
console.log(that)
四、继承
继承说的就是: 子类继承付父类中的属性和方法,这样的话,子类的实例就可以使用父类的属性和方法了。
4.1 原型继承
原型继承: 我们现在已经知道的是,实例出来的对象,如果我们调用实例对象的某一属性或者方法,其自身是不存在的,它就会通过原型__proto__向上一层层寻找,直到找到或者到达最顶层null终止,也就是说,我们只要将父类的属性和方法置于子类的原型链上即可,这也是所谓的原型继承。 下面我们来看个实例:
function Parent(name, age, love) {
this.names = name;
this.age = age;
this.love = love;
}
function Child(sing) {
this.sing = sing;
}
Parent.prototype = {
constructor: Parent,
dream: function() {
console.log('我是父类')
}
}
let father = new Parent('父亲', '18', 'mother')
Child.prototype = father
Child.prototype.constructor = Child
Child.prototype.loving = function () {
console.log('我是子类')
}
let child = new Child('海阔天空')
let father2 = new Parent('父亲', '18', 'mother')
console.dir(useEx)
father.dream()
child.dream()
father.loving()
child.loving()
father2.loving()
实例分析:
- 首先有两个构造函数 父类Parent 和 子类 Child
- 接着给父类Parent增加方法 dream 并创建一个实例对象father
- 改变子类Child的原型对象指向,指向父类的实例对象father
- 子类Child的原型对像的构造函数指向不变,constructor指向Child
- 创建子类实例对象 child 和父类实例对象father2调用方法测试
我们创建的child实例对象,调用dream方法的时候,首先在自身检索又没有没该方法,没有则通过child.__proto__ 检索原型对象中有无该方法,(因为现在的原型对象prototype指向的是父类的实例对象father,所以是正在father中检索),如果father中还没有该方法,则继续通过child.__proto__.__proto__ 也就是 father.__proto__ 向上检索,检索Parent.prototype 原型对象,如果有,则调用,如果没有则继续向上检索Object.prototype直到最顶端
检索图示如下:
原型继承的特点:
- 基于__proto__原型链查找的,是将父类的属性和方法置于子类的原型链上的;
- 子类可以重写父类的方法;
- 父类中私有或者公有的属性和方法,都会变成子类中公有的属性和方法
4.2 call继承
call继承 就是把父类函数在子类函数中当成普通函数执行一次,并将this指向子类的实例。
function Parent(name, age, love) {
this.names = name;
this.age = age;
this.love = love;
}
Parent.prototype = {
constructor: Parent,
dream: function() {
console.log('我是父类')
}
}
function Child(sing) {
Parent.call(this, '飞翔', 20, '篮球')
this.sing = sing;
}
let child = new Child('垃圾')
console.log(child.names)
console.log(child.age)
console.log(child.love)
console.log(child.sing)
child.dream()
call继承的特点:
- 只能继承父类的私有属性和方法,函数执行相当于在子类中添加了几个属性和方法,和没有原型指向问题
- 父类私有的也是子类私有的
4.3 寄生组合继承
寄生组合继承 就是call继承 与 原型继承变体的结合,就是将父类私有属性和方法变成子类私有的,父类公有的属性和方法,变成子类公有的。关键点就是将原型继承中子类原型对象的指向父类实例对象,改变成 指向 空对象,而这个空对象的原型 指向父类的原型对象。使用Object.create(Parent.prototype) 实现。
function Parent(name, age, love) {
this.names = name;
this.age = age;
this.love = love;
}
Parent.prototype = {
constructor: Parent,
dream: function() {
console.log('我是父类')
}
}
function Child(sing) {
Parent.call(this, '飞翔', 20, '篮球')
this.sing = sing;
}
Child.prototype = Object.create(Parent.prototype)
Child.prototype.constructor = Child
let child = new Child('垃圾')
console.log(child.names)
console.log(child.age)
console.log(child.love)
console.log(child.sing)
child.dream()
寄生组合继承特点:
- 父类私有和公有的属性和方法分别继承给子类的私有和公有
- 子类可以修改父类公有属性和方法
具体继承检索如下图所示:
五、小结
至此,原型、原型链以及继承就要告一段落了,并不是所有的都涵盖了,比如继承方式中寄生继承、es6新增的class类继承等等,这里我们只要清楚,无论那种方式的继承,都是让子类实例对象能够拥有父类的属性和方法,从而避免多次定义,优化代码。至于原型链,那就更加简单了,无非就是原型的层层向上检索,不知道看完文章的你有哪些不清楚,可以留言,大家一起讨论。我是阿树,我们下期见!
六、声明
- 创作不易,转载请注明来源
- 创作不易,copy请留情
七、参考文献
- 黑马程序员pink老师相关视频