面向对象之继承

本文首发于个人博客:www.wyb.plus

1. 原型链深入分析

构造函数、原型和实例的关系 >>>

每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针

原型链的概念 >>>

一个实例的原型可以是另一个构造函数的实例, 这个可以无限嵌套,这就是所谓原型链的基本概念.

function GrandFather() {
 this.age = 99;
}
GrandFather.prototype.sex = "男";

function Father() {
 this.name = "Father";
}
Father.prototype = new GrandFather();
Father.prototype.height = 180;
let son = new Father()

在这里插入图片描述

注意:不是Father的原型的constructor 属性被重写了,而是Father的原型指向了另一个对象---->GrandFather的原型,而这个原型对象的constructor 属性指向的是GrandFather 。

究极版原型链 >>>

在这里插入图片描述

2. 经典继承(伪造对象)

函数只不过是在特定环境中执行代码的对象!!!

这句话是我们本节的核心哲学

2.1 经典继承之call

当我们在一个全局环境中已经创造了若干的对象及其对应方法时, 有些时候需要实现一个方法借用的功能 :

例 : 对象o1有一个方法a是输出对象的name值, 而对象o2没有该方法, 那么如果我们也想输出o2的值的话, 那么我们就可以把对象o1的值借过来用用

let o1 = {
         name: "wangyubo",
         age: 18,
         add: function(num1, num2) {
             console.log(num1 + num2);
         }
     }
     let o2 = {
         name: "wyb",
         age: 28,
         sayAge: function() {
             console.log(this.age);
         }
     }

在这里插入图片描述

  • o1本来没有sayAge的方法 , 从o2借过来之后 , 它就有了 , 并且输出了o1中的age
  • o2本来没有add的方法 , 从o1借过来之后 ,它也有了 , 并且按照add方法传参之后输出了结果

call的语法

fn.call( {} ,参数1,参数2)
// .call左侧是要借用的那个函数
// .call右侧括号中第一个参数是借用此方法的对象 , 后面是该函需要的参数
o1.add.call(o2,10,20) 等价于==> o2.function(num1,num2){
	 console.log(num1 + num2);
}

参数是根据该函数需要的参数一一写入 , 可以是任意多个 , 但是只能一个一个写入

2.2 call的经典应用

在数组一章我们学到了很多的数组方法 , 但是我们日常应用中最常见的却是由DOM节点组成的类数组 , 而类数组中的方法极少,使用时极不方便 , 所以我们便可以用call来借用数组的方法给类数组使用

<body>
 <ul>
     <li></li>
     <li></li>
     <li></li>
     <li></li>
     <li></li>
 </ul>
 <script>
     let aLi = document.getElementsByTagName("li")
     Array.prototype.forEach.call(aLi, function(item, index, array) {
         item.style.cssText = "width:50px;height:50px;margin-bottom:10px;background-color:#bfc"
     })
 </script>
</body>

在这里插入图片描述

2.3 经典继承之apply

语法 >>>

fn.apply( {},[参数1,参数2])
  • apply方法和call在功能上并没有什么不同
  • 唯一的别就是apply方法的参数都放在一个数组里面 , 这个数组可以是一个类数组或是标准数组

仍然用上面的例子

 let o1 = {
            name: "wangyubo",
            age: 18,
            add: function(num1, num2) {
                console.log(num1 + num2);
            }
        }
        let o2 = {
            name: "wyb",
            age: 28,
            sayAge: function() {
                console.log(this.age);
            }
        }

在这里插入图片描述

但是这点确值得注意 , 因为数组是一个引用类型的值 ,

let o2 = {}
function add(num1, num2, num3) {
    let arr = [num1, num2, num3]
    arr.forEach((item, index, array) => {
        item += 1
    })
    console.log(arr);
}
add.apply(o2, [1, 2, 3])

在这里插入图片描述

我给每一个item都+1,但是效果却没有

需要操作这个数组本身

 item += 1 ==> array[index] +=1

在这里插入图片描述

2.3 补充

补充一个不相关的经典问题 var 和 let

var name = "wangyubo";
function sayName() {
 console.log(name);
}
sayName() // ==>wangyubo
var name = "wangyubo";
function sayName() {
 console.log(this.name);
}
sayName() // ==>wangyubo
let name = "wangyubo";
function sayName() {
console.log(name);
}
sayName() //==>wangyubo
let name = "wangyubo";
function sayName() {
console.log(this.name);
}
sayName() //==> 空/undefined

let 申明的变量并不挂载在window上

在这里插入图片描述

2.4 经典继承之bind

语法 >>>

fn.bind( {},参数1,参数2)
  • bind()方法创建一个新的函数,在调用时设置this关键字为提供的值。
  • 并在调用新函数时,将绑定时给定参数列表作为原函数的参数序列的前若干项。

