✍️ Tangxt | ⏳ 2020-06-17 | 🏷️ JS 专题 |
在JS里边继承方式有很多种,但在真实项目里边,最常用的继承姿势就是这以下四种姿势:
应用场景:
我们需要关注的:
new
__proto__
指向了该实例的构造器的 prototype
正常一个类的多态就分为「重载」和「重写」
重载(一个东西:在一个类里边):
在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()
原型继承的效果:
原型继承的不好之处:
constructor
属性(或者原本在prototype
上设置的属性和方法) -> 如:b.constructor -> function A(){}
or 在使用「原型继承」之前你这样了B.prototype.sum = function sum(){}
,那么「原型继承」之后,sum
方法就GG了b.__proto__.xx = 'hi'
」(其它B的实例会受到影响,毕竟其它B的实例也可以访问这个xx
属性)、「b.__proto__.proto__.getX = null
(其它A的实例会受到影响,毕竟它们发现自己无法访问getX
方法了)」 -> 可以看到:「一个小小的b
实例居然有那么大的能耐……把A这个父类生成的实例搞得乌烟瘴气,甚至对自己的同僚,如B的其它实例,也搞得乌烟瘴气……」可见,我们明白「原型继承」的底层机制是怎么一回事儿之后,你会发现「原型继承」存在很多不合理性、很多不好的地方,但这就是整个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继承:把父类当做普通函数执行,让其执行的时候,其函数体中的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)
特点:
this.x = "A"
呗! -> 即便this.x = {name:'x'}
也不会影响到父类,毕竟我们是在执行一个函数,每次执行都会为「引用类型值」开辟一个新的空间好处:
局限性:
总之:
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;
可以看到:
prototype
)还是公有的,而私有的属性(来自类里边的this.xxx
)还是私有的……__proto__
修改公有的属性 -> 所以IE浏览器禁用了开发者使用__proto__
这个属性constructor
消失了 -> 所以还得要重新指定一下constructor
为B
!所谓的寄生组合?
A.call(this)
(到达私有只对私有,完成现实版的继承){} -> A -> B -> C -> …… -> Object -> null
-> Object.creat()
(到达公有只对公有)对于我们现在的真实项目来说,大多采用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 {}
然而,报错了:
在所有
this.x = xxx
代码走之前,请super()
一下
class B extends A {
constructor() {
// 一但使用extends实现继承,只要自己写了constructor,就必须写super -> 这是语法规定哈!
super();
this.y = "B";
}
getY() {
console.log(this.y);
}
}
测试:
一些对比:
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 = 15
、3+3+3+3+3 = 15
、45/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 也算一种基本类型? - 知乎
➹:使用es5和es6实现继承详解以及class的基本使用 - 知乎
has a
-> 也就是所谓的「组合」 -> 我看它的原型链,也就是它的__proto__
,也就是它有什么公有属性和方法 -> 也就是所谓的「继承」继承与组合
日常表述中经常会有如下的理解——
但严格说来,「父子」就是「面向对象」中对「继承」关系的描述。
而第 2 种说法或许正是引发困惑的原因
问题中的两种情况,从概念上其实很好区分
ClassB is a ClassA ClassA 是抽象,ClassB 是继承。因此可以说——ClassB 是 ClassA 的子类 当只需要考虑 ClassA 的特性时,ClassB 和 ClassA 基本等价
ClassB has a ClassA ClassB 是容器,ClassA 是部件。有时会说成——ClassA 是 ClassB 的「子类」 ClassB 和 ClassA 一般不可相互替代
A瓶子有「红色衣服」,B瓶子有「蓝色衣服」 -> 它们都是瓶子
➹:「is a」和「has a」分别是什么意思?有什么区别? - 钢盅郭子的回答 - 知乎
➹:Java is-a、has-a和like-a、组合、聚合和继承 两组概念的区别_caobaokang的专栏-CSDN博客_is-a 继承 java
兔子和羊「属于或是」食草动物类,狮子和豹「属于或是」食肉动物类。
食草动物和食肉动物又是「属于或是」动物类。
所以继承需要符合的关系是:is-a,父类更通用,子类更具体。
虽然食草动物和食肉动物都是「属于或是」动物,但是两者的属性和行为上有差别,所以子类会具有父类的一般特性也会具有自身的特性
不是很认同「js实际没有类的概念」这句话。类的概念是一个很抽象的东西,和是不是语法糖是没有联系的。只要是符合面向对向的概念进行了实现,就可以认为是创建了一个类,就算是用ES5的函数来实现也是一样。“概念”只是对一组规范的描述,作者可能对这一点有什么误解吧。
恩,没错,那你认为js中的数组是真正的数组吗?
哈哈,那要看对数组的理解了。js中的数组本质是对象,或者说放到任何语言里面都可以这么理解:数组是一系列具有相似结构或功能(泛型数组可以没有相似的结构,不过要使用数组的场景一般其元素是有相似功能的,除了极个别特殊情况)的一系列数据的集合,这个集合是一个提供了特定的数组操作方法的对象。