zf-fe

✍️ Tangxt ⏳ 2020-06-17 🏷️ JS 专题

24-综合专题之JS中的四大继承方案

★概述

在JS里边继承方式有很多种,但在真实项目里边,最常用的继承姿势就是这以下四种姿势:

应用场景:

我们需要关注的:

★回顾之前的认识

★类的多态

正常一个类的多态就分为「重载」和「重写」

重载和重写

重载(一个东西:在一个类里边):

在Java中:

在JS中:

但对于运算符+是有所谓的「重载」现象的,如:

重载(运算子的不同,导致了不同的语法的行为)

'3' + 4 + 5  //  得到"345"
3 + 4 + '5'  //  得到'75'

而减法、乘法、除法运算符一律都是数值做相应的数学运算,不会发生重载:

1-2    // 得到-1
1-'2'  //得到-1
1*4    // 得到4
1*'4'  //得到4
1/2    // 得到0.5
1/'2'  //得到0.5

重写:子类重写父类上的方法(两个东西:子类 & 父类) -> 涉及到子类继承父类的属性和方法! -> 引出我们要讲的「继承」话题

◇代码测试(重载)

Java重载代码:

public String fn(int n,int m) {}
public String fn(int n) {}
public String fn(int n,double m) {}

fn(10,20) // -> 走第一个
fn(10) // -> 走第二个
fn(10,20.86) // -> 走第三个

// 后台语言是很严谨的,是啥就是啥,如你值传一个1值,那么即便你想走第一个,也无法走……

可以看到,方法名相同,但参数的个数不同或者类型不同或者返回值不同,就会发生重载 -> 也就是说调用同一个方法,但给定了不同的参数个数或者参数类型,就会走不同的逻辑 -> 注意:这并不会发生方法覆盖(对于JS来说是会发生覆盖的……但对于Java来说则不会)


Q:为啥后台语言要把一个方法拆成好多个方法?然后通过传参做不同的事儿呢?

主要原因:


JS重载代码?

function fn(n,m) {}
function fn(n) {}

fn(10,20) // -> 都走后边那个
fn(10) // -> 同上

JS可不管你传多少个参数,fn是谁就调谁! -> 不严谨

总之,后来的fn会覆盖前边的fn

当然,JS也是可以用重载的(不是严格意义上的重载),只是这重载只针对同一个方法呀:

function fn(n,m) {
  // 如果API调用仔没有传第二个参数,那就走xxx逻辑
  // 如果传了,那就走yyy逻辑
  if(!m) {
    // ……
  }
}

fn(10,20)
fn(10)

JS是咩有重载的,上边的代码并不是严格意义上的重载!

★继承

◇什么是继承?

子类继承父类中的属性和方法(JS中的继承机制和其它后台语言是不一样的,有自己的独特处理方式)


一个笑话:

有俩程序员 -> A和B

B不舒服,去医院检查,医生诊断说「你有类风湿性关节炎」

B一回来,A就问「有啥事没?」 -> B回答「我太难了,我这病我儿子也会有」

B为啥会这样说呢?

因为程序员眼中的「类」就是可以被继承的,所以跟「类」相关的词都应该可以被后一代所继承……

B的孩子 -> B -> 类风湿性关节炎


现实中的继承:

你继承了你爸的基因,相当于你爸的基因被克隆了一份到你身上去,而你也有特属于你自己的基因

我们经常在生活里边会听到,不要吃转基因食品,就是怕这些食品会改变我们的基因 -> 假如作为儿子的我们吃了转基因食品,因此改变了继承自父亲而来的基因,那么我们是否会因此而改变父亲的基因呢? -> 显然是不可能的呀!从你出生的那一刻起,你与父亲就是两个独立的个体了……


后台语言的继承:

子类继承父类,把父类的属性和方法拷贝一份 -> 子类可以重写继承过来的属性和方法,不会影响父类的属性和方法

JS的继承:

子类可以影响父类的属性和方法……

◇测试代码

