在面向对象编程语言中,最基础最重要的概念就是 类和实例。
类的三大特性——继承、封装、多态
-
封装:类作为一个函数,把实现功能的代码进行封装,实现“低耦合高内聚”。
-
多态:包括方法的重载和重写。
- 重载:在其他语言中(如Java、C++等)重载是指相同的方法,参数或返回值不同,具备不同的功能。但JS不具备严格意义上的重载。
// java public void fn(int x,int y){} public void fn(int x){} fn(10,20); // 执行第一个函数 fn(10); // 执行第二个函数
// JS function fn(x,y){} function fn(x) // 会把前一个覆盖 fn(10,20) // 执行第二个函数 fn(10) // 同上
- 重写:指的是子类重写父类上的方法,一般伴随继承。
-
继承:子类继承父类中的属性、方法。继承的目的是让子类的实例同时也能使用父类中的私有 / 共有的属性 & 方法。
- 其他后端语言的继承类似自然界的机制,子类(儿子)把父类(父亲)身上的部分基因“copy”一份到自己身上,子类在自身上重写继承来的属性或方法,不会影响到父类。
- JS中的继承不是copy继承,而是基于原型链__proto__查找指向式的继承。
JS中的多种继承方式
原型继承
function Parent(){
this.x = 100;
}
Parent.prototype.getX = function getX(){
return this.x;
}
function Child(){
this.y = 200;
}
Child.prototype.getY = function getY(){
return this.y;
}
let c1 = new Child;
console.log(c1);
从前,Parent类和Child类分别同他们的原型、实例生活在一起,两个家族互不相干。
突然有一天他们相遇了,Parent强行要成为Child的爸爸,并许诺我家的东西你都可以用,但不是复制一份给你,也不是送给你,而是你可以通过秘密通道来拿我家的东西用。
秘密通道如何建立?那就是让子类的原型=父类的实例。
function Parent(){
this.x = 100;
}
Parent.prototype.getX = function getX(){
return this.x;
}
function Child(){
this.y = 200;
}
Child.prototype = new Parent; // 原型继承的核心代码
Child.prototype.getY = function getY(){
return this.y;
}
let c1 = new Child;
console.log(c1);
我们把子类的原型指向了父类的一个实例,子类的实例就可以通过__proto__找到子类的原型(这时是父类的实例),再继续沿__proto__找到父类的原型,如黄色的秘密通道所示。
原型继承有以下几个特点:
- 子类实例中只有子类的私有属性 / 方法(本例中是y),而无论是父类的私有还是公有的属性方法,都变成了子类实例公有的(x,getY,getX)。
- 是基于__proto__的指向查找式继承。
c1.__proto__.xxx = xxx
这样修改子类原型上的属性方法,会对子类的其他实例有影响(公有属性方法被修改了),而对父类的其他实例没有影响。通过c1.__proto__.__proto__.xxx = xxx
修改了父类原型上的属性方法,则对父类和子类的实例均有影响。
CALL继承
在原型继承的特点1中,父类的私有&公有属性方法都变成了子类的公有。这样不好…不好,我们希望通过继承,实现父类的私有(属性和方法,下同)成为子类的私有,父类的公有成为子类的公有。即私对私,公对公——公私分明!
沿用上例。
// 部分代码
function Parent(){
this.x = 100;
}
function Child(){
Parent.call(this); // 核心代码
this.y = 200;
}
let c1 = new Child;
在子类中把父类当作普通函数执行。默认Parent()函数中的this->window,而我们希望在c1实例中添加x属性,因此要通过call修改函数中的this,让其指向c1,即Child类中的this。这样就实现了父类的私有属性x也成为了子类的私有属性(这里其实是copy过来的)。
但有一点不好,父类当作普通函数执行,那它就失去了作为类的功能,子类无论如何都找不到父类的原型了,也就丢失了在父类原型上的属性方法…
寄生组合继承
这个方法有点意思,融合了原型继承和call继承的优点,真正实现了私对私,公对公~为此,我们需要在call继承的基础上加上原型继承。
原型继承的思想是让子类原型=父类的一个实例,即Child.prototype = new Parent
,换句话说,即Child.prototype.__proto__ = Parent.prototype
(父类实例原型链指向父类原型,没毛病吧)。
从另一个角度看,我们不创建新的父类实例了!而是让子类原型的__proto__重定向,原本指向Object.prototype,现在让它指向Parent.prototype。实现了原型继承同样的效果。——实际上就是原型继承。
但还有一个问题,IE6~8下是不允许我们操作__proto__的,我们使用Object.create的方法代替。
Object.create(A):创建一个空对象,让这个空对象的__proto__指向A。
// 寄生组合继承
function Parent(){
this.x = 100;
}
Parent.prototype.getX = function getX(){
return this.x;
}
function Child(){
Parent.call(this); // call继承当然要保留,实现私对私
this.y = 200;
}
Child.prototype = Object.create(Parent.prototype); // 另类原型继承
Child.prototype.getY = function getY(){
return this.y;
}
let c1 = new Child;
console.log(c1);
Child.prototype = Object.create(Parent.prototype)
这句核心代码实际上和Child.prototype = new Parent
别无二致,只是创建的不是父类的实例,而是一个空对象,然后手动更改原型链指向而已。这个操作也可以让父类的公有属性方法成为子类的公有属性方法。
ES6中的类和继承
class Parent{
constructor(){ // constructor函数实际上是Parent函数的私有属性方法
this.x = 100;
}
getX(){ // 在Parent.prototype上的公有属性方法
return this.x;
}
}
// 原本毫无关联的两个类,因为extends而成为了父子
class Child extends Parent{
constructor(){
super(); // 只要用extends实现继承,就要在constructor函数的第一行加一句super(),否则会报错
this.y = 200;
}
getY(){
return this.y;
}
}
ES6中的继承可以实现与寄生组合继承同样的效果,但写法是不是极其简单呐~