JS中的经典继承实现方法与原型的深入探讨

852 阅读5分钟

原文链接

前言 🎤

JS在很久很久之前,其实是并没有类这种概念的,也没有明确的继承这个概念,所以聪明的人们就创造出了很多种办法,来模拟出和继承类似的效果,本文就来讲一下继承的原理和常见的继承方法(不包含Class语法糖)

简单的定义一下主角

function Tank(){
    this.speed = 10
}

Task.prototype.fire = function(){
    console.log('fire')
}

function Tiger(){
    this.armar = 10
}

Tiger.prototype.SpeedUp = function(){
    console.log('up!')
}

寄生组合式继承

function Tank(){
    this.speed = 10
}

Tank.prototype.fire = function(){
    console.log('fire')
}

function Tiger(){
    Tank.call(this) // 调用Tank构造函数中的内容
    this.armar = 10
}

(function(){
    let empty = function(){}
    empty.prototype = Tank.prototype
    //复制原型 并且传递给一个新的临时函数对象
    Tiger.prototype = new empty()
})()

//子元素的prototype内容需要在上面的IIFE之后调用
Tiger.prototype.speedUp = function(){
    console.log('up!')
}

let tank = new Tiger()
console.log(tank)
tank.fire()
tank.speedUp()
/*
Tank { speed: 10, armar: 10 }
fire
up!
*/

这个方法非常好,没有过多的空间浪费,没有多余的调用,一步到位堪称完美。
但是我们可以稍微升级一下

升级后的方法

function Tank(){
    this.speed = 10
}

Tank.prototype.fire = function(){
    console.log('fire')
}

// function Tiger(){
//     this.armar = 10
// }

function Tiger(){
    Tank.call(this)
    this.armar = 10
}

// modify
Tiger.prototype = Object.create(Tank.prototype)
Tiger.prototype.constructor = Tiger

Tiger.prototype.speedUp = function(){
    console.log('up!')
}

let tank = new Tiger()
console.log(tank)
tank.fire()
tank.speedUp()

改动非常少,但是更加简洁了,而且更加的清晰易懂。其实原理是一样的,通过原型函数创建新的对象,然后将其作为基础进行修改。同时修改构造函数为子类。

Object.create

什么是Object.create?它是一个处于ES5规范中的,用于通过原型创建对象的函数。也就是说,新对象的__proto__将会指向传入的参数原型。
但是,为了更加升入的理解,所以我们需要实现一个Object.create来加深理解程度。

Object.breate = function(p){
    if(typeof p != 'object' && typeof p != 'function'){
        throw new Error("....!%$") //加密语言
    }
    function C(){}
    C.prototype = p
    return new C()
}

哈哈,是不是似成相识?效果其实是完全一样的。只不过要注意一点⚠️Object.create的参数可以传递两个,而并非只能一个,你可以去MDN上详细阅读它的使用方法。这里不进行展开。
有人可能认为xx.prototype = new YY()是错误的方法,而只有Object.create才是唯一的正确答案。其实这种想法非常过激,我们对错误的定义有时候太过于狭隘。如果它能得到正确的答案,它的运行状态正常而且稳定,它的运行结果并无任何偏差,那么我们怎么能说他是一种错误的方式?

Prototype

其实在阅读上面的代码的时候,你一定会产生很多疑问🤔️,什么是prototype?,为什么可以传入一个对象作为prototype?为什么函数可以直接new?等等之类的问题。接下来,我将讲述我的见解。

在我研究JS的时候,我就一直在思考这些问题,随着我的不断深入,我感觉我对prototype的形象越发的了解,虽然我现在并不能100%的肯定,我理解的prototype就一定是正确的,但是我觉得八九不离十。

Prototype是一个实例对象

  • prototype可以被对象随意覆盖
  • 可以随时修改prototype的属性
  • prototype中的修改会影响到所有继承当前prototype的对象。
  • prototype可以当成一个对象来进行操作

但是!!请注意,你在对原型赋值的时候,并不是在你想的prototype进行赋值。

这里其实有个非常非常神奇的特性。

Prototype ?= Prototype

请注意⚠️,构造对象,也就是Function,在搞乱我们对原型链对理解这件事上面,占有很大一笔责任。
一个普通对构造对象,它同时有着两个属性,__proto__表示继承的上一个原型,prototype表示当前构造函数中的原型。 而当他被构造后,会产生新的情况。最重要的是构造函数中的prototype会转移到实例对象的__proto__之中
因此,如果你直接赋值一个构造好的实例到子类的prototype时,一切都恰恰刚好。
而如果你在父类对象的构造函数中赋值了实例属性,那么这些属性也会成为子类prototype的一部分。

//伪代码
//构造对象结构
let A = {
    prototype:{
        __proto__:{x:1},
        y:2,
        constructor:function(){}
    }
}
//实例对象结构
let newA = new A()
newA == {
    __proto__:{
        __proto__:{x:1},
        y:2,
        constructor:function(){}
    }
}
//继承了A的B
let B = {
    prototype:{
        __proto__:{
            __proto__:{x:1},
            y:2,
            constructor:function(){}
        }
    },
}
//实例化B
let newB = new B()
newB == {
    __proto__:{
        __proto__:{
            __proto__:{x:1},
            y:2,
            constructor:function(){}
        }
    }
}

prototype的谜题解开了。

Prototype结论

经过多次研究发现,实际上的prototype机制异常的简单。重点就两个。

  • 实例对象中不存在prototype
  • 在构造对象中,如果这个对象被构造,那么构造对象的prototype将会变成实例对象的__proto__
function A(){}
let a = new A()
console.log(a.__proto__ === A.prototype)

还记得我们一开始是怎么实现继承的吗?

    let empty = function(){}
    empty.prototype = Tank.prototype
    //复制原型 并且传递给一个新的临时函数对象
    Tiger.prototype = new empty()

结合一张图片看看 {% img /images/JS中的经典继承实现方法-2020-11-12-18-17-11.png %}
现在明白为什么需要创建一个新函数了吗?

不过,其实这种继承有着少量的缺点,下面看一看Babel是如何实现继承的。

Babel

function _inherits(subClass, superClass) {
    // 喜闻乐见的Object.create
    subClass.prototype = Object.create(superClass.prototype, {
        //第二个参数的说明详细见MDN,这里的作用类似于 subClass.prototype.constructor = subClass
        constructor: {
            value: subClass,
            enumerable: false,
            writable: true,
            configurable: true;
        }
    })
    if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; 
    // 设置subClass中__proto__属性,将其设置为superClass的构造函数
    // 这个举动的作用是将赋值于构造函数上的方法进行继承,在es6中是class{static xx(){}}中的static
}

function A(){}
function B(){}
A.StaticMethod = ()=>{} // 可以认为是静态方法
A.prototype.hello = ()=>{} // 可以认为是对象实例化后的方法

一个小例子

function A(){}
function B(){}
B.C = function(){
    console.log('static')
}
console.log(A instanceof B)
_inherits(A,B)
let a = new A()
console.log(a instanceof B)
A.C()
/*
false
true
static
*/

这里其实也反应了JS原型链的机制,当你在一个对象上面调用方法时,无论是实例对象还是构造对象,JS都会从其__proto__中查找方法,如果找不到就在__proto__.__proto__中继续查找,直到为null

结语 👨‍🏫

prototype真的是js中非常重要的一个部分,我觉得每个前端开发者都需要好好理解掌握prototype中的每一个细节。