原型链和继承

188 阅读7分钟

私有属性和方法:仅仅函数内使用

公有属性和方法:函数可以使用(push、shift)

静态属性和方法:定义在构造函数上的属性和方法,不用实例调用(Promise.all()、Promise.race()、Object.assign()、Array.from())

构造函数(ES6之前定义)

很好区分:

在函数内用var定义的就是私有的;

在函数内用this承接的,或者设置在构造函数原型对象上比如Person.prototype.xxx就是公有;

原型对象的函数和变量不会在实例对象上,但是实例对象可以访问和调用它们。

区分定义在实例对象和原型对象上的属性和方法

for...in...:自身和原型

Object.keys():自身

Object.getOwnPropertyNames():自身

原型链查找:当访问一个对象的属性/方法时,它不仅仅在该对象上查找,还会查找该对象的原型,以及该对象的原型的原型,一层一层向上查找,直到找到一个名字匹配的属性 / 方法或到达原型链的末尾(null)。

class

  1. class基本概念
  • 当使用class的时候,它会默认调用constructor这个函数,来接收一些参数,并构造出一个新的实例对象(this)并将它返回。
  • 如果没有定义constructor,会隐式生成构造函数;
  1. class中几种定义属性的区别::
  • 在constructor中var一个变量,它只存在于constructor这个构造函数中
  • 在constructor中使用this定义的属性和方法会被定义到实例上
  • 在class中使用=来定义一个属性和方法,效果与第二点相同,会被定义到实例上
  • 在class中直接定义一个方法,会被添加到原型对象prototype上
  • 在class中使用了static修饰符定义的属性和方法被认为是静态的,被添加到类本身,不会添加到实例上
  1. other:
  • class本质虽然是个函数,但是并不会像函数一样提升至作用域最顶层
  • 如遇class中箭头函数等题目请参照构造函数来处理

在构造函数中如果使用了箭头函数的话,this指向的就是这个实例对象。

class Cat {  
    constructor () {    
        this.name = 'guaiguai'    
        var type = 'constructor'  
    } 
    type = 'class'  
    getType = () => {
        console.log(this.type)    
        console.log(type)  
    }
}
var type = 'window'
var guaiguai = new Cat()
guaiguai.getType()
console.log(guaiguai)

转化为构造函数


function Cat () {  
    this.type = 'class'  
    this.getType = () => {    
        console.log(this.type)  //this指向的是这个实例对象,所以输出class  
        console.log(type) 
    }}
    Cat.prototype.constructor = function () {  
    this.name = 'guaiguai' 
    var type = 'constructor'
}
var type = 'window'
var guaiguai = new Cat()
guaiguai.constructor()
guaiguai.getType()
console.log(guaiguai)

'class'
'window'
Cat {type: "class", name: "guaiguai", getType: ƒ}
  • 使用class生成的实例对象,也会有沿着原型链查找的功能

下面正式谈论继承

对象封装、继承、多态

原型链

继承

原型链继承

Son.prototype = new Person()

优点:继承了父类的模板和父类的原型对象

缺点:
- 如果要给子类的原型上新增属性和方法,就必须放在Child.prototype = new Parent()这样的语句后面
- 无法实现多继承(因为已经指定了原型对象了)
- 来自原型对象的所有属性都被共享了,这样如果不小心修改了原型对象中的引用类型属性,那么所有子类创建的实例对象都会受到影响(这点从修改child1.colors可以看出来)
- 创建子类时,无法向父类构造函数传参数(这点从child1.name可以看出来)




构造继承

Person.call(this, arg)

function Person(){
    
}
function Son(){
    
}

优点:

解决了原型链继承中子类实例共享父类引用对象的问题,实现多继承,创建子类实例时,可以向父类传递参数(见题目3.3)

缺点:

  • 构造继承只能继承父类的实例属性和方法,不能继承父类原型的属性和方法(见题目3.4)
  • 实例并不是父类的实例,只是子类的实例(见题目3.5)
  • 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能

组合继承

Person.call(this, arg)

Son.prototype = new Person()

实现方式:

  • 使用原型链继承来保证子类能继承到父类原型中的属性和方法
  • 使用构造继承来保证子类能继承到父类的实例属性和方法

优点:

  • 可以继承父类实例属性和方法,也能够继承父类原型属性和方法
  • 弥补了原型链继承中引用属性共享的问题
  • 可传参,可复用

缺点:

  • 使用组合继承时,父类构造函数会被调用两次
  • 并且生成了两个实例,子类实例中的属性和方法会覆盖子类原型(父类实例)上的属性和方法,所以增加了不必要的内存。

