从js实现继承来学习其中的原型链原理

172 阅读4分钟

摘要

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)。

第二步注意不要浅拷贝,并且注意因为修改原型链导致的子类实例的构造函数为父类的问题。

参考

www.jianshu.com/p/3eb7a1843…