原型链的缺陷
1.引用类型的值在原型链传递中存在的问题
js中有简单数据类型和引用数据类型(复杂数据类型),引用类型的值有一个特点:在赋值的时候,传递给变量的其实是它在内存当中的地址。被赋值完的变量相当于一个指针。这样一来就有大问题了,上代码:
function Father() {
this.name = 'father';
this.hobby = ['sport', 'read'];
}
function Son() {
}
Son.prototype = new Father();
let son1 = new Son();
let son2 = new Son();
console.log(son1.name);
console.log(son2.name);
console.log(son1.hobby);
console.log(son2.hobby);
打印如下:接着我们修改一下son1的hobby和name属性看看:
son1.name = 'son';
son1.hobby.push('play games');
console.log(son1);
console.log(son2);
console.log(son2.name);
console.log(son1.hobby);
console.log(son2.hobby);
打印结果如下:
(1).son1和son2有自己的hobby属性吗?
没有!只是Son的原型对象上有hobby属性,因为我修改了指向,指向了Father的实例,son1和son2其实是通过
__proto__
属性访问Son的原型对象上的hobby属性,从而访问和操作数组的。(2).son1和son2有自己的name属性吗?
开始都没有,但是后来我们手动给son1添加了name属性,相当于son1就有了,但是son2依然没有name属性
所以在原型链中如果
Father
含有引用类型的值,那么子类型的实例共享这个引用类型的值,也就是上面的hobby数组,这就是原型链的第一个缺陷。
2.第二个缺陷是:在创建子类型的实例时,是无法向父类型的构造函数中传递参数的,在上面的例子中,如果Father
的name
属性是要传递数的,而不是写死的,那么我们在实例化son1和son2的时候根本没办法传参。
借用构造函数继承
为了解决引用类型值带来的问题,我们采用借用构造函数继承的方式,它的核心思路是:在子类型的构造函数中调用父类型的构造函数,这里需要借助call()
或者apply()
方法,具体代码如下:
function Father() {
this.name = 'father';
this.hobby = ['sport', 'read'];
}
function Son() {
Father.call(this);
}
let son1 = new Son();
let son2 = new Son();
console.log(son1.name);
console.log(son2.name);
console.log(son1.hobby);
console.log(son2.hobby);
son1.name = 'son';
son1.hobby.push('play games');
console.log('------------------');
console.log(son1);
console.log(son2);
console.log(son2.name);
console.log(son1.hobby);
console.log(son2.hobby);
打印结果:
call()
方法,修改父构造函数的指向,来实现在子构造函数中借用父构造函数,完成继承,这样一来son1和son2都分别拥有自己的name
和hobby
属性,也就是打印结果的6,7行,son1和son2是完全独立的。这样就解决了原型链继承的第一个缺陷。我们再稍微修改一下Father构造函数:
function Father(name) {
this.name = name;
this.hobby = ['sport', 'read'];
}
function Son(name) {
Father.call(this, name);
}
let son1 = new Son('Jack');
let son2 = new Son('Bob');
console.log(son1.name);
console.log(son2.name);
console.log(son1.hobby);
console.log(son2.hobby);
打印结果如下:
sayF
方法并继承,那么只能在Father的构造函数里面写,Father.prototype
上的方法,没办法通过这种方式继承,也就是说,如果把所有的属性和方法都在构造函数中定义的话,就不能对函数方法进行复用。 虽然我们依然可以通过实例对象来调用这个方法,但是却是独立存在于实例当中的,不属于共享的方法。(你可以在Father构造函数中添加一个方法,然后打印实例对象,可以看到实例对象中包含了这个方法,而这个方法不是存在于原型链上的)
组合继承
我们了解了原型链继承和借用构造函数继承后,我们可以发现这两种继承是互补的一个关系。
- 原型链继承可以把方法定义在原型上,从而复用方法。
- 借用构造函数继承可以解决引用类型值的继承问题和传递参数的问题。
因此,我们可以结合这两种方法,就诞生了组合继承,具体代码实现如下:
function Father(name) {
this.name = name;
this.hobby = ['sport', 'read'];
}
Father.prototype.sayF = function () {
console.log('father');
}
function Son(name, age) {
Father.call(this, name);
this.age = age;
}
Son.prototype = new Father();
Son.prototype.constructor = Son;
Son.prototype.sayS = function () {
console.log('son');
}
let son1 = new Son('Jack', 20);
let son2 = new Son('Bob', 24);
console.log(son1);
console.log(son2);
son1.sayF();
son2.sayS();
打印结果如下:
sayF()
方法,在Son原型对象上定义了sayS()
方法,增加了Son.prototype = new Father();
原型链。最终实现了效果是,son1和son2都有各自的属性,同时方法还绑定在两个原型对象上,就达到了我们的目的:属性独立,方法复用
原型链负责原型对象上的方法,call借用构造函数负责让子类型拥有各自的属性,组合继承是js中最常用的继承方式。