关于构造函数、原型、原型链、多种方式继承

38次阅读

共计 4015 个字符,预计需要花费 11 分钟才能阅读完成。

构造函数与实例对象
又是这个经典的问题,嗯,我先来写个构造函数,然后实例化一个对象看看。
function Person(name) {
this.name = name
}
Person.prototype.eat = () => {console.log(‘eat’)}
Person.prototype.play = () => {console.log(‘play’)}
let Han = new Person(‘Han’)
通过一系列打印发现了这样的关系:
原型链 — 原型(prototype)和隐式原型(__proto__)
可以看出实例对象没有 prototype(也就是原型),只有构造器才拥有原型。而所有的 js 对象都拥有__proto__(也就是隐式原型),这个隐式原型所指向的就是创造这个对象的构造器的原型。如实例 Han 的隐式原型指向了其构造函数(Person)的原型;Person 的隐式原型指向了 Function 的原型;而原型自身也有隐式原型,指向了 Object 的原型。
有点绕口,其实就是通过隐式原型可以向上找到是谁构造了自己,并且如果自己没有相应的属性或者方法,可以沿着这条原型链向上找到最近的一个属性或方法来调用。如 Han.eat(),实际上是调用了 Han.__proto__.eat(),把构造器 Person 的原型的 eat 方法给拿来用了;再如 Han.hasOwnProperty(‘name’), 实际上是调用了 Han.__proto__.__proto__.hasOwnProperty(‘name’), 因为 Han 自己没 hasOwnProperty 这方法,就通过隐式原型向上找到了 Person 的原型,发现也没这方法,就只能再沿着 Person 的原型的隐式原型向上找到了 Object 的原型,嗯然后发现有这方法就拿来调用了。
构造器 constructor
所有构造函数都有自己的原型(prototype),而原型一定有 constructor 这么个属性,指向构造函数本身。也就是告诉大家这个原型是属于本构造函数的。
Function & Object
可以看出 Person 这个构造函数是由 Function 创建出来的,而我们看下 Function 的隐式原型,竟然是指向了 Function 的原型,也就是 Function 也是由 Function 创建出来的。很绕是不是,我们先不管,继续溯源下去,再看下 Function 的原型的隐式原型,指向的是 Object 的原型,继续往上找 Object 的原型的隐式原型,嗯终于结束了找到的是 null,也就是 Object 的原型是原型链上的最后一个元素了。
接下来看下 Object,Object 是由 Function 创建出来的,而 Function 的隐式原型的隐式原型是 Object 的原型也就是 Function 通过原型链可以向上找到 Object 的原型,两者看起来是你生我我生你的关系,这里也就引用比较好懂的文章来解释下:从 Object 和 Function 说说 JS 的原型链

ObjectJavaScript 中的所有对象都来自 Object;所有对象从 Object.prototype 继承方法和属性,尽管它们可能被覆盖。例如,其他构造函数的原型将覆盖 constructor 属性并提供自己的 toString() 方法。Object 原型对象的更改将传播到所有对象,除非受到这些更改的属性和方法将沿原型链进一步覆盖。FunctionFunction 构造函数 创建一个新的 Function 对象。在 JavaScript 中, 每个函数实际上都是一个 Function 对象。
—- 来自 mozilla

接下来说下构造函数实例化对象到底做了些啥,其实看也能看出来了。
let Jan = {}
Person.call(Jan, ‘Jan’)
Jan.__proto__ = Person.prototype
1、创建一个空对象。2、将构造函数的执行对象 this 赋给这个空对象并且执行。3、把对象的隐式原型指向构造函数的原型。4、返回这个对象
是的就是这样,next page!
继承
原型链继承
function Person(name) {
this.name = name
this.skills = [‘eat’, ‘sleep’]
}
Person.prototype.say = ()=> {console.log(‘hi’)}

