摘要
ES6实现Class之前,js使用函数和js的原型链来进行面向对象编程,而继承是面向对象编程中很重要的一个特性。在继承中,从已有的类中派生出新的类称为子类,子类继承父类的数据属性和行为,并能根据自己的需求扩展出新的行为,提高了代码的复用性。我们可以使用函数的call方法和让子类的原型指向父类的原型来实现js的组合继承,实际上这种方式的核心还是去操作js的原型链,所以我们可以通过了解js实现继承的方式去学习js的原型链。
介绍
本文主要介绍js的几种继承方式,并分析各自的优缺点和其中的原型链原理。
假设我们现在有以下父类Parent,我们来看看如何创建一个可以继承其属性和方法的子类。
function Parent(name){
this.name = name ? name : 'Tom';
this.gender = 'male';
this.arr = [1, 2, 3];
this.sleep = () => {
console.log(this.name + '正在睡觉');
}
}
Parent.prototype.eat = function(food){
console.log(this.name + '正在吃' + food)
}
一、通过函数的call方法实现继承
通过使用函数的call方法,我们可以改变子类this的指向,来让其继承父类的属性。
function Child(name){
Parent.call(this);
this.name = name ? name : 'chuck';
this.address = '开发区';
}
const child = new Child('yh2');
child.eat('rice'); //报错 不能继承父类原型链上的属性和方法
问题: call方法可以让Child的this获取到Parent的所有属性,但却获取不了原型链上的属性和方法。
二、通过修改原型链实现继承
由于实例对象找不到的属性会去构造函数的原型链上找,所以为了让子类能继承父类原型链上的方法,我们可以让子类的prototype指向一个父类实例:
function Child(){
this.address = '开发区'; // 新增属性
}
Child.prototype = new Parent();
const child = new Child();
child.eat('rice'); // Tom正在吃rice
原理: child的构造方法Child没有eat方法,所以child会去Child的原型链(Child.prototype)上找,而Child.prototype等于Parent的实例parent,所以child就能继承到parent对象的所有属性,但parent本身是没有eat方法的,于是又去Parent.prototype上找到了eat方法。总之:实例对象(object)找不到的属性会去构造函数(Object.prototype)的原型链上找。
问题: 但是Child的原型只是指向了同一个Parent的实例,所以他们继承到的属性都是来自同一个内存地址:
const child2 = new Child();
child2.arr.push(4);
console.log('child1', child1.arr); // child1 [ 1, 2, 3, 4 ]
console.log('child2', child2.arr); // child2 [ 1, 2, 3, 4 ]
如果操作的属性不是复杂类型,则不会出现以上的问题
child2.gender = 'female';
console.log(child2.gender); // female
console.log(child1.gender); // male
这是因为第一行代码给child2对象设置了新的属性gender, 而child1本身没有该属性,所以循着原型链找到了Parent的属性gender。其实把child1和child2打印一下就一目了然了:
child1 Parent { address: '开发区' }
child2 Parent { address: '开发区', gender: 'female' }
三、通过call方法和修改原型链实现组合继承
为了避免子类的实例因找不到属性而去获取父类的属性,我们可以使用call方法使子类获取父类的属性,然后再修改原型链获取父类的原型方法。
function Child(){
Parent.call(this); // 继承父类属性
this.address = '开发区'; // 新增属性
}
Child.prototype = Parent.prototype; // 继承父类原型方法
问题1:在Child的原型上加方法会影响父类的原型。
Child.prototype.foo = () => {
console.log('test');
}
console.log(Parent.prototype); // { eat: [Function (anonymous)], foo: [Function (anonymous)] }
问题2:子类Child的实例对象的构造函数竟然为Parent,按理应该为Child才对。
let child = new Child();
console.log(child.constructor); // [Function: Parent] constructor竟然为Parent而不是Child
console.log(child1.constructor === Child); // false
原因: 这两个问题都是因为Child的原型是浅拷贝到Parent的原型上的,问题1很好理解,问题2的话我们可以看看下面的原型链:
child1.constructor === Child.prototype.constructor; // true
Child.prototype === Parent.prototype; // true console.log(Parent.prototype.constructor); // [Function: Parent]
原型链原理:任何对象实例的构造函数都等于构造函数的原型的构造函数。 上面第一行其实可以一直扩展下去:
child1.constructor === Child.prototype.constructor; // true
Child.prototype.constructor === Child.prototype.constructor.prototype.constructor; // true
Child.prototype.constructor.prototype.constructor === Child.prototype.constructor.prototype.constructor.prototype.constructor; // true
......
四、完美组合继承
为了解决上面构造函数原型对象浅拷贝的问题,我们可以使用Object.create()方法,并设置Child的原型的构造函数为Child,这就是js的完美组合继承。
Object.create()方法创建一个对象,并使用现在的对象来提供新创建的对象的__proto__ 。
function Child(){
Parent.call(this);
this.address = '开发区';
}
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
let child = new Child();
console.log(child.constructor); // [Function: Child]
五、总结
js实现继承总的来说就分两步:
第一:使用call方法继承父类的属性;
第二:修改子类原型使其继承父类的原型属性(Parent.prototype)。
第二步注意不要浅拷贝,并且注意因为修改原型链导致的子类实例的构造函数为父类的问题。