白话浅谈javaScript的继承方式(7种)

前言:

        本文旨在帮助小白理解当前js各种继承方式,内容采取口语式描述,需要干货的童鞋请自己提炼内容。

        本文以《js高级程序设计》为主体,整理了一些大佬的观点,以自己的表达方式呈现给大家,表述不当时请指教。

基础知识:

首先我们得了解一下构造函数的一些基本知识:prototypenew操作符

如果你对prototype原型及原型链的知识不太了解,可先查看《js原型和原型链》学习

直接先来一个构造函数 :

function Parent(name,age){
    this.name = name
    this.age = age
}

生成一个构造函数实例需要用到new操作符,我们看看new操作符都干了什么?

//生成一个实例 p1
let p1 = new Parent("张三",18)

以上例子中,new 操作符主要做了三步:

        1.以构造函数的prototype属性为原型,创建新对象,将它的引用赋给this

//可以这么理解
let this = {}
this.__proto__ = Parent.prototype
 
//以上就是为啥 p1.__proto__ === Parent.prototype 的原因

        2. 将上面的 this 和 调用参数 传给构造器,执行

this.name = name
this.age = age

        3. 最后返回 this 指向的新对象,也就是实例

return this

有点晕是不是,全部步骤放在一起看一下:

    //第一步
    let this = {} 
    this.__proto__ = Parent.prototype 
 
    //第二步
    this.name = name
    this.age = age

    //第三步
    return this    
 
// 也就是说,构造函数在new调用时,第一步和第三步是隐式的
// 看似没有却偷偷执行了!

由于this是个普通对象,那return给了p1肯定也是个普通对象,p1实例里面都有些啥?

console.log(p1)

// { 
//    name:"张三",
//    age:18, 
//    __proto__ : { 这个对象就是Parent.prototype,参考上面讲过的内容}
//  }

继承:

        转入正题,继承就是把父类的属性和方法提供给子类使用,讲究分享。js创作者们希望借用原型和原型链的思想去实现js独特的继承方式,于是最原始的原型链继承就诞生了。

        1.原型链继承

        参考原型和原型链的原理,如果我们将父类的实例作为子类的原型,那么属性和方法就能一直传递下去,上核心代码:

function Parent(name){
    this.name = name
    this.cards = ["身份证","社保卡"]
}
//给构造函数原型添加属性和方法
Parent.prototype.age = 35
Parent.prototype.getAge = function(){
    console.log("父类age:" + this.age)
}


function Child(){
    this.name = "子类"
}

Child.prototype = new Parent(); //这一步是关键

var c1 = new Child();
console.log(c1.age) //35
console.log( c1.getAge() ) //父类age:35

 根据new操作和原型链的原理,子实例可以继承到:

        1.自身构造函数的属性和方法

        2.父类构造函数的属性和方法

        3.父类原型的属性和方法

但是这种继承方法有很多的缺点:

        1.由于new Parent()是单独赋给某个子类原型的,所以继承单一,只能继承一个父类

        2.由于new Parent()这一步拿不到参数,所以新实例无法向父类构造函数传参

        3.由于原型属性类似于浅拷贝,子类所有新实例都会共享父类实例的属性和方法,如果理解不了这个缺点,那么看看下面这段代码:

var c1 = new Child();
var c2 = new Child();

c1.cards.push("驾驶证");

c1.cards //["身份证","社保卡","驾驶证"] 
c2.cards //["身份证","社保卡","驾驶证"] 被c1影响

本来我是想给c1实例的cards数组添加一个 "驾驶证" ,c2实例却跟着变了,这是为啥子呢?因为呐,那个crads属性是一个引用类型(数组对象),子类都是用的这个引用类型的地址而非独立拥有,所以修改了大家都会跟着变。

那咋个办呢?盗用构造函数继承来帮你解决

2.盗用构造函数继承

又称借用构造函数继承。

刚才说原型属性是浅拷贝,那我们不走原型,直接把父类构造函数的this指针改了,把属性和方法直接弄到子类构造函数不就行了!没错,这时候我们需要借用call()或者apply()来实现修改指针。同样的,不懂就看《啥子是call和apply》

少废话,上核心代码:

function Parent(name){
    this.name = name
    this.cards = ["身份证","社保卡"]
    this.getCards = function(){
        console.log(this.cards)
    }
}

function Child(name){
    Parent.call(this,name)  //核心步骤
    this.address = "地址"
}

var c1 = new Child("张三");
var c2 = new Child("张三");

c1.cards.push("驾驶证");

c1.cards //["身份证","社保卡","驾驶证"]  属性私有不共享了
c2.cards //["身份证","社保卡"]   

细心的童鞋已经发现,原型链继承的三个缺点已经被弥补啦!

        1.可以通过多次call()的方式继承多个父类构造函数

        2.由于call方法可以传参,所以子实例可以向父类传参

        3. 新实例引入构造函数属性是私有的,不共享

看了上面的优点,是不是觉得盗用构造函数继承更优秀,No!新的问题来了:

        1.由于没有new父类构造函数,所以只能继承父类构造函数的属性,没有继承父类原型的属性,而且属性和方法只能在构造函数上定义

        2.既然属性私有了,那方法也会私有,每个子类都会有独立的一份,无法实现函数的复用

等等!无法函数复用?子类不都能调用到父类的函数吗?啥是复用你把我弄糊涂了。

原型链继承上的属性和方法都是共享的,所以一个函数方法会被大家共享,而盗用构造函数继承的方法是私有的,同一个方法会有多份存储,占用更多内存空间。上代码看:

        

function Parent(name){
    this.name = name || "张三"
    this.cards = ["身份证","社保卡"]
    this.getCards = function(){
        console.log(this.cards)
    }
}

this.prototype.getName = function(){}

//先看看原型链继承
function Child(){

}
Child.prototype = new Parent();
var c1 = new Child();
var c2 = new Child();

c1.getName === c2.getName //true


//再看看盗用构造函数继承
function Child(name){
    Parent.call(this,name)
}

var c1 = new Child();
var c2 = new Child();

c1.getCards === c2.getCards //false

明明是同一个方法getCards,却要另起炉灶弄一个新的来执行,看看原型链继承,大家始终都是用的同一个getName,这就是函数复用。

所以说,盗用构造函数继承也不是最优方式!

既然原型链继承和盗用构造函数继承优缺点互补,那能不能组合使用,双排上分?

当然可以咯!

3.组合继承

就是把原型链继承和盗用构造函数继承组合来使用,上菜:

function Parent(name){
    this.name = name || "张三"
    this.cards = ["身份证","社保卡"]
    this.getCards = function(){
        console.log(this.cards)
    }
}

this.prototype.getName = function(){}


function Child(name){
    Parent.call(this,name)
    this.address = "地址"
}

Child.prototype = new Parent(); 

var c1 = new Child('张三');

把属性放在构造函数里,把方法写在原型上,一切问题都木有了。

好多优点啊:可以继承原型上的属性方法、可以传参、可以函数复用、属性也可以私有

是不是觉得很完美?可惜了,还是有缺点。纳尼!!!你给我说清楚啥问题,不然我要捶你哈。

function Child(name){
    Parent.call(this,name) //第二次调用Parent()
    this.address = "地址"
}

Child.prototype = new Parent(); // 第一次调用 Parent()

执行了两次父类构造函数,第一次就把父类构造函数内的属性和方法给了子类,第二次又给,结果就是子类构造函数内部有一份,原型上有一份。一模一样,白白浪费了内存(耗内存)。因为在查找属性时,构造函数自身优先,原型上那份永远都用不上。

还有

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值