V少JS基础班之第十弹:面向对象之继承

一、 前言

第十弹内容是面向对象之继承。 面向对象是JavaScript语言中不可或缺的一部分。它非常非常非常的重要, 学好面向对象会为未来js逆向打下良好的基础,如果不能深刻的理解面向对象的思维,不能熟练掌握面向对象的内容,将会在后面的js逆向中走很多的弯路。 面向对象的学习是一个很大的工程,我无法在一篇文章中详细的讲完,所以决定将面向对象分成三部分来讲;也是面向对象的三大核心,他们分别是面向对象的: 封装继承多态

在开始之前还是有点废话的, 面向对象已经是js逆向学习中比较重要的知识点了, 我会尽心的给大家分享,也是真心的希望大家能通过我的文章学习到一些东西,那还是那句话,如果觉得文章对大家有帮助,请帮忙点赞、收藏,其实之前也只是零零散散的写一些文章,这次能如此有规划的写一篇专栏,大家鼓励的成分占了大半。好的,废话说完,让我们开始今天的内容。

本系列为一周一更,更新时间也是随便写的。后续内容做了部分修改。 从JS最基础【变量与作用域】到【异步编程,API】。希望自己能坚持下来, 也希望给准备入行JS逆向的朋友一些帮助, 脸皮厚了一点,明目张胆的要点赞,评论和收藏。也是希望如果本专栏真的对大家有帮助可以点个赞,有建议或者疑惑可以在下方随时问。

先预告一下【V少JS基础班】的全部内容。。
第一个月【变量作用域BOMDOM数据类型操作符
第二个月【函数闭包原型链this
第三个月【面向对象编程、 异步编程、事件】
第四个月【JSON、Storage、部分API】

==========================================================

二、本节涉及知识点

原型链进阶, 继承

==========================================================

三、重点内容

0、前言

封装我们已经学完了, 它其实更注重的是一种思想,一种编程习惯。它让我们的代码更加规范,数据处理更安全。
而我们今天学习的继承就更加实用:继承

口语os:

继承口语解释就是, 将通用的属性和方法抽离出来,放在一个公共类中,我们称这个类为基类或者父类。然后将需要使用这些方法或属性的类称为子类。 这样就构成了,子类继承父类的模式。这就是我们说的继承

1、原型链回顾

在学习继承之前,先理解原型
在 Java、Python 这样的语言里,继承都是围绕 class(类) 来实现的。我们定义一个类,再通过 extends 或类似的语法让子类继承父类。
但 JavaScript 的情况不一样。 早期的 JavaScript 并没有 class 这个概念(直到 ES6 才引入 class 语法,而且它本质上还是对原型的语法糖)。JS 中所有对象的继承,实际上是通过 原型(prototype) 来实现的。

所以,要真正理解 JavaScript 中的继承,就必须先理解原型机制:
规则如下

================================ 核心规则 ================================

三种对象的归纳(针对继承中):
原型对象
函数对象
实例对象

1- 所有的对象都有[[prototype]] 隐式原型,他不是一个属性,而是一个指向
1.1: 原型对象[[prototype]] :原型对象 [[Prototype]] 指向上一层构造函数 prototype【说明: 原型对象的 [[Prototype]] 指向上一层构造函数的 prototype。如果没有父类(基类),上一层就是 Object → 指向 Object.prototype;如果有父类 → 指向父类的 prototype。】
1.2: 函数对象的[[prototype]] :指向Function.prototype
1.3: 实例对象的[[prototype]] :指向其构造函数的.prototype

2- 只有函数对象有prototype这个属性,叫做原型对象。
2: 函数对象的prototype :构造函数的原型对象就是其原型对象

3- 所有对象都有 constructor 这个属性,叫做构造函数。
3.1: 原型对象的 constructor : 就是他的构造函数
3.2: 函数对象的 constructor : 是Function
3.3: 实例对象的 constructor : 是他的构造函数

