4

构造函数与实例对象

又是这个经典的问题,嗯,我先来写个构造函数,然后实例化一个对象看看。

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的原型链

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

Function
Function 构造函数 创建一个新的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)

看起来是完美解决了一切。但就是?

clipboard.png

实例化的对象实际上是用构造函数继承的方法往自己身上加属性从而覆盖原型链上的相应属性的,既然如此,为什么不直接那父构造器的原型加到子构造器的原型上呢?这样就不会出现那多余的父类实例化对象出来的属性了。

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)

clipboard.png

看起来很是完美,反正效果是达到了,性能优化也是最佳。但问题就是这样一点继承关系都看不出来啊,父类和子类的原型完全融合在一块了,一点都不严谨。

所以最优的继承方式应该是。。。

寄生组合继承

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


AwesomeHan
125 声望4 粉丝