引言
前面我们复习了JS创建对象的几种方式,在里面也提到了原型和原型链的概念,而这两个东西在我们这一篇文章中也会经常用到,JS的继承就是根据原型链来进行的。在这里我们不讨论ES6
中Class
的Extends
继承,只讨论ES5
中常见的六种继承方式。
1.原型链继承
在上一节创建对象中,我们提到,每一个实例都有自己的原型,而这个实例可以通过prototype
这个属性找到他对应的原型。原型链继承的原理就来自于这里。这种继承方式应该是所有继承方法里面最简单的一个,当然也会存在很多的缺点。该方法继承的核心在于一个地方:子类型的原型为父类型原型的一个实例。下面我们举一个例子来说明:
// father
function Internet(name, ip) {
this.name = name;
this.ip = ip;
this.setName = function() {
}
}
// 定义一个私有方法
Internet.prototype.getIp = function() {
}
// children
function Iot(mac) {
this.mac = mac;
this.setMac = function() {
}
}
// 子类型的原型为父类型的一个实例,我们通过new操作符来创建
Iot.prototype = new Internet(); // 物联网继承了互联网的所有属性,替换物联网的原型
let Iotxh = new Iot('6A421');
let Iotce = new Iot('6A410');
console.log(Iotxh);
console.log(Iotce);
复制代码
这里我们可以在浏览器中去查看一下输出的结果,可以看到打印出来的对象的__proto__
属性,也就是上一节提到的指向该实例的构造函数的隐式原型的一个指针,是我们的Internet
实例,这样就可以通过Iotxh.prototype
访问到父类Internet
上的私有属性,然后再通过父类实例的prototype
就可以访问到父类原型上的方法。所以到最后,我们父类的所有的私有方法和公有方法和属性,都可以当做是子类的公有属性。
但是在这里有两个地方是需要我们注意的,分别是:
-
因为在JS中,我们操作基本数据类型的值的时候是直接修改其对应内存中的值,而当我们在修改引用类型的时候,我们修改的是这个地址所对应的值,所有引用这个地址的变量都将会受到影响,这个在我们上一节的创建对象当中也提到过。例如我们修改
Iotxh
中的ip
,那么Iotce
中的ip
也会受到影响。 -
当我们需要在继承后的子类中添加新的方法或者是重写父类的方法的时候,要放到我们替换原型之后进行。如果在替换原型之前进行,那么在我们替换原型之后,原来添加的东西还是在原来的原型上,最终是无效的。
老规矩,前面的方法,肯定都是有很多缺陷的,原型链继承的缺陷在于以下几点: 1.无法实现多继承 2.来自原型对象的所有属性被所有实例共享 3.创建子类实例时,没有办法向父类构造函数传参 4.上面提到的两个需要注意的地方
2.构造函数继承
为了解决部分上面提到的问题,我们继续复习构造函数继承的继承方式。
这个方法的核心在于this
指针的指向问题,也就是说,我们通过修改父类构造函数的this
指针指向来实现继承。先撸代码:
// father
function Internet(name, ip) {
this.name = name;
this.ip = ip;
this.setName = function() {
}
}
// 给父类的原型添加一个方法
Internet.prototype.getIp() {
}
// children
function Iot(mac, name, ip) {
Internet.call(this, name, ip);
this.mac = mac;
}
let Iotxh = new Iot('6A421','Weily','192.168.1.1');
console.log(Iotxh);
复制代码
在这里的继承也有一个地方是需要注意的,单纯的构造函数的继承方式,只是实现部分的继承,因为我们每次继承的时候都相当于调用了一次父类型的构造函数,那么当父类的原型还有构造函数外的方法和属性,子类是拿不到这些方法和属性的。例如上面的getIp()方法,就是无法通过这种方式继承的。
OK,简单的复习这种继承方式,我们谈一下他的缺点: 1.创建的实例并不是父类的实例,只是子类的实例 2.只能继承父类的实例属性和方法,不能继承原型属性和方法 3.无法实现函数复用,每个子类都有父类的实例函数的副本,浪费内存
3.原型链 + 构造函数 === 组合继承
按照套路,讲完了两种继承方式,我们肯定有一种他们结合的更优的继承方式。这种继承的方式可以概括为:通过调用父类的构造函数,继承父类的属性并能进行传参,并通过将子类的原型指向父类的实例,实现函数的复用。
老规矩,我们还是先撸代码:
// father
function Internet(name, ip) {
this.name = name;
this.ip = ip;
this.setName = function() {
}
}
// add methods
Internet.prototype.setNmae = function() {
}
// children 构造继承
function Iot(mac, name, ip) {
Internet.call(this, name, ip);
this.mac = mac;
this.getMac = function() {
}
}
// 原型继承
Iot.prototype = new Internet();
// 将子类的构造函数从Internet修复到Iot
Iot.prototype.constructor = Iot;
// 为子类的原型添加新方法
Iot.prototype.getName() {
}
let iotxh = new Iot('6A421', 'Weily', '192.168.1.1');
let iotce = new Iot('6A410', 'ce', '192.168.1.2');
console.log(iotxh);
复制代码
这种方式融合了前面两种继承方式的优点,是JS中最常用的继承模式,但是并不是最优的继承模式。当然,这种方式也存在一定的缺点,分别是: 1.无论什么时候都会调用两次构造函数,第一次调用是在创建子类的原型时,另一次是在子类的构造函数的内部,子类最终会包含父类的全部实例属性,但我们不得不在调用子类构造函数时重写这些属性。
4.原型式继承
这种方式就开发来说,似乎用得很少,基本上没有看到使用这种方式进行继承的,他的原理是Object.creat()
的模拟实现,将传入的对象作为创建的对象的原型,先上代码:
function createObj(o) {
function F(){}
F.prototype = o;
return new F();
}
// father
let internet = {
name: 'weily',
ip: '192.168.1.1',
person:['ybw','wkm']
}
// 继承
let iotxh = createObj(internet);
let iotes = createObj(internet);
iotxh.name = 'Weily';
console.log(iotes.name); // weily
iotxh.person.push('ll');
console.log(iotes.person); // ['ybw', 'wkm', 'll']
复制代码
上面举了两个例子,想必大家也看出来了,这种继承方式的缺点也是会共享值。至于为什么修改name属性不影响,是因为那行代码是为iotxh创建了一个自己的name属性。
5.寄生式继承
这种继承方式的核心在于一个地方,构造一个中间件,让中间件的prototype
代理父类的prototype
。先上代码:
// 构造的中间件
function object(o) {
function F(){}
F.prototype = o;
return new F();
}
function createAnother(original) {
// 通过调用函数创建一个新对象
var clone = object(original);
// 以某种方式来增强这个对象
clone.sayHi = function() {
alert("hi");
}
return clone;
}
var person = {
name: "Bert",
friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = createAnother(person);
anotherPerson.sayHi(); // Hi
复制代码
通过中间件代理的方式,我们可以让子类的原型间接的和父类联系起来。
缺点: 每次创建对象的时候都会创建一遍方法。
6.BOSS--寄生组合式方式
OK,前面复习了五种继承方式,各有各的好处,当然缺点也不少,所以,我们有了最优的继承方式,寄生组合继承。
这种继承方式我们要达到的效果是,完美继承父类的属性和方法,且只调用一次父类的构造函数,最终子类的原型链还要保持不变。那么,我们需要如何来实现这个需求呢?
回想一下,组合继承方式已经能完美的实现除了调用一次父类构造函数之外的全部需求,那么,我们需要做的就是去解决这一个需求,很巧的是,我们上面提到的寄生继承方式,恰恰就是只调用了一次构造函数,那么解决办法就有了,通过中间件,间接的实现这个需求。
在组合继承中,我们第二次调用构造函数,是为了让子类的原型访问到父类的原型,那么我们通过中间件的方式,也能实现这样的功能,上代码:
// father
function Internet(name, ip) {
this.name = name;
this.ip = ip;
this.person = ['ybw', 'wkm'];
}
// add methods
Internet.prototype.changeName = function() {
}
// children
funciton Iot(mac, name, ip) {
Internet.call(this, name, ip);
this.mac = mac;
}
// 中间件
let F = function() {}
F.prototype = Internet.prototype;
Iot.prototype = new F();
let iotxh = new Iot('Weily', '192.168.1.1');
console.log(iotxh);
复制代码
最后,我们来封装一下这个最优的继承方法,便于以后我们开发中的使用:
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
function prototype(child, parent) {
let prototype = object(parent.prototype);
prototype.constructor = child;
child.prototype = prototype;
}
prototype(Child, Parent);
复制代码
OK,我们的ES5中的六种寄生方式就复习完了,ES6中的extend继承,我们后面在复习ES6的时候再来复习。欢迎关注我的博客博客
参考文献
JavaScript深入之继承的多种方式和优缺点 JavaScript常见的六种继承方式 JavaScript寄生继承的理解