constructor总结:

  • constructor它是构造函数原型对象中的一个属性,正常情况下它指向的是原型对象。
  • 它并不会影响任何JS内部属性,只是用来标示一下某个实例是由哪个构造函数产生的而已。
  • 如果我们使用了原型链继承或者组合继承无意间修改了constructor的指向,那么出于编程习惯,我们最好将它修改为正确的构造函数。

寄生组合继承

Child.prototype = Object.create(Parent.prototype) Child.prototype.constructor = Child

有没有一种办法直接跳过父类实例上的属性,直接继承父类实例上的属性?也就是说我们需要一干净的实例对象,作为子类的原型。

干净就想到Object.create(proto, propertiesObject)

  • 参数一,需要指定的原型对象(作用:指定你需要创建的这个对象的原型对象是谁)
  • 参数二,可选参数,给新对象自身添加新属性以及描述器

怎么说呢?

就好比,我们使用var parent1 = new Parent()创建了一个对象parent1,那parent1.__proto__就是Parent.prototype。
使用var obj = new Object()创建了一个对象obj,那obj.__proto__就是Object.prototype。
而这个Object.create()屌了,它现在能指定你新建对象的__proto__。

这正不是我们想要的吗?我们现在只想要一个干净并且能链接到父类原型链上的对象。 Child.prototype = Object.create(Parent.prototype) ,这样就只会创建一份父类属性,调用一次父类构造函数。

寄生组合继承算是ES6之前一种比较完美的继承方式吧。

它避免了组合继承中调用两次父类构造函数,初始化两次实例属性的缺点。

所以它拥有了上述所有继承方式的优点:

  • 只调用了一次父类构造函数,只创建了一份父类属性
  • 子类可以用到父类原型链上的属性和方法
  • 能够正常的使用instanceOf和isPrototypeOf方法

class继承

class继承的效果和之前我们介绍过的寄生组合继承方式一样。

关键内容:extends和super

class Child extends Parent {}

// 等同于
class Child extends Parent {
    constructor (...args) {
        super(...args)
    }
}

ES6中的继承:

  • 主要是依赖extends关键字来实现继承,且继承的效果类似于寄生组合继承
  • 使用了extends实现继承不一定要constructor和super,因为没有的话会默认产生并调用它们
  • extends后面接着的目标不一定是class,只要是个有prototype属性的函数就可以了

super相关:

  • 在实现继承时,如果子类中有constructor函数,必须得在constructor中调用一下super函数(不能少super),因为它就是用来产生实例this的。
  • super有两种调用方式:当成函数调用和当成对象来调用

函数调用:

  • super当成函数调用时,代表父类的构造函数,但返回的是子类的实例,也就是此时super内部的this指向子类。在子类的constructor中super()就相当于是Parent.constructor.call(this)。
  • 子类constructor中如果要使用this的话就必须放到super()之后,super当成函数调用时只能在子类的construtor中使用。

super当成对象调用:

  • 普通函数(constructor函数、原型对象上函数、实例对象上函数)中super对象指向父类的原型对象
  • 静态函数中super对象指向父类
  • 且通过super调用父类的方法时,super会绑定子类的this,就相当于是Parent.prototype.fn.call(this)。
class Parent {
  constructor (name) {
    this.name = name
  }
  getName () {
    console.log(this.name)
  }
}
Parent.prototype.getSex = function () { 
	console.log('boy')
}
Parent.getColors = function () {
  console.log(['white'])
}
class Child extends Parent {
  constructor (name) {
    super(name)
    super.getName()
  }
  instanceFn () {
    super.getSex()  //super代表Parent.prototype,也就是父类原型对象
  }
  static staticFn () {
    super.getColors() //super代表Parent,也就是父类对象
  }
}
var child1 = new Child('child1')
child1.instanceFn()
Child.staticFn()
console.log(child1)


'child1'
'boy'
['white']
Child{ name: 'child1' }

new.target: 指向当前正在执行的那个函数,你可以理解为new后面的那个函数

ES5继承和ES6继承的区别:

  • 在ES5中的继承(例如构造继承、寄生组合继承) ,实质上是先创造子类的实例对象this,然后再将父类的属性和方法添加到this上(使用的是Parent.call(this))。
  • 而在ES6中却不是这样的,它实质是先创造父类的实例对象this(也就是使用super()),然后再用子类的构造函数去修改this

juejin.cn/post/684490…

juejin.cn/post/684490…