示例 >>>

let obj = {
    name: "wangyubo"
}
function sayName(age, sex) {
    console.log(`${this.name}:${age}:${sex}`);
}
let newFN = sayName.bind(obj, 18);//使用bind会创建一个新函数,所以需要一个变量来接收
//绑定bind时传了一个参数,这就作为sayName方法的第一个参数永久不变了
newFN("男");//函数第一次执行时又传入一个参数,这就作为sayName的第二个也是最后一个参数了
newFN("22");//函数第二次执行时又传入一个参数,此时这个参数会顶替掉sayName的最后一个参数

在这里插入图片描述

//你认为上面代码中的认知对吗?其实是不准确的
//绑定bind时传了一个参数,这就作为sayName方法的第一个参数永久不变了,这是对的,并且,如果绑定了两个参数,那么sayName方法在执行时传入的参数都无效了
let obj = {
    name: "wangyubo"
}
function sayName(age, sex) {
    console.log(`${this.name}:${age}:${sex}`);
}
let newFN = sayName.bind(obj, 18, "男");

newFN(28, "女")

在这里插入图片描述

输出的是bind绑定时的两个参数

//如果bind绑定时参数未满,那么函数执行时传入的参数就有效,并且是同位顶替,而不是一一叠加
let obj = {
    name: "wangyubo"
}

function sayName(age, sex, height) {//sayName需要的参数增加到3个
    console.log(`${this.name}:${age}:${sex}:${height}`);
}
let newFN = sayName.bind(obj, 18);//绑定时传入一个
newFN("男");//第一次执行时传入一个
newFN(180)//第二次执行时再传入一个

在这里插入图片描述

  • 如果是一一叠加 , 那么第二次执行时函数已经有了三个参数 , height应该是输出180才对
  • 而实际结果是180顶替了 , 成了第二个参数
// newFN("男");
// newFN(180)
newFN("男", 180)//这样才是第二个参数和第三个参数

在这里插入图片描述

  • 也就是说 , bind绑定时传入的参数是永久不变的 , 位置固定的
  • 而函数执行时 , 每次执行都会重新计算传入参数的位置
2.5 伪造对象
function GrandFather() {
 this.sayName = function() {
     console.log(this.name);
 }
}
function Mother() {
 this.name = "wangyubo"
}
function Father() {
 console.log(this);
 GrandFather.call(this);
}

let son = new Father()
son.name = "wangyubo"
son.sayName()

在这里插入图片描述

通过结果可知 , this指向Father的实例对象 , son是继承自Father , 且son虽然使用了GrandFather和Mother的一些方法和属性却不是继承自GrandFather和Mother , 而是Father 借用了GrandFather和Mother的一些方法和属再继承给了son

  • 通过伪造对象实现了借用原型链上的方法 , 而又不通过继承的方式
  • 相对于原型链而言,借用构造函数有一个很大的优势,即可以在子类型构造函数中向父类型构造函数传递参数
function GrandFather(name) {
    this.name = name;
}

function Father() {
    console.log(this);
    GrandFather.call(this, 'wangyubo');
}

let son = new Father();
console.log(son.name);

在这里插入图片描述

  • 那么实例对象也就能拿到这个属性
  • 而且也不会延伸原型链

3. 组合继承

  • 组合继承(combination inheritance),有时候也叫做伪经典继承,指的是将原型链和借用构造函数的技术组合到一块,从而发挥二者之长的一种继承模式。
  • 简单点说 , 实例对象利用call , apple , bind借用原型上的一些方法和属性 , 而原型上的方法本来又会继承给实例对象 , 这种就叫组合继承
  • 这样,既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有它自己的属性
  • 但是他会造成严重的属性重复问题 , 及其恶心
function GrandFather(name) {
    this.name = name;
    this.girlFriends = ["斋藤飞鸟", "新垣结衣"]
}
GrandFather.prototype.sayName = function() {
    console.log(this.name);
}

function Father(name, age) {
    GrandFather.call(this, name);
    this.age = age;
}
Father.prototype = new GrandFather();
Father.prototype.constructor = Father;
Father.prototype.sayAge = function() {
    console.log(this.age);
}

let son = new Father('wangyubo', 18);

在这里插入图片描述

  • name和girlFriends出现了两次 , 一级属性是源自于call方法借用的GrandFather里面的name和girlFriends , 第二次出现是GrandFather的实例对象Father继承了GrandFather原型对象上的属性
  • 这种方法基本没什么人用 , 仅用于学术使用

4. 原型式继承

道格拉斯·克罗克福德在2006 年写了一篇文章,题为Prototypal Inheritance in JavaScript (JavaScript中的原型式继承)。在这篇文章中,他介绍了一种实现继承的方法,这种方法并没有使用严格意义上的构造函数。他的想法是借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。为了达到这个目的,他给出了如下函数。