以上是原型链的内容,如果不懂,请查看之前原型链的章节

================================================================================================

2、对象中的prototype和constructor

了解了上述的原型链知识之后,我们做一个小的回顾练习。 我现在有一个 Student 函数。 此时它是一个函数对象

function Student(name, age) {
  this.name = name;
  this.age = age;
}
s1 = new Student('zs', 18)

我们按照上述规则: 规则2, 函数对象有prototype,这个原型对象。 我们看一下这个原型对象. 我们不用看__ proto__了,其实他是Function。 因为跟继承关系不大,我们先忽略,而constructor也是与Function有关,我们暂时也忽略。

Student.prototype = {
  constructor: Student
  [[Prototype]]: Object
}

在这里插入图片描述
口语OS:

我们此时看到 Student 的 prototype上 只有一个 constuctor 和 [[prototype]]。 那我们给原型对象上增加方法之后,

Student.prototype.sayName = function () {
  console.log("My name is " + this.name);
};

我们再看一下, 此时,我们就将sayName挂载到了 Student.prototype 上

在这里插入图片描述

此时就有一个问题了。 这个原型对象是怎么来的呢, 我们是否可以随意操作它呢。

Student.prototype 是一个对象, 这个对象从Js引擎解析到 function Student 时就已经生成了, 并且自动绑定到了 Student 这个函数对象上
另一个问题, 我们当然可以修改原型对象。 我们先简单的置空

Student.prototype = {};           // 给 Student 构建一个新的空原型对象

在这里插入图片描述
此时, 我们已经将 Student 这个函数对象的 prototype 置空成了一个 {] 空对象。 以此我们实现了Student的原型修改。
但是!!! 此时的{}空对象,并不是完整的原型对象,甚至我们不可以把它叫成原型对象。 我们上面看到原型对象中需要自带两块内容。

Student.prototype = {
  constructor: Student
  [[Prototype]]: Object
}

我们在手动修改了圆形对象之后,需要将 constructor 和 [[Prototype]] 也做同样的修改。 这里我们根据规则1.1知道父类原型对象的上一层为Object.prototype。 其实我们手动创建的空对象上层也是Object.prototype。 所以此时我们不用修改[[Prototype]] 。 只需要重新指定constructor就可以了。

Student.prototype.constructor = Student; // 修正 constructor 指向

此时,我们就完成了整个 Student 原型对象的修改。 到这里如果能理解,那我们今天的继承就已经能完全理解了。

3、各种继承方式

1- 原型链继承
function Parent(name) {
  this.name = name;
}
Parent.prototype.sayHi = function() {
  console.log(`Hi, I'm ${this.name}`);
};

function Child() {}
Child.prototype = new Parent(); // 子类原型指向父类实例
Child.prototype.constructor = Child;

const c = new Child();
c.sayHi();

口语os:

new Parent();
// 为一个实例对象, 我们可以将他看成一个普通对象

我们使用 Parent 的实例对象作为原型对象,赋值给Child。 此时Child就能通过 Child.prototypede 形式访问到该原型对象, 而该原型对象又能通过原型链访问到Parent.prototype。此时就实现了继承。
在此方法中我们要注意的是需要重新构建constructor保持原型链的完整。

Child.prototype.constructor = Child;

原型链继承是最常见的继承方式,他是直接将父类的实例对象替换成子类的原型对象,并重新修复其constructor指向完成继承。

原型链形成:c -> Child.prototype -> Parent实例 -> Parent.prototype -> Object.prototype

缺点

我们确实通过原型链完成了继承, 但是我们还有一个关键的问题要考虑。 那就是,我们所有的子类原型对象都来自于统一Parent实例。此时就会有一个问题。那就是引用类型的问题

function Parent() {
  this.names = ["Alice", "Bob"]; // 引用类型属性
  this.age = 10;                 // 值类型属性
}