测试代码

画图之:

测试代码图示

◇原型继承

需求:让A和B这两个类有关联 -> 如让B作为A的子类 -> B(子类)、A(父类)

实现:

function A() {
  this.x = "A";
}

A.prototype.getX = function getX() {
  console.log(this.x);
};

function B() {
  this.y = "B";
}

B.prototype = new A();

// B.prototype.__proto__ = A.prototype;

B.prototype.getY = function getY() {
  console.log(this.y);
};

let b = new B();

console.dir(b);

画图解释:

原型继承

一行代码实现继承:

B.prototype = new A()

原型继承的效果:

原型继承的不好之处:

  1. 重定向(重置)子类的原型后,默认丢失了原本的constructor属性(或者原本在prototype上设置的属性和方法) -> 如:b.constructor -> function A(){} or 在使用「原型继承」之前你这样了B.prototype.sum = function sum(){},那么「原型继承」之后,sum方法就GG了
  2. 子类或者子类的实例,可以基于原型链「肆意」修改父类上的属性和方法,对父类造成一些「不必要的破坏」 -> 如你这样:「b.__proto__.xx = 'hi'」(其它B的实例会受到影响,毕竟其它B的实例也可以访问这个xx属性)、「b.__proto__.proto__.getX = null(其它A的实例会受到影响,毕竟它们发现自己无法访问getX方法了)」 -> 可以看到:「一个小小的b实例居然有那么大的能耐……把A这个父类生成的实例搞得乌烟瘴气,甚至对自己的同僚,如B的其它实例,也搞得乌烟瘴气……
  3. 会把「父类中私有的属性和方法」作为「子类的公有的属性和方法」继承过来(简而言之,父类中不管是公有的还是私有的,最后都会变为子类公有的)

可见,我们明白「原型继承」的底层机制是怎么一回事儿之后,你会发现「原型继承」存在很多不合理性、很多不好的地方,但这就是整个JS的继承机制 ,即通过原型链来搞,况且,JS的面向对象机制都是基于原型链来搞的,所以JS的继承也是基于原型链来搞的 -> 这也就是为什么IE浏览器把__proto__给禁用的原因所在了,即一个子类实例可以肆意修改父类的东西


Q:`B.prototype.__proto__`指向`A.prototype`不也是一样的吗?

IE不支持__proto__,而且即便支持了,如你这样 B.prototype.__proto__ = A.prototype;,那么你在b.getX()的时候是拿不到x属性的值的!

而我们用B.prototype = new A();则是可以拿到的!


除了原型继承以外,还有call继承……

◇call继承

CALL继承:把父类当做普通函数执行,让其执行的时候,其函数体中的this变为子类的实例即可 -> 简单来说,就是在子类中执行

function A() {
  this.x = "A";
}

A.prototype.getX = function getX() {
  console.log(this.x);
};

function B() {
  // call继承
  A.call(this); // this.x = "A" -> b.x = "A"
  this.y = "B";
}

B.prototype.getY = function getY() {
  console.log(this.y);
};

let b = new B();

console.dir(b);

效果:

私有属性

一行代码实现继承:

A.call(this)

特点:

  1. 只能继承父类中的私有属性(继承的私有属性赋值给子类实例的私有属性),而且是类似拷贝过来一份,而不是链式查找 -> 说白了,就相当于我们在B里边写了个this.x = "A"呗! -> 即便this.x = {name:'x'}也不会影响到父类,毕竟我们是在执行一个函数,每次执行都会为「引用类型值」开辟一个新的空间
  2. 因为只是把父类当做普通的方法执行,所以父类原型上的公有属性方法无法被继承过来

好处:

  1. 拷贝了一份来自父类的私有属性(深拷贝),不会出现所谓的「链式查找机制」

局限性:

  1. 玩不了父类的公有属性

总之:

