继承
继承有很多种方式
第一种:原型链继承
原型链继承是比较常见的继承方式之一,其中涉及的构造函数、原型和实例,三者之间存在着三角的关系,即每一个构造函数都有一个原型对象「prototype」,原型对象又包含一个指向构造函数的指针「constructor」,然后实例则包含一个隐式原型对象的指针「__ proto __ 称为隐式原型我想做一层区分而已帮助理解,抱歉因为Markdown语法关系,__ proto __ 存在空格」。
function Parent1() {
this.name = 'parent1';
this.play = [1, 2, 3]
}
function Child1() {
this.type = 'child1';
}
Child1.prototype = new Parent1();
console.log(new Child1());
var s1 = new Child1()
var s2 = new Child1()
s1.play.push(4)
console.log(s1.play, s2.play) // [1,2,3,4] [1,2,3,4]
但是有一个弊端就是,原型继承的对象只是一个引用,那么就是每个实例都可以修改,这就是使用原型链继承方式的一个缺点。
因为我们期望的是s2 = [1,2,3]
关于编程想法,为什么Child1.prototype = new Parent1()不放入Child1构造函数里面呢?
因为当我们new关键字作用的时候,通过 obj.__proto__ = Object.create(constructor.protype),这相当于拷贝对象,
那么下面执行这个constructor改变的原型已经意义不大了,因为指向的位置变了。
参考我之前写的new 在中间位置
第二种:构造函数继承(借助 call/apply)
function Parent2(){
this.name = 'parent2';
}
Parent2.prototype.getName = function () {
return this.name;
}
function Child2(){
Parent2.call(this);
this.type = 'child2'
}
let child = new Child2();
console.log(child); // 没问题
console.log(child.getName()); // 会报错
除了 Child2 的属性 type 之外,也继承了 Parent2 的属性 name。这样写的时候子类虽然能够拿到父类的属性值,解决了第一种继承方式的弊端,但问题是,只能继承父类的实例属性和方法,不能继承原型属性或者方法。
上面的两种继承方式各有优缺点,那么结合二者的优点,于是就产生了下面这种组合的继承方式。
第三种:组合继承(前两种组合)
function Parent3 (age) {
this.name = 'parent3';
this.play = [1, 2, 3];
this.age = age
}
Parent3.prototype.getName = function () {
return this.name;
}
function Child3() {
// 第二次调用 Parent3()
Parent3.call(this,30);
this.type = 'child3';
}
// 第一次调用 Parent3()
Child3.prototype = new Parent3(30);
// 手动挂上构造器,指向自己的构造函数
Child3.prototype.constructor = Child3;
var s3 = new Child3();
var s4 = new Child3();
s3.play.push(4);
console.log(s3.play, s4.play); // 不互相影响
console.log(s3.getName()); // 正常输出'parent3'
console.log(s4.getName()); // 正常输出'parent3'
又增加了一个新问题:通过注释我们可以看到 Parent3 执行了两次,第一次是改变Child3 的 prototype 的时候,第二次是通过 call 方法调用 Parent3 的时候,那么 Parent3 多构造一次就多进行了一次性能开销,这是我们不愿看到的。
这里还有一个比较细节的问题是第二次调用的Parent3,出现了属性在不同层级重复,Parent3的age也会在实例第一层对象上面,拥有这个“多余的”属性也按照原型链的规则,没什么问题。但在某些情况下会造成错误,例如删除实例上的age属性后,实际上还能访问到,此时获取到的是原型上的属性。
第四种:原型式继承
let parent4 = {
name: "parent4",
friends: ["p1", "p2", "p3"],
getName: function() {
return this.name;
}
};
let person4 = Object.create(parent4);
person4.name = "tom";
person4.friends.push("jerry");
let person5 = Object.create(parent4);
person5.friends.push("lucy");
console.log(person4.name); // tom
console.log(person4.name === person4.getName()); // tom === tom ===> tom
console.log(person5.name); // parent4
console.log(person4.friends); // ["p1", "p2", "p3",'jerry','lucy']
console.log(person5.friends); // ["p1", "p2", "p3",'jerry','lucy']
这种继承方式弊端是拷贝对象引用,这种可能会导致对象被修改。
第五种:寄生式继承
使用原型式继承可以获得一份目标对象的浅拷贝,然后利用这个浅拷贝的能力再进行增强,添加一些方法,这样的继承方式就叫作寄生式继承。
虽然其优缺点和原型式继承一样,但是对于普通对象的继承方式来说,寄生式继承相比于原型式继承,还是在父类基础上添加了更多的方法。那么我们看一下代码是怎么实现
let parent5 = {
name: "parent5",
friends: ["p1", "p2", "p3"],
getName: function() {
return this.name;
}
};
function clone(original) {
let clone = Object.create(original);
clone.getFriends = function() {
return this.friends;
};
return clone;
}
let person5 = clone(parent5);
console.log(person5.getName());
console.log(person5.getFriends());
第六种:寄生组合式继承
我们将组合继承和寄生式继承结合起来,得出了寄生组合式的继承,这也是所有继承方式里面相对最优的继承方式,代码如下。
function Parent6() {
this.name = 'parent6';
this.play = [1, 2, 3];
}
Parent6.prototype.getName = function () {
return this.name;
}
function Child6() {
Parent6.call(this);
this.friends = 'child6';
}
function clone (parent, child) {
// 这里改用 Object.create 就可以减少组合继承中多进行一次构造的过程
child.prototype = Object.create(parent.prototype);
child.prototype.constructor = child;
}
clone(Parent6, Child6);
Child6.prototype.getFriends = function () {
return this.friends;
}
let person6 = new Child6();
console.log(person6);
console.log(person6.getName());
console.log(person6.getFriends());
// 它可以解决组合继承 父类被调用两次和在不同层级属性重复的问题。
ES6 的 extends 关键字实现逻辑
我们可以利用 ES6 里的 extends 的语法糖,使用关键词很容易直接实现 JavaScript 的继承,但是如果想深入了解 extends 语法糖是怎么实现的,就得深入研究 extends 的底层逻辑。
class Person {
constructor(name) {
this.name = name
}
// 原型方法
// 即 Person.prototype.getName = function() { }
// 下面可以简写为 getName() {...}
getName = function () {
console.log('Person:', this.name)
}
}
class Gamer extends Person {
constructor(name, age) {
// 子类中存在构造函数,则需要在使用“this”之前首先调用 super()。
super(name)
this.age = age
}
}
const juiceice = new Gamer('juiceice', 20)
juiceice.getName() // 成功访问到父类的方法
因为浏览器的兼容性问题,如果遇到不支持 ES6 的浏览器,那么就得利用 babel 这个编译工具,将 ES6 的代码编译成 ES5,让一些不支持新语法的浏览器也能运行。
function _possibleConstructorReturn (self, call) {
// ...
return call && (typeof call === 'object' || typeof call === 'function') ? call : self;
}
function _inherits (subClass, superClass) {
// 这里可以看到
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: {
value: subClass,
enumerable: false,
writable: true,
configurable: true
}
});
if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
}
var Parent = function Parent () {
// 验证是否是 Parent 构造出来的 this
_classCallCheck(this, Parent);
};
var Child = (function (_Parent) {
_inherits(Child, _Parent);
function Child () {
_classCallCheck(this, Child);
return _possibleConstructorReturn(this, (Child.__proto__ || Object.getPrototypeOf(Child)).apply(this, arguments));
}
return Child;
}(Parent));
从上面编译完成的源码中可以看到,它采用的也是寄生组合继承方式,因此也证明了这种方式是较优的解决继承的方式。
总结
通过 Object.create 来划分不同的继承方式,最后的寄生式组合继承方式是通过组合继承改造之后的最优继承方式,而 extends 的语法糖和寄生组合继承的方式基本类似。
各位看官如遇上不理解的地方,或者我文章有不足、错误的地方,欢迎在评论区指出,感谢阅读。