终极原型链与继承

213 阅读6分钟

原型链

对象和一系列的原型对象形成的一个链式结构,即称为原型链。

作用:决定了属性或方法的查找规则。

原型对象

  • 构造函数会有一个prototype属性,此属性指向着的就是原型对象,称为显示原型。主要用来添加共享的方法或属性
  • 通过构造函数new出来的新对象,会有一个__proto__属性,此属性指向的也是原型对象,称为隐式原型。主要用来实现继承
  • 原型对象中会自带一个constructor构造器属性,此值指向构造函数本身。

原型对象的作用:解决了堆内存空间浪费的问题

01-prototype.png

函数对象与实例对象

  • 实例对象:通过构造函数new出来的对象,称为实例对象
  • 函数对象:将函数作为对象使用时,称为函数对象
function Person(){}
var p = new Person() // p为实例对象

function fun(){}
fun.call() // fun为函数对象

终极原型链

不论是自定义的还是内置的构造函数都会有有原型对象,他们的原型都指向Object.prototype

  • 内置的String构造器String.prototype.__proto__指向Object.prototype

  • 自定义的Person构造器Person.prototype.__proto__也指向Object.prototype

  • Object.prototype.__proto__值为null,即到了原型链的终点

02-prototype.png

注意 :

内置的Math也可以叫做静态类,因为没有构造函数,不能用new操作。需要调用方法时只能通过:Math.方法名(),如Math.random()

Object构造函数是所有对象的基类(超类),为整个原型链的顶端

原型链中查找原则

查找标识符,即对象中的属性名或方法名

寻找原则:就近原则

  1. 先从自身内存空间寻找
  2. 若自身空间内没有,则通过自身的__proto__找其原型对象的内存空间
  3. 若原型对象内没有,则通过原型对象的__proto__找,一直找到Object.prototype

通过上述步骤寻找后,找到了就会立刻终止并返回结果。若最终都没有找到,寻找的若是属性返回undefined,若为方法则报错。

原型链中相关的API

  • hasOwnProperty():判断自身内存空间中是否存在某个属性

    function Person(name, age) {
          this.name = name
          this.age = age
       }
    Person.prototype.say = 'hello'
    var p = new Person('张三', 20)
    // hasOwnProperty():判断自身内存空间中是否存在某个属性
    console.log(p.hasOwnProperty('name')) // true
    console.log(p.hasOwnProperty('say')) // false
    
  • isPrototypeOf():判断某个对象是否是一个对象的原型

    console.log(Person.prototype.isPrototypeOf(p)) // true
    
    console.log(Function.prototype.isPrototypeOf(Person)) // true
    
    console.log(Object.prototype.isPrototypeOf(p)) // true
    
    console.log(Object.prototype.isPrototypeOf(Person)) // true
    
    console.log(Object.prototype.isPrototypeOf(Function)) // true 
    
  • instanceof:判断某个对象是否是在其原型链上的实例

    //   instanceof:判断某个对象是否是其原型链上的实例
    console.log(p instanceof Person) // true [研究的比较多]
    console.log(p instanceof Object) // true
    
    console.log(p instanceof Function) // false
    console.log(Person instanceof Function) // true
    console.log(Person instanceof Object) // true
    
  • for-in:用来循环对象自身空间的属性及原型中的属性

    // for-in:循环对象自身及原型对象中的自定义属性
    for (var k in p) {
        console.log(k) // name,age,say
    }
    
    // 只循环自身空间属性
    for (var key in p) {
        if (p.hasOwnProperty(key)) {
            console.log(key) // name,age
        }
    }
    

DOM节点和事件对象的继承链

  • DOM节点对象继承链: HTMLButtonElement -> HTMLElement -> Element -> Node -> EventTarget -> Object
  • 事件对象继承链: PointerEvent -> MouseEvent -> UIEvent -> Event -> Object

原型扩充方法

在需要添加的原型对象中封装一个方法,即可让对应的实例共享方法。

// 扩充一个数组共享的abc方法
Array.prototype.abc = function(){
    console.log('调用了abc方法')
}

// 也可以在基类对象中直接扩充让实例共享
Object.prototype.aaa = function(){
    console.log('调用了aaa方法')
}
var arr = []
arr.abc() // 调用了abc方法
arr.aaa() // 调用了aaa方法

检测对象的精确类型

Object.prototype.toString.call(),此方法来自Object.prototype原型对象中。

  • toString()方法返回一个对象的字符串形式。
  • 获取对象的精确类型(重点),call换成apply也行。
Object.prototype.toString.call([]) // '[object Array]'
Object.prototype.toString.call({}) // '[object Object]' 也称原始对象 plainObject
Object.prototype.toString.call(new Date()) // '[object Date]'
Object.prototype.toString.call(1) // '[object Number]'
Object.prototype.toString.call(true) // '[object Boolean]'
Object.prototype.toString.call('123') // '[object String]'
Object.prototype.toString.call(undefined) // '[object Undefined]'
Object.prototype.toString.call(null) // '[object Null]'

