深入理解Js中的继承

1340.640.jpg

1.引言

明确一点:JavaScript并不是真正的面向对象语言,没有真正的类,所以我们也没有类继承

实现继承==有且仅有两种方式,call和原型链==

在介绍继承前我们先介绍下其他概念

2.函数的三种角色

一个函数,有三种角色。
当成普通函数,当成构造函数(类),当成对象

function Person (nickname) {
        var age = 15 //当普通函数使 私有属性
        this.age = 30    //当构造函数使 实例属性
    }
    Person.prototype.age = 50 //当构造函数使   原型属性
    Person.age =100  //当对象使 静态属性(类属性)  

举例:
Array.isArray是类上的方法
Array.push是Array原型上的方法
Array.toString是沿着原型链查找到的object类上的原型方法

3.继承的方式

继承原则:
使用call继承实例上的属性
使用原型链继承原型上的属性

3.1 组合继承

const Person = function (name) {
        this.name = name
    }
Person.prototype.introduce = function(){
  Object.entries(this).forEach((item)=>{
        console.log(`my ${item[0]} is ${item[1]}`)
    })
    
}

const Student = function (name,age) {
        Person.call(this,name)
        this.age = age
    }
Student.prototype = new Person()      //这里new了父类一次,增加了额外开销
Student.prototype.constructor =  Student         //这一句可以让student.constructor.name由Person变为Student 方便确认构造函数

let student = new Student('小明',15)
student.introduce()  继承父类原型方法的同时继承父类实例上的属性
//my name is 小明
//my age is 15      

组合继承有一个缺点,会额外new父类一次,增加了额外开销(想一想如果父类特别大这消耗会有多大)

3.2 Student.prototype = new Person() 做了什么

我们仔细研究一下这一句话,为什么它就能实现原型链继承

在上一篇文章中我们学过,实例能访问类上的原型

如果子类实例能访问父类的原型,那么我们是不是可以说子类继承了父类?

但是子类实例只能访问子类原型呀,所以可我可以让子类的原型等于父类的实例,因为父类的实例可以访问父类的原型,这就相当于子类实例可以访问父类原型了

这里你可能会问,为什么不直接这么写student.prototype = Person.prototype,这样子实例也可以访问父实例呀

**没错!单从访问上来说,你是对的。但是如果我后面先重写子类的原型,
比如我想写student.prototype = null,因为现在子类父类原型共用同一地址,父类也被改了,这个不符合我们的初衷 **

3.3 优化原理

还记得3.1说的原型链继承有个地方可以优化吗?在我们知道了3.2原型链继承的写法后,我们产生这样一个疑问,

  1. 真的要new一个对象,咱么实际只需要_Proto_来建立关系,能不能不复制父类的各种属性?
  2. 一定要new一个对象吗?实例和类原型的关系是通过_proto_来建立的,我手动设置这玩意,不new行不行?

这也是优化的两个方向

3.4 优化1

自动生成_proto_,改的是类的原型的指向
先说第二种优化,使用new自动生成_proto_,但是肯定不能直接new父类吧,我们new出一个空对象,然后改变这个类的原型指向我们需要继承的

比如我们需要继承obj
也就是能访问obj
实例能访问类的原型,让类的原型的地址指向Obj,实现继承
类为空类,减少开销

var create = function(obj){

var fn = funcion(){} //空类
fn.prototype = obj //改变类的原型的指向,指向要继承的对象
reurturn new fn() //自动生成_proto_

}

var A = create(B) //A能找到B(通过_proto_)

这个方法被es6实现了

Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。
const person = {
  isHuman: false,
  printIntroduction: function () {
    console.log(`My name is ${this.name}. Am I human? ${this.isHuman}`);
  }
};

const me = Object.create(person);

me.name = "Matthew"; // "name" is a property set on "me", but not on "person"
me.isHuman = true; // inherited properties can be overwritten

me.printIntroduction(); 
//My name is Matthew. Am I human? true

3.5 优化2

不用new,直接改_proto_
不就是让子类原型指向父类的原型吗

student.prototype 直接指向 Person.prototype 有问题,那咱就不让它直接指向了,让它间接指向

student.prototype指针不变,它找不到属性,会找它的_proto_吧,我让它的_proto_指向Person.prototype不就行了

也就是 student.prototype._proto_ = Person.prototype

3.6总结一下优化

