JavaScript 对象系列:
第六篇
闲聊
JavaScript中有7中继承方式,你会哪几种呢?
- 原型链继承
- 借用构造函数继承
- 原型链 + 借用构造函数组合继承
- 原型式继承
- 寄生式继承
- 寄生组合式继承
- ES6的class继承
到了 2022 年,普遍都是支持 ES6 新语法的浏览器,那么肯定是 class 继承是最香的啦。其他的继承方式怎么办呢?还是了解一下比较的好,哈哈哈~~~
在正题开始之前,先问自己两个问题:
- 继承解决了什么问题?为什么要使用继承?
- 如何实现继承?
问题一:
为什么要使用继承,一张图片给你答案。
不同的类,存在大量的重复代码,需要封装,需要继承。
问题二:
一段代码,两张图给你答案。
// 父类
function Person() {
this.name = "父类";
}
Person.prototype.running = function () {
console.log(this.name + " running~~");
};
// 子类
function Coder() {
this.level = "初级前端工程师";
}
Coder.prototype.showLevel = function () {
console.log(this.level);
};
var coder = new Coder();
上面代码,存在两个类(Person、Student),那么它们在内存中的表现如何:
类与类之间相互独立,毫无相关。那么应该怎么产生关联呢?
看图说话,需要创建一个中间对象,该中间对象的作用:
- 子类的函数原型对象指向中间对象
- 中间对象的原型指向父类的函数原型对象
最后的结果就是子类和父类间接挂钩,继承也就在此诞生。
问了自己两个问题之后,搞清楚之后,正题就可以开始了。
原型链继承
将子类的函数原型(prototype)指向父类的函数原型(prototype),这就是原型链继承。
Coder.prototype = Person.prototype // 是这样吗?
当然不是。这样写的话,针对一个子类也还行,但是存在多个子类,都是使用了同一个原型(即父类的原型),那么子类与子类之间就会相互影响。那么应该怎么写呢?
Coder.prototype = new Person() // 正确写法
// 拆分来看
var obj = new Person()
Coder.prototype = obj
创建了一个实例对象Obj, Obj就是所谓的中间对象,那么子类的原型指向它,子类与子类之间就不会相互影响。
完整代码
function Person(name) {
this.name = name
}
Person.prototype.play = function () {
console.log("play");
};
function Coder(name, age) {
// 这里的name不好处理
this.age = age;
}
Coder.prototype = new Person(); // 关键步骤
Coder.prototype.eat = function () {
console.log("eat");
};
var coder = new Coder('copyer', 12)
console.log(coder) // Person { age: 12 } 类型错误,应为Coder
coder.eat() // eat
coder.play() // play
缺点
- 参数不好传递(子类的name不好传递到父类中)
- 子类的实例对象类型错误,继承的name属性也没有显示
类型错误的原因:Coder.prototype.constructor.name,由于本身丢弃,中间对象又没有constructor属性,就会寻找到Person的函数原型上,所以类型为Person
借用构造函数继承
子类直接调用父类函数。
function Person(name) {
this.name = name
}
Person.prototype.play = function () { }
function Coder(name, age) {
Person.call(this, name)
/*
借用了父类的代码:
this.name = name
*/
this.age = age
}
var coder = new Coder('copyer', 18)
console.log(coder) // Coder { name: 'copyer', age: 18 }
// 报错
coder.play() // coder.play is not a function
优点:
解决了传递参数的问题
缺点:
仅仅继承了父类的属性,并没有继承原型上的方法。
原型链 + 借用构造函数组合继承
为了解决原型链继承中存在的问题,开发人员提供了一种新的技术: constructor stealing(有很多名称: 借用构造函数或者称之为经典继承或者称之为伪造对象)。
借用继承的做法非常简单:在子类型构造函数的内部调用父类型构造函数。
- 因为函数可以在任意的时刻被调用,因此通过apply()和call()方法也可以在新创建的对象上执行构造函数。
//父类
function Person(name) {
this.name = name;
}
Person.prototype.play = function () {
console.log("play");
};
//子类
function Coder(name, age) {
Person.call(this, name) // 步骤一:借用构造函数
this.age = age;
}
Coder.prototype = new Person(); // 步骤二:原型链继承
Object.defineProperty(Coder.prototype, "constructor", { // 纠正实例对象的类型
enumerable: false,
configurable: true,
writable: true,
value: Coder,
});
var coder = new Coder('copyer', 18)
console.log(coder) // Coder { name: 'copyer', age: 18}
coder.play() // play
优点:
- 解决了传递参数的问题
- 解决了子类的类型不对的问题(指定子类的constructor)
缺点:
- 构造函数会被调用两次: 一次在创建子类型原型对象的时候, 一次在创建子类型实例的时候。
- 父类型中的属性(示例:
name属性)会有两份: 一份在中间对象中, 一份在子类型实例中
原型式继承
这种模式要从道格拉斯·克罗克福德(Douglas Crockford,著名的前端大师,JSON的创立者)在2006年写的 一篇文章说起: Prototypal Inheritance in JavaScript(在JS中使用原型式继承)。
怎么实现继承?需要中间对象,最开始我们就已经提及了。
那么是不是可以自己定义一个对象,使其__proto__指向另外一个对象呢?
可以定义一个函数,该函数接受一个对象作为参数,函数体内部创建一个新的对象,使其新对象的原型指向参数对象,然后返回新的对象。实现类似功能的函数,被称为原型式继承函数。
实现函数的三种方式:
// 方式一:借用构造函数原型的指向(最早使用的方法)
function createObject1(obj) {
function Fn() {}
Fn.prototype = obj
return new Fn()
}
// 方式二: 利用Object.setPrototypeOf可以改变原型
function createObject2(obj) {
var newObj = {};
Object.setPrototypeOf(newObj, obj); // newObj的原型指向obj
return newObj;
}
// 方式三:利用Object.create(),本质上内部代码实现跟方式一的实现是一样的 (最新版本ECMAScript提供)
const newObj = Object.create(obj)
既然有了原型式继承函数,那么就可以实现对象与对象之间的继承,类与类之间的继承了。
案例一:对象与对象之间的继承
const Person = {
name: "父类",
play: function () {
console.log("play~~");
},
};
const obj = Object.create(Person);
console.log(obj); // {}
console.log(obj.__proto__); // { name: '父类', play: [Function: play] }
案例二:类与类之间的继承
替换到原型链继承的一行代码即可
Coder.prototype = new Person()
// 修改成:
Coder.prototype = Object.create(Person.prototype)
优点:
创建中间对象简单,容易实现继承
缺点:
必须与其他方式的继承才能实现完整的继承(自己瞎编的)
寄生式继承
寄生式(Parasitic)继承是与原型式继承紧密相关的一种思想, 并且同样由道格拉斯·克罗克福德(Douglas Crockford)提出和推广的;
为什么需要寄生式继承?
当使用原型式继承的时候,会有如下的缺点(对象与对象之间的继承):
var Person = {}
var p1 = Object.create(Person)
p1.name = 'copyer'
p1.age = 12
var p2 = Object.create(Person)
p2.name = 'james'
p2.age = 35
...
当创建多个对象的时候,就需要写大量的重复性代码。就需要把相同的代码封装成一个函数,该函数就被称为寄生式继承函数。
函数的实现:创建一个封装继承过程的函数, 该函数在内部以某种方式来增强对象,最后再将这个对象返回。
// 寄生式继承函数
function createObject(name, age) {
var p = Object.create(Person)
p.name = name
p.age = age
p.play = function() {}
return p
}
// 然后调用
var p1 = createObject('copyer', 12)
var p2 = createObject('james', 35)
createObject函数是不是跟工厂函数一样啊。
所以:寄生式继承是由原型式继承与工厂函数的组合套餐。
寄生组合式继承
为什么叫这个名字,我也不清楚,没有达到见名知意效果。
在前面的原型链+借用构造函数的组合继承中,继承已经是比较理想了(不考虑性能的话)。那么为什么还需要寄生组合继承呢?
就是性能方面考虑,需要解决组合继承的两个缺点:
- 构造函数会被调用两次: 一次在创建子类型原型对象的时候, 一次在创建子类型实例的时候。
- 父类型中的属性会有两份: 一份在中间对象中, 一份在子类型实例中
寄生组合式继承就是为了解决这两个问题。
// 父类
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.running = function () {
console.log("running~~");
};
// 子类
function Coder(name, age, level) {
Person.call(this, name, age); // 借用构造函数继承(关键步骤一)
this.level = level;
}
//关键步骤二:Coder的原型指向创建出来的中间对象
Coder.prototype = Object.create(Person.prototype); // 省去了第二次调用构造函数
// 为了使子类的实例对象的类型正确
Object.defineProperty(Coder.prototype, "constructor", {
enumerable: false,
configurable: true,
writable: true,
value: Coder,
});
Coder.prototype.write = function () {
console.log("write~~");
};
var coder = new Coder("copyer", 12, "初级前端工程师");
console.log(coder); // Coder { name: 'copyer', age: 12, level: '初级前端工程师' }
coder.write(); // write~~
coder.running(); // running~~
优点:
- 父类只会被调用一次
- 继承属性只会保留在实例对象上,并不会出现在中间对象中。
缺点:
暂无(代码量大???)
class继承
ES6 中推出了class新语法特性,其继承只需要一个关键词extends即可。
// Student类 继承 Person类
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
play() {
console.log("play");
}
}
class Student extends Person {
constructor(name, age, address) {
super(name, age);
this.address = address;
}
running() {
console.log("running");
}
}
var s = new Student("copyer", 18, "cq");
优点:
代码量少,思路清晰,便于理解。
缺点:
浏览器兼容性的问题。ie10及以下都不支持。
小小推荐一波,我写的另外一篇,class语法被转化成ES5的代码,最后发现class就是寄生组合式继承。感兴趣的话,可以看看。
总结
本篇的知识点,是否需要掌握呢?
从基础角度来说,需要彻底掌握;因为这是面对对象的一个重要特性,面对对象又是一个很重要的思想,所以需要掌握。
从开发角度来说,了解即可;因为无论是react开发,还是vue3开发,现在都是函数式编程,面向对象的思想基本用不上(不是绝对哈)。
哈哈哈,最后的结论就是,根据自身的水平即可(基础好的,肯定掌握;基础不好的,也不用太过认真)。