对象的继承

在javaScript中实现对象的继承常用的方法:

  • 通过伪造冒充this,伪造继承

  • 通过原型对象重写,原型继承

  • 组合继承(伪造+原型对象实现完整继承)

伪造继承

通过call和apply方法实现伪造继承,也称为冒充继承或构造函数继承

  • 通过call和apply,在子类的构造函数中,使用call或apply伪造父类构造函数中的this,把父类构造函数中的属性添加到子类对象的自身空间中

    function 子类(){
        父类.apply(this)
    }
    
    
    // 定义父类构造函数
    function Animal(name) {
        this.name = name
    }
    // 定义子类构造函数
    function Dog(name, age) {
        Animal.call(this, name)
        this.age = age
    }
    var dog = new Dog('旺财', 20)
    console.log(dog)
    

伪造继承的优缺点

  • 优点:可以将父类构造函数中的属性让子类对象访问使用
  • 缺点:无法继承父类原型中的方法

原型继承

通过父类原型对象重写子类原型对象,实现原型继承。重写即覆盖

// 重写子类原型,让子类原型继承父类原型中的属性和方法
子类.prototype = 父类.prototype 
// 定义父类构造函数
function Animal(name) {
    this.name = name
}
Animal.prototype.drink = function () {
    console.log('喝水')
}

// 定义子类构造函数
function Dog(name, age) {
    this.age = age
}

// 子类.prototype = 父类.prototype 
// 重写子类原型 目的:继承父类原型中的属性和方法
Dog.prototype = Animal.prototype
// 先重写子类原型,在添加方法
Dog.prototype.getName = function () {
    console.log('my name is ' + this.name)
}

var dog = new Dog('旺财', 20)
console.log(dog) // Dog {age: 20}
dog.getName() // 'my name is undefined'
dog.drink() // '喝水'

原型继承的优缺点

  • 优点:可以继承父类原型中的属性和方法
  • 缺点:无法将父类构造函数中的属性让子类对象访问使用

组合继承

核心思想:

  • 通过伪造继承,把父类构造函数中的属性加到子类对象的自身空间中
  • 通过原型继承,继承父类原型上的属性与方法

将两者结合形成互补

// 定义父类构造函数
function Animal(name) {
    this.name = name
}
​
Animal.prototype.drink = function () {
    console.log('喝水')
}
// 定义子类构造函数
function Dog(name, age) {
    Animal.call(this, name) // 伪造继承
    this.age = age
}
Dog.prototype = Animal.prototype // 原型继承// 重写原型之后,要保证constructor指向依然是原来的构造器
// 所以,需要修正子类原型的construtor属性
// 这样做的目的:主要是保证原型链的完整性!
Dog.prototype.constructor = Dog // 修复构造函数指向Dog.prototype.getName = function () {
    console.log('my name is ' + this.name)
}
​
var dog = new Dog('旺财', 20)
console.log(dog) // Dog {age: 20}
dog.getName() // 'my name is 旺财'
dog.drink() // '喝水'

组合继承的优缺点

  • 组合继承实现了子函数继承父函数自身的属性和父函数原型上的方法。

  • 但是存在父函数的原型与子函数原型指向相同引用地址的问题,即修改子函数的原型会导致父函数也更改。

    如:子函数原型重新绑定了constructor为子函数,父函数原型中的constructor也会被更改为子函数。

寄生组合继承

核心思想:让子函数原型与父函数原型不直接共用一个地址,而是修改子函数的原型链,将父函数原型添加到子函数的原型链中。

Son.prototpye.__proto__ === Dad.prototype

es5实现

利用一个绑定函数,让子函数的原型指向绑定函数的实例对象,再将绑定函数的原型指向父函数原型。

原理:函数的实例对象的__proto__指向函数的prototype

function Bound() {}
Son.prototype = new Bound()
Bound.prototype = Dad.prototypeSon.prototype.constructor = Sonconsole.log(Son.prototype.__proto__ === Dad.prototype) // true

Object.create()实现

Object.create()方法用于创建一个新对象,使用现有的对象来作为新创建对象的原型(prototype)

Son.prototype = Object.create(Dad.prototype)
Son.prototype.__proto__ = Dad.prototype
Son.prototype.constructor = Sonconsole.log(Son.prototype.__proto__ === Dad.prototype) // true

es6实现

es6中新增了class与extends,原理与es5中的绑定函数(源码)同理

class Dad {
    constructor(name){
        this.name = name
    }
    drink(){
        console.log(this.name + '在喝水')
    }
}
class Son extends Dad {
    constructor(name,age){
        super(name)
        this.age = age
    }
    eat(){
        console.log(this.name + '吃骨头')
    }
}
const s = new Son('son', 5)
console.log(s.constructor) // class Son
console.log(Son.prototype.__proto__ === Dad.prototype) // true
s.drink()
s.eat()