call姿势可以实现私有到私有,所以目前的问题是如何实现「公有到公有」,而我们的「原型继承」实现的是「把私有和公有的都变成公有的」 -> 所以,如何把「原型继承」的「私有公有化」给剔除出来呢?然后做到「私有的私有化,公有的公有化」 -> 「寄生组合继承」

◇寄生组合继承

寄生组合继承:CALL继承+变异版的原型继承共同完成的

function A() {
  this.x = "A";
}

A.prototype.getX = function getX() {
  console.log(this.x);
};

function B() {
  A.call(this); // this.x = "A" -> b.x = "A"
  this.y = "B";
}

// -> Object.create(OBJ) 创建一个空对象,让其__proto__指向OBJ(把OBJ作为空对象的原型)
B.prototype = Object.create(A.prototype);
B.prototype.constructor = B;

B.prototype.getY = function getY() {
  console.log(this.y);
};

let b = new B();

console.dir(b);

效果:

寄生组合继承

图示:

寄生组合继承图示

两行代码实现继承:

B.prototype = Object.create(A.prototype);
B.prototype.constructor = B;

可以看到:

所谓的寄生组合?

◇class实现继承

对于我们现在的真实项目来说,大多采用ES6的class语法来实现类与类之间的继承……

在ES6里边创建类用class

需求一:私有属性(似乎叫自有属性比较合适,毕竟私有属性一般指的是我们开发者无法访问的属性)和原型方法的创建

class A {
  constructor() {
    // 私有属性
    this.x = "A";
  }
  // 设置到A.prototype上的方法 -> 公有属性
  getX() {
    console.log(this.x);
  }
  // 这样和构造函数中的this.xxx=xxx没啥区别,即这设置的是私有属性(ES7)
  name = "A";
}
class B {
  constructor() {
    this.y = "B";
  }
  getY() {
    console.log(this.y);
  }
}

需求二:我们知道ES5里边的构造函数既是一个函数也是一个普通对象,那么这个ES6出现的class是否也是一个普通对象呢?即我们是否可以给class A添加键值对呢?

class A {
  constructor() {
    // 私有属性
    this.x = "A";
  }
  // 设置到A.prototype上的方法 -> 公有属性
  getX() {
    console.log(this.x);
  }
  name = "A";
  // 把 A当做普通对象设置的属性和方法
  static n = 200;
  static getN() {
    console.log(this.n);
  }
}

测试:

测试静态方法和静态字段

typeof A // -> "function"
({}).toString(A) // -> "[object Object]"

需求三:如何实现类与类之间的继承?

灰常简单(只使用一个extends关键字就好了):

class B extends A {}

然而,报错了:

加上super

在所有this.x = xxx代码走之前,请super()一下

class B extends A {
  constructor() {
    // 一但使用extends实现继承,只要自己写了constructor,就必须写super -> 这是语法规定哈!
    super();
    this.y = "B";
  }
  getY() {
    console.log(this.y);
  }
}

测试:

class继承

一些对比:

super('hi') // <=> A.call(this,'hi')
extends // <=> B.prototype = Object.create(A.prototype)、B.prototype.constructor = B

所以说:

extends继承和寄生组合继承基本类似,只是写起来用的是ES6语法来搞的!


Q:ES6是语法糖吗?还是浏览器实现了class?

不是语法糖,浏览器实现了class -> babel能把class语法编译成ES5的构造函数姿势,但这并不代表浏览器内部是这样实现class的,说白了babel它只是做了一个替代品的工作而已! -> 这就像是你打算通过计算得到一个数值15,那么你可以这样做3*5 = 153+3+3+3+3 = 1545/3 = 15……只是我们可以近似把它们看成是等价罢了,也就是大众意义所认为的语法糖!但其实这并不是……

总之,严格意义上来说,这并不是语法糖,但很多人都认为它是语法糖!


Q:浏览器是怎么实现class的?也是用prototype吗?还是用C(C++)实现的?

用C实现的,但其机制还是用prototype那套机制来走的,总之,都是按面向对象的那套「原型链查找机制」来搞的!