function Boss() {}
Boss.prototype = new Person()
let Han = new Boss()
原理就是这样???? 子构造函数的原型指向了父构造函数的实例对象,因此子构造函数的实例对象可以通过原型链找到父构造函数的原型方法和类属性。
优点:所有实例对象都可以共享父构造函数的原型方法。
缺点:1、父构造函数的引用属性也被共享了,相当于所有的实例对象只要对自身的 skills 属性进行修改都会引发共振,因为其实修改的是原型链上的 skills 属性。当然对 skills 重新赋值可以摆脱这一枷锁,相当于自身新建了 skills 属性来覆盖了原型链上的。2、实例化时无法对父构造函数传参。3、子构造函数原型中的 constructor 不再是子类自身,而是通过原型链找到了父类的 constructor。
构造函数继承
function Person(name) {
this.name = name
this.skills = [‘eat’, ‘sleep’]
}
Person.prototype.say = ()=> {console.log(‘hi’)}

function Boss(name) {
Person.call(this, name)
}
let Han = new Boss(‘Han’)
原理就是父构造函数把执行对象赋给子构造函数的实例对象后执行自身。
优点:1、实例化时可以对父构造函数传参。2、父类的引用属性不会被共享。3、子构造函数原型中的 constructor 还是自身,原型没有被修改。
缺点:每次实例化都执行了一次父构造函数,子类不能继承父类的原型,如果把父类原型上的方法写在父类的构造函数里,虽然子类实例对象可以调用父类的方法,但父类的方法是单独加在每个实例对象上,会造成性能的浪费。
组合继承
结合了原型链继承和构造函数继承两种方法。
function Person(name) {
this.name = name
this.skills = [‘eat’, ‘sleep’]
}
Person.prototype.say = ()=> {console.log(‘hi’)}

function Boss(name, age) {
Person.call(this, name)
this.age = age
}

Boss.prototype = new Person()
Boss.prototype.constructor = Boss
Boss.prototype.sleep = ()=> {console.log(‘sleep’)}

let Han = new Boss(‘Han’, 18)
看起来是完美解决了一切。但就是????

实例化的对象实际上是用构造函数继承的方法往自己身上加属性从而覆盖原型链上的相应属性的,既然如此,为什么不直接那父构造器的原型加到子构造器的原型上呢?这样就不会出现那多余的父类实例化对象出来的属性了。
function Person(name) {
this.name = name
this.skills = [‘eat’, ‘sleep’]
}
Person.prototype.say = ()=> {console.log(‘hi’)}

function Boss(name, age) {
Person.call(this, name)
this.age = age
}

Boss.prototype = Person.prototype //modified
Boss.prototype.constructor = Boss
Boss.prototype.sleep = ()=> {console.log(‘sleep’)}

let Han = new Boss(‘Han’, 18)

看起来很是完美,反正效果是达到了,性能优化也是最佳。但问题就是这样一点继承关系都看不出来啊,父类和子类的原型完全融合在一块了,一点都不严谨。
所以最优的继承方式应该是。。。
寄生组合继承
function Person(name) {
this.name = name
this.skills = [‘eat’, ‘sleep’]
}
Person.prototype.say = ()=> {console.log(‘hi’)}

function Boss(name, age) {
Person.call(this, name)
this.age = age
}
Boss.prototype = Object.create(Person.prototype)
Boss.prototype.sleep = ()=> {console.log(‘sleep’)}
Boss.prototype.constructor = Boss
let Han = new Boss(‘Han’, 18)
先看图???? 其实跟组合继承有点像,构造函数继承部分和组合继承的一样就不说了。原型链那块和原型链继承有所不同的是原型链继承是直接拿了父类的实例对象来作为子类的原型,而这里是用以父类的原型为原型的构造函数实例化出来的对象作为子类的原型(Object.create 做的事情),完美避开了不必要的父类构造函数里的东西。
Object.create() 方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。
相当于这样????
function create(proto) {
function F() {}
F.prototype = proto
return new F()
}

听说 ES6 的 class extend 也是这么做的,更多的继承细节可以看看这篇文章,本继承章节也参考了的???? 一篇文章理解 JS 继承——原型链 / 构造函数 / 组合 / 原型式 / 寄生式 / 寄生组合 /Class extends

正文完
 0