继承是面向对象的,继承可以帮助我们更好的复用以前的代码,缩短开发周期,提高开发效率;继承也常用在前端工程技术库的底层搭建上,在整个js的学习中尤为重要
常见的继承方式有以下的六种
原型链继承是比较常见的继承方式之一,其中涉及到构造函数、原型对象、实例对象,这三者之间存在着一定的关系,即每个构造函数都有一个原型对象,原型对象又包含一个指向构造函数的指针,而实例对象则包含一个原型对象的指针
function Parent() {
this.wealth = ["1w", "2k", "30w"];
this.fun = () => {
console.log("fun")
}
}
function Child() {
this.sex = "1";
}
Child.prototype = new Parent();
let child1 = new Child();
let child2 = new Child();
console.log(child1.wealth === child2.wealth , child1.fun === child2.fun);//true true
child1.fun.a = 1;
console.log(child1.fun.a, child2.fun.a);//1 1
child1.wealth.push("4k");
console.log(child1.wealth, child2.wealth);
// ["1w", "2k", "30w", "4k"] ["1w", "2k", "30w", "4k"]
可以看到,原型对象上的方法和属性都可以被实例对象访问,但是存在一个问题,多个实例对象共享一个原型对象,也就是说实例对象获取的原型对象上的属性和方法的内存空间是共享的,其中一个实例对象改变了它的原型对象,另外的实例对象获取原型对象身上的属性和方法也会发生改变,这是使用原型链继承的一个缺点
为了解决原型共享问题,构造函数继承方式借助call方法来解决
function Parent() {
this.wealth = ["1w", "2k", "30w"];
this.fun = () => {
console.log("fun");
};
}
Parent.prototype.getWealth = function () {
return this.wealth;
};
function Child() {
Parent.call(this);
this.sex = "1";
}
let child1 = new Child();
let child2 = new Child();
console.log(child1.wealth === child2.wealth, child1.fun === child2.fun);// false false
child1.fun.a = 1;
console.log(child1.fun.a, child2.fun.a);//1 undefined
child2.wealth.push("4k");
console.log(child1.wealth, child2.wealth);
// ["1w", "2k", "30w"] ["1w", "2k", "30w", "4k"]
child1.getWealth();//报错
可以看到,构造函数继承能够解决原型链继承的弊端,但同时也产生了新的问题,子类只能继承父类实例对象的属性和方法,无法继承父类原型上的属性和方法
由此可以看出,以上两种继承方式各有优缺点,那么结合二者的优点,就产生了下面这种组合的继承方式
function Parent() {
this.wealth = ["1w", "2k", "30w"];
this.fun = () => {
console.log("fun");
};
}
Parent.prototype.getWealth = function () {
console.log(this.wealth);
};
function Child() {
Parent.call(this);
this.sex = "1";
}
// 继承原型链上的方法属性
Child.prototype = new Parent();
// 原型对象私有化
Child.prototype.constructor = Child;
let child1 = new Child();
let child2 = new Child();
console.log(child1.wealth === child2.wealth, child1.fun === child2.fun);//false false
child1.fun.a = 1;
console.log(child1.fun.a, child2.fun.a);//1 undefined
child2.wealth.push("4k");
console.log(child1.wealth, child2.wealth);
// ["1w", "2k", "30w"] ["1w", "2k", "30w", "4k"]
child1.getWealth();// ["1w", "2k", "30w"]
结合前两种方式的优缺点,能够解决前两种方式的问题,但同时也带来了新的问题,Parent构造函数执行了两次,第一次是改变Child的原型时,第二次是通过call方法调用Parent时,多了一次性能开销
是否有更好的解决办法呢?下面的第六种继承方式可以更好的解决这里的问题
上面介绍的更多是围绕构造函数的方式,那么对于JS的普通对象,要怎么实现继承呢?
利用ES5里的Object.create
方法,实现普通对象的继承,是浅拷贝的实现方式之一,所以存在和浅拷贝一样的问题,存在多个实例对象的引用类型数据共享,存在篡改的可能
let parent = {
name: "parent",
friends: ["liming", "xiaolan"],
getName: function () {
return this.name;
},
};
let person1 = Object.create(parent);
let person2 = Object.create(parent);
console.log(
person1.name === person2.name,
person1.friends === person2.friends,
person1.getName === person2.getName
);
// true true true
person1.name = "xiaogui";
person1.friends.push("xiaohua");
person2.getName.desc = "good";
console.log(person1.name, person2.name);// xiaogui parent
console.log(person1.friends, person2.friends);
// ["liming","xiaolan","xiaohua"]
// ["liming","xiaolan","xiaohua"]
console.log(person1.getName.desc, person2.getName.desc);
//good good
关于这种继承方式的缺点也很明显,多个实例的引用类型属性指向相同的内存,存在篡改的可能
在原型式继承的基础上进行优化,利用原型式继承可以获得一份目标对象的浅拷贝,在浅拷贝实现功能的基础上再添加一些方法,这样的继承方式就叫做寄生式继承
let parent = {
name: "parent",
friends: ["liming", "xiaolan"],
getName: function () {
return this.name;
},
};
function clone(obj) {
let cloneObj = Object.create(obj);
cloneObj.getFriends = function () {
return this.friends;
};
return cloneObj;
}
let person1 = clone(parent);
let person2 = clone(parent);
console.log(
person1.name === person2.name,
person1.friends === person2.friends,
person1.getName === person2.getName
);// true true true
person1.name = "xiaogui";
person1.friends.push("xiaohua");
person2.getName.desc = "good";
console.log(person1.name, person2.name);//xiaogui parent
console.log(person1.friends, person2.friends);
// ["liming", "xiaolan", "xiaohua"] ["liming", "xiaolan", "xiaohua"]
console.log(person1.getName.desc, person2.getName.desc);//good good
console.log(person1.getFriends());// ["liming", "xiaolan", "xiaohua"]
console.log(person2.getFriends())// ["liming", "xiaolan", "xiaohua"]
可以看到,实例person1和person2通过clone方法,不仅有了原型上的getName方法,还增加了自身的getFriends方法,从而使普通对象在继承过程种又添加了一个方法,这就叫做寄生式继承
其优缺点和原型式继承一样,但对于普通对象的继承方式来说,寄生式继承相比于原型式继承,还是在父类基础上添加了更多的方法。
我们在讲组合继承方式时提到了一些弊端,即两次调用父类构造函数造成性能浪费,下面要讲的寄生组合式继承就可以解决这个问题
结合第四种原型式继承方式,解决普通对象继承问题的Object,create方法,在前面这几种继承方式的优缺点基础上进行改造,得出寄生组合式继承方式,相对最优的继承方式
function Parent() {
this.name = "parent";
this.fun = () => {
console.log("fun");
};
this.friends = [1, 2, 3];
}
Parent.prototype.me = () => {
console.log("11");
};
function Child() {
Parent.call(this);
this.sex = 1;
}
function clone(parent, child) {
// 这里改用Object.create可以减少组合继承中执行父类构造函数的次数
child.prototype = Object.create(parent.prototype);
child.prototype.constructor = child;
}
// Child.prototype = new Parent();
// Child.prototype.constructor = Child;//封装这两句话
clone(Parent, Child);
let child1 = new Child();
let child2 = new Child();
console.log(
child1.name === child2.name,
child1.fun === child2.fun,
child1.friends === child2.friends
);//true false false
child1.name = "xiaogui";
child1.fun.a = 1;
child1.friends.push(4);
console.log(child1.name, child2.name);//xiaogui parent
console.log(child1.fun.a, child2.fun.a);// 1 undefined
console.log(child1.friends, child2.friends);//[1,2,3,4] [1,2,3]
child2.me();//11
可以看到,寄生组合式继承基本可以解决前几种继承方式的缺点,较好地实现了继承想要的结果,同时也减少了构造次数,减少了性能开销
整体看下来,这六种继承方式中,寄生组合式继承是最优的继承方式,另外,ES6还提供了继承的关键字extends,下面是extends的底层实现继承的逻辑
利用ES6中的extends语法糖,可以很容易直接实现JS的继承,但如果想深入了解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 asuna = new Gamer('Asuna', 20)
asuna.getName() // 成功访问到父类的方法
因为浏览器的兼容性问题,如果遇到不支持ES6的浏览器,就得利用babel这个编译工具,将ES6代码编译成ES5,让一些不支持新语法的浏览器也能运行
最后extends编译成了
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));
从编译完的源码中可以看到,采用的也是寄生组合式继承方式,证明这种方式是较优的解决继承的方法
PS:这块的源码我没怎么看懂,先记下来后面反复看几遍
通过Object.create来划分不同的继承方式,最后的寄生组合式继承是通过组合继承改造后的最优继承方式,而ES6中的extends的语法糖和寄生组合继承的方式基本类似