Q:`super`的含义是啥?起到什么作用?

super就是父类的别名

我们 super() 就相当于是call继承 -> 即把父类当作是个函数执行一下,让父类中的constructor中的this变成是子类的实例!

如果你传参super('xxx'),那么就相当于是,假如父类是A -> A.call(this,'xxx') -> 传参意味着,父类的constructor是有形参的!

★何时用继承?

在真实项目当中,什么地方会用到继承呢?

在真实项目当中,使用继承的地方有(说到继承就得有「类」这个字眼):

1)REACT创建类组件,如:

class Vote extends React.Component{

}

2)自己写插件或者类库的时候(平时业务中很少会用到……),如:

class Utils { 
  // -> 项目中公共的属性和方法
}

// -> 自定义一个模态框 -> Dialog插件需要用到Utils旗下的方法,所以需要继承
class Dialog extends Utils{}

把公共的方法都封装好,想用的时候就继承过来呗!

总之,写React组件时会用到,写类库、插件时会用到,而平时业务开发很少会用到!

★了解更多

➹:运算符和表达式-笔记 - 知乎

➹:Java 重写(Override)与重载(Overload) - 菜鸟教程

➹:JavaScript 里 Function 也算一种基本类型? - 知乎

➹:JS 的 new 到底是干什么的? - 知乎

➹:ES5继承 和 ES6继承 - 知乎

➹:使用es5和es6实现继承详解以及class的基本使用 - 知乎

★总结

★Q&A

1)`is a` & `has a` 之间的区别?

继承与组合

日常表述中经常会有如下的理解——

  1. 生出了乙,甲乙是「父子」
  2. 包含了乙,甲乙在层级上也可以视为「父子」

但严格说来,「父子」就是「面向对象」中对「继承」关系的描述。

而第 2 种说法或许正是引发困惑的原因

问题中的两种情况,从概念上其实很好区分

ClassB is a ClassA ClassA 是抽象,ClassB 是继承。因此可以说——ClassB 是 ClassA 的子类 当只需要考虑 ClassA 的特性时,ClassB 和 ClassA 基本等价

ClassB has a ClassA ClassB 是容器,ClassA 是部件。有时会说成——ClassA 是 ClassB 的「子类」 ClassB 和 ClassA 一般不可相互替代

is a & has a

A瓶子有「红色衣服」,B瓶子有「蓝色衣服」 -> 它们都是瓶子

➹:「is a」和「has a」分别是什么意思?有什么区别? - 钢盅郭子的回答 - 知乎

➹:Java is-a、has-a和like-a、组合、聚合和继承 两组概念的区别_caobaokang的专栏-CSDN博客_is-a 继承 java

2)生活中的继承?

继承

兔子和羊「属于或是」食草动物类,狮子和豹「属于或是」食肉动物类。

食草动物和食肉动物又是「属于或是」动物类。

所以继承需要符合的关系是:is-a,父类更通用,子类更具体。

虽然食草动物和食肉动物都是「属于或是」动物,但是两者的属性和行为上有差别,所以子类会具有父类的一般特性也会具有自身的特性

➹:Java 继承 - 菜鸟教程

3)js实际没有类的概念?

不是很认同「js实际没有类的概念」这句话。类的概念是一个很抽象的东西,和是不是语法糖是没有联系的。只要是符合面向对向的概念进行了实现,就可以认为是创建了一个类,就算是用ES5的函数来实现也是一样。“概念”只是对一组规范的描述,作者可能对这一点有什么误解吧。

恩,没错,那你认为js中的数组是真正的数组吗?

哈哈,那要看对数组的理解了。js中的数组本质是对象,或者说放到任何语言里面都可以这么理解:数组是一系列具有相似结构或功能(泛型数组可以没有相似的结构,不过要使用数组的场景一般其元素是有相似功能的,除了极个别特殊情况)的一系列数据的集合,这个集合是一个提供了特定的数组操作方法的对象

➹:js继承的几种方式 - 知乎