所以对于这句话 Student.prototype = new Person() 的优化,
重心放在了怎么减少开销来建立联系上

我们可以既通过增加一个中间空对象(减少开销),来完成优化
Student.prototype = Object.create(Person.prototype)

也可以增加一个中间属性来完成优化
Student.prototype.__proto__ = Person.prototype

都能建立父类子类的联系

4.new干了啥

既然new在“类”的创建里面必须使用,那么我们就说一下new到底干了啥事情
题外话,new干了啥事,一定要从new完以后实例和类的关系来入手记忆,实例和类啥关系?两个关系实例是不是又类上面的实例属性,同时_proto_的指向关系
所以new 办了三件事
1.创建一个对象o继承构造函数
2.让构造函数的this变为o,并执行构造函数,将返回值设置为k
3.如果k是对象则返回对象,如果不是则返回o

//仿写new
function new1(func) {
        var o = Object.create(func.prototype)
        var k = func.apply(o,arguments[1])
        return typeof k === 'object'? k: o
    }
const x = new1(Student,['张三'])
x.name //'张三'
x.eat //'i am hungry,i want to eat!'

我们回过头再分析一下构造函数模式继承

const Person = function (name) {
        this.name = name
    }
const Students = function (name) {
        Person.call(this,name) //this是student实例
    }
const xm = new Students('小明')  //分析这里干了什么
console.log(xm)  //Students {name: "小明"}

1.让空对象o继承Students(o能访问Students的原型)
2.student执行,执行Person的代码,this是o,并且传入name, o.name='小明'返回的k是undefined
3.返回o,也就是返回{name:'小明'}

5.es6继承

class Person {
}
class Student extends person{
}

在babel es2015-loose模式下编译后的源码如下

"use strict";

    function _inheritsLoose(subClass, superClass) {
        subClass.prototype = Object.create(superClass.prototype);
        subClass.prototype.constructor = subClass;  //修正constructor,避免constructor判断的不对,也一般用不到
        subClass.__proto__ = superClass;  //这句话看不懂,感觉没啥用呀
    }

    var Person = function Person() {
    };

    var Student =
        /*#__PURE__*/
        function (_person) {
            _inheritsLoose(Student, _person);

            function Student() {
                return _person.apply(this, arguments) || this;
            }

            return Student;
        }(person);

严格模式下,高级单例模式返回一个Student, 可以看到Person的实例属性用的Person的构造函数+apply继承的
原型属性用的_inheritsLoose这个方法继承的
_inheritsLoose方法貌似就是我们之前说的寄生组合继承

6.继承的应用:vue数组变异方法的实现

我们知道vue里面的数组有变异方法,变异方法有啥功能呢,就拿push,pop来说,一方面数组会变,另外一方面有响应式(假设触发render方法)
思路:APO编程思想
数组之所以有push方法,是因为Array.prototype上有push方法我们需要实现自己的push方法,然后让响应式数据里面的数组的_proto_
指向我们的变异方法
原型链示意:

    Vue里面添加过监控的数组实例--->我们自己实现的变异方法
    `有一点需要明确,Array.proptryoe不能不修改`
    

const arrList = ['push','pop']
const render = ()=>{console.log('响应式,渲染视图')}
const proto = Object.create(Array.prototype) //为什么继承,因为我只对Push进行变异,其他的还是用的Array.proptryoe上的方法
arrList.forEach((method)=>{
    proto[method] = function(){
        render()
        Array.prototype[method].call(this,...arguments)
    }
})
var data = [1,2,3]
data.__proto__ = proto    //mvvm响应式原理,如果添加响应式的目标是数组,我就执行这个操作

data.push(4)   // 响应式,渲染视图,(data[1,2,3,4])
data.pop()   // 响应式,渲染视图,(data[1,2,3])
data.shift() // data [2,3]

这样就实现了对push pop的变异,利用原型链进行拦截,同时利用继承,使得其他方法还是用的Array.protorype上的

7.总结

本节详细介绍了继承的原理以及优化,对于es6的继承语法糖也做了剖析。同时介绍了一下mvvm下数组借用继承实现响应式的用法,由于本人水平有限,如果有什么不对的地方,欢迎留言指出。

阅读 325

推荐阅读
前端小羊羊
用户专栏

爱健身,爱编码,爱生活,欢迎小妹妹来撩我

209 人关注
32 篇文章
专栏主页