function obj(o) {
 function F() {};
 F.prototype = o;
 return new F();
}

在obj内部先创建一个临时性的构造函数 , 然后将传入的对象作为这个构造函数的原型对象 , 最后返回这个临时构造函数的新实例

用法之一 :

function obj(o) {
 function F() {};
 F.prototype = o;
 return new F();
}
var person = {
 name: "wangyubo",
 girlFriends: ["斋藤飞鸟", "迪丽热巴"],
}
var newPerson = obj(person);
newPerson.name = "wyb01";
newPerson.girlFriends.push("日向雏田")
var newPerson02 = obj(person);
newPerson02.name = "wyb02";
newPerson02.girlFriends.push("日向花火")
console.log(person.girlFriends);
console.log(newPerson.girlFriends);
console.log(newPerson02.girlFriends);

在这里插入图片描述

通过person对象新生成的新对象 , 引用类型的属性仍然在共用一个地址 , 这其实就是浅拷贝 , 也就是说

newPerson.girlFriends.push("日向雏田")
newPerson02.girlFriends.push("日向花火")

都是在操作原型对象 , 并非操作自己的属性

//当然 如果我在末尾给newPerson02.girlFriends重新赋值
newPerson02.girlFriends = ["新垣结衣"];
console.log(newPerson02.girlFriends);
//那么newPerson02.girlFriends就有了新的内存地址了 ,也就不再和前两个一样了
//这和前面学的是一样的道理

在这里插入图片描述

这种模式的缺点就是所有的方法和属性都堆积在原型上

5. 寄生式继承

  • 寄生式(parasitic)继承是与原型式继承紧密相关的一种思路,并且同样也是由克罗克福德推而广之的。
  • 寄生式继承的思路与寄生构造函数和工厂模式类似:
  • 创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真地是它做了所有工作一样返回对象
  • 在主要考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式。
function obj(o) {
    function F() {};
    F.prototype = o;
    return new F();
}

function createAnother(original) {
    var clone = obj(original);//通过调用函数来创建一个新对象
    clone.sayHello = function() {//以某种方式扩展这个对象
        console.log("Hello");
    }
    return clone;
}

和组合模式差不多 , 上面类似是构造函数 ,下面类似是原型

使用寄生式继承来为对象添加函数,会由于不能做到函数复用而降低效率;这一点与构造函数模式类似。

6. 寄生组合式继承

组合继承是JavaScript 最常用的继承模式;不过,它也有自己的不足。

  • 组合继承最大的问题就是无论什么情况下,都会调用两次超类型(父类)构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。
  • 没错,子类型最终会包含超类型对象的全部实例属性,但我们不得不在调用子类型构造函数时重写这些属性
function GrandFather(name) {
    this.name = name;
    this.girlFriends = ["斋藤飞鸟", "新垣结衣"]
}
GrandFather.prototype.sayName = function() {
    console.log(this.name);
}

function Father(name, age) {
    GrandFather.call(this, name);//第一次调用GrandFather超类构造函数
    this.age = age;
}
Father.prototype = new GrandFather();//第二次调用GrandFather超类构造函数,也是第一次调用构造函数
Father.prototype.constructor = Father;
Father.prototype.sayAge = function() {
    console.log(this.age);
}

let son = new Father('wangyubo', 18);//第二次调用构造函数

这种多次调用显得过于麻烦

所以就有了寄生组合式继承

  • 所谓寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。
  • 其背后的基本思路是:不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已。
  • 本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。
function obj(o) {
    function F() {};
    F.prototype = o;
    return new F();
}

function inheritPrototype(subType, superType) {
    var prototype = obj(superType.prototype); //创建超类的原型对象
    prototype.constructor = subType; //超类原型的构造函数指向子类构造函数
    subType.prototype = prototype; //将超类原型对象赋值给子类原型对象
}

示例 :

function SuperType(name) {
    this.name = name;
    this.girlFriends = ["斋藤飞鸟", "新垣结衣"]
}
SuperType.prototype.sayName = function() {
    console.log(this.name);
}

function SubType(name, age) {
    SuperType.call(this, name);
    this.age = age
}
inheritPrototype(SubType, SuperType)
SubType.prototype.sayAge = function() {
    console.log(this.age);
}
let instance = new SubType("wyb", 18)

在这里插入图片描述

  • 这个例子的高效率体现在它只调用了一次SuperType 构造函数,并且因此避免了在SubType.prototype 上面创建不必要的、多余的属性。
  • 与此同时,原型链还能保持不变;因此,还能够正常使用instanceof 和isPrototypeOf()。
  • 开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

你是时光 轻轻呵唱

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值