function Child() {}
Child.prototype = new Parent(); // 原型继承
Child.prototype.constructor = Child;

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

在原型对象中的属性和方法中,我们都能很好的运用, 只有一个点,那就是引用类型属性【this.names】,我们的方法可以随便用,this.age可以随便修改。 但是我们在修改this.names时会发现异常

c1.names.push("Charlie");
console.log(c1.names); // ["Alice","Bob","Charlie"]
console.log(c2.names); // ["Alice","Bob","Charlie"]

当我们修改c1时, c2.names也同步修改了。 原因就是我们继承自同一个Parent实例。 这是原型链继承的一大缺陷之一。那有没有什么好的解决办法呢, 我们接下来看一下构造函数继承。

2- 构造函数继承
function Parent(name) {
  this.name = name;
}
function Child(name) {
  Parent.call(this, name); // 借用父类构造函数
}

const c = new Child("Alice");
console.log(c.name);

口语os:

构造函数继承是通过 在子类构造函数内部调用父类构造函数 来实现的。
具体做法是使用 call 或 apply 方法,将父类构造函数的作用域绑定到子类实例上,从而继承父类实例属性。

但是说实话,构造函数继承本质上只是把父类构造函数内部逻辑执行在子类实例上,用于复用属性初始化代码,但它并不会形成原型链,也不是继承方法的手段。它只能作用于属性,而不能作用于方法。
甚至,如果父类的属性很少,使用构造函数继承,有一种多此一举的嫌疑。我甚至可以直接手写属性。 所以其实构造函数继承的作用是用于弥补原型链继承引用类型上的不足。

3- 组合继承

1- 定义父类

function Parent(name) {
  this.name = name;
  this.friends = ["Alice", "Bob"]; // 引用类型属性
}

Parent.prototype.sayHello = function () {
  console.log("Hello, my name is " + this.name);
};

2- 子类组合继承

function Child(name, age) {
  Parent.call(this, name); // 构造函数继承 → 给每个实例独立属性
  this.age = age;
}

// 原型链继承 → 继承父类方法
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;

// 给子类添加方法
Child.prototype.sayAge = function () {
  console.log("I am " + this.age + " years old");
};

3- 使用示例

const c1 = new Child("Tom", 18);
const c2 = new Child("Jerry", 20);

c1.friends.push("Charlie");

console.log(c1.friends); // ["Alice","Bob","Charlie"]
console.log(c2.friends); // ["Alice","Bob"]
console.log(c1 instanceof Child); // true
console.log(c1 instanceof Parent); // true

c1.sayHello(); // Hello, my name is Tom
c1.sayAge();   // I am 18 years old
4- 原型式继承(Object.create)
const parent = {
  name: "parent",
  sayHi() {
    console.log(`Hi, I'm ${this.name}`);
  }
};

const child = Object.create(parent);
child.name = "child";
child.sayHi();

原型式继承的特点是: 简单,直接创建一个以父对象为原型的对象。 更接近 JS 原型机制

口语os:

同样的, 示例中我们可以忽略。但是实际使用中我们还是要重新构建constructor

5- ES6 Class 继承(Class Inheritance)
class Parent {
  constructor(name) {
    this.name = name;
  }
  sayHi() {
    console.log(`Hi, I'm ${this.name}`);
  }
}

class Child extends Parent {
  constructor(name) {
    super(name); // 调用父类构造函数
  }
}

const c = new Child("Alice");
c.sayHi();

口语os:

了解一下吧

四、总结

最后留了一个小问题。 我们原型链继承中讲到。我们只重新构建了 constructor。 而没有重新构建重新构建[[prototype]]。 其实在实际应用中,如果我们有父类,子类和孙类的时候,是会出现异常的。大家可以考虑一下,如果在多层级的继承中合理修复原型对象,保证原型链的完整

以上就是本次 面向对象之继承 的全部内容了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值