前言
在我写文章之前照例去网上找了很多继承的文章看,大多数文章换汤不换药的写了六七种继承方式,然后标记每种继承方式的优缺点,知识点当然说的够多,但我以为还是存在以下问题:
构造函数和类混为一谈
类就是类,既不是函数,也不是对象,它是单独抽象的一个概念,在面向对象中自有其地位
为什么现在很多公司要求前端开发人员也要会一门后端语言,就是因为只学JavaScript无法全面的理解面向对象,比如说类的概念...
没弄明白究竟什么是继承
很多文章根本连继承都没有定义,把对象a和对象b有方法和属性上的联系就称为继承,这也是相当不严谨的说法
这两点看似不重要,只是说法问题,但涉及到如何理解面向对象,这是基础概念,基础概念重要不重要,我认为是重要的
1、相关定义
基于前面的问题,我必须先在此明确讨论的基础:
类:严格来说js中并没有类的概念,es6中的class从用法来说确实可称为类,但核心依然基于原型与原型链,因此和基于类的面向对象有巨大的差别
因此,为避免混淆,除了写到class与excends的部分,其他时候一律不使用类的概念。
继承:不是什么阿猫阿狗都可以被称为继承,要想被称为继承,起码应该具备以下几个特点:
- 儿子拥有父亲所有非私有的属性和方法;
- 儿子可以有自己的属性和方法;
- 儿子对父亲的方法可以有自己的实现;
- 儿子不管怎么做不能影响到父亲;
- 儿子之间不能互相影响;
这是最基本的继承的条件,这样看js中所谓的原型链继承、构造函数继承等根本不符合继承的特点,弊端太多,顶多可以叫缺胳膊短腿的继承
原型就是原型、构造函数就是构造函数、工厂模式就是工厂模式,它们已经有了明确的定义,不需要强行和继承拉郎配...
2、js如何实现一个简单的继承:原型+构造函数
我们根据上文提到的继承的基础特点,结合原型链来实现继承:
- 儿子的原型需要指向父亲,这样才可以访问到父亲的属性和方法——原型指向及绑定this
- 每个儿子对象都需要维护自己的一套变量,这样才不会影响的父亲和其他儿子——新对象
具体实现如下:
//定义一个构造函数用来代表父亲
function Parent(name,age) {
this.name = name;
this.age = age;
this.staticLike = ['吃肉','喝酒'];
}
//父亲具备的方法
Parent.prototype.show = function () {
console.log('我的名字是: ',this.name,',年龄是:',this.age)
}
//定义一个构造函数用来代表孩子
function Child(name, age, gender) {
Parent.call(this,name,age); //继承了父亲的属性,但是this指向孩子
this.gender = gender;
}
Child.prototype = new Parent(); //原型指向
Child.prototype.constructor = Child; //重新指定原型后需要手动设置constructor
//孩子具备的方法
Child.prototype.showMe = function(){
console.log('我的名字是: ',this.name,',年龄是:',this.age,',性别是:',this.gender)
}
let allen = new Child('allen',12,'男');
let amy = new Child('amy',10,'女');
allen.staticLike.push('打牌');
console.log(allen.staticLike); //['吃肉','喝酒','打牌']
console.log(amy.staticLike); //['吃肉','喝酒']
allen.show(); //我的名字是: allen ,年龄是: 12
allen.showMe(); //我的名字是: allen ,年龄是: 12 ,性别是: 男
amy.showMe(); //我的名字是: amy ,年龄是: 10 ,性别是: 女
也许你一看这不就是别人说的组合式继承么,没错,最简单的也是最起码的继承就是上面的这种写法
它实现了开头那几点继承的定义,它们之间的关系是真·继承
但这种写法仍有缺陷,为了实现继承,我们多new了一个对象
Child.prototype = new Parent();
主要因为Parent是一个函数,无法充当原型,所以我们之后new了一个对象,让这个对象充当allen和amy的原型
但是此原型对象的属性是冗余的
总的来说,它简单易理解的实现了继承,是最常用的继承方式
3、没有明显缺陷的继承:寄生组合式继承
上述实现继承的方法主要问题是多new了一个对象,原型上的属性为冗余属性,内存方面多少有所浪费,那么我们有没有办法解决这个问题?
答案是肯定的,并且不只有一种实现
简单的实现是把原来的Child.prototype = new Parent();方式改写为Object.create()方式,实现如下:
//定义一个构造函数用来代表父亲
function Parent(name,age) {
this.name = name;
this.age = age;
this.staticLike = ['吃肉','喝酒'];
}
//父亲具备的方法
Parent.prototype.show = function () {
console.log('我的名字是: ',this.name,',年龄是:',this.age)
}
//定义一个构造函数用来代表孩子
function Child(name, age, gender) {
Parent.call(this,name,age); //继承了父亲的属性,但是this指向孩子
this.gender = gender;
}
// Child.prototype = new Parent(); //原型指向
// Child.prototype.constructor = Child; //重新指定原型后需要手动设置constructor
//具体描述符意义可见MDN:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
Child.prototype = Object.create(Parent.prototype,{
constructor: {
value: Child,
enumerable: false, //不可枚举
writable: true,
configurable: true
}
})
//孩子具备的方法
Child.prototype.showMe = function(){
console.log('我的名字是: ',this.name,',年龄是:',this.age,',性别是:',this.gender)
}
let allen = new Child('allen',12,'男');
let amy = new Child('amy',10,'女');
allen.staticLike.push('打牌');
console.log(allen.staticLike); //['吃肉','喝酒','打牌']
console.log(amy.staticLike); //['吃肉','喝酒']
allen.show(); //我的名字是: allen ,年龄是: 12
allen.showMe(); //我的名字是: allen ,年龄是: 12 ,性别是: 男
amy.showMe(); //我的名字是: amy ,年龄是: 10 ,性别是: 女
上述代码只是将注释掉的两行换了写法,结果如下:
没有了冗余属性
另一种方式是手动实现,用没有属性但链接了原型的空函数替代Parent,核心代码是:
inheritPrototype(Child, Parent)
function inheritPrototype(childFunc, parentFunc) {
let prototype = realizeInheritance(parentFunc.prototype) //创建原型对象
prototype.constructor = childFunc //绑定constructor
childFunc.prototype = prototype //指向原型
}
function realizeInheritance(parent) {
// 临时函数
function tempFunc() {}
tempFunc.prototype = parent
return new tempFunc()
}
这种实现方法优化了第一种的弊端,更完善的实现了继承
4、class,extends,super
es6提出了类的概念,并且用extends关键字实现了继承,其实现方式与寄生组合式实现类似
之前的方法都是为了更好的理解js语言,实际开发当然是直接用class,extends,super一套带走...
class Parent{
constructor(name,age){
this.name = name;
this.age = age;
this.staticLike = ['吃肉','喝酒'];
}
show(){
console.log('我的名字是: ',this.name,',年龄是:',this.age)
}
}
class Child extends Parent{
constructor(name,age,gender) {
super(name,age);
this.gender = gender;
}
showMe(){
console.log('我的名字是: ',this.name,',年龄是:',this.age,',性别是:',this.gender)
}
}
let allen = new Child('allen',12,'男');
let amy = new Child('amy',10,'女');
allen.staticLike.push('打牌');
console.log(allen.staticLike); //['吃肉','喝酒','打牌']
console.log(amy.staticLike); //['吃肉','喝酒']
allen.show(); //我的名字是: allen ,年龄是: 12
allen.showMe(); //我的名字是: allen ,年龄是: 12 ,性别是: 男
amy.showMe(); //我的名字是: amy ,年龄是: 10 ,性别是: 女
与众多后端语言写法一样,无甚好说,但要知道,js实际上做的还是函数、原型那一套东西
最后再强调一遍:JavaScript没有类,只有和类相同的写法。