JavaScript 原型链实现继承

date
Nov 25, 2022
slug
achieve-inherit-by-prototype-in-js
status
Published
tags
Interview
summary
achieve inherit by prototype in javaScirpt
type
Post

基于原型链实现继承

原型链的继承并不是一上来就有一个完美的方案,而是随着需求和问题不断衍生不断优化的结果。
 

无继承存在的问题

在没有继承的情况下,即使多个类之间有很多相同的属性或者方法,每次定义类都需要重新编写重复代码,不利于代码的复用。
 

利用原型链实现简单的继承

 
function Person(){
    this.name = 'Kevin'
}

Person.prototype.eating = function(){
    console.log(this.name + ' eating')
}

function Student(){
    this.sno = 111
}

// 创建父类对象,并且作为子类的原型对象
// Student.prototype = new Person();
var p = new Person();
Student.prototype = p;

Student.prototype.studying = function(){
    console.log(this.name + ' studying')
}

var s = new Student();
s.eating();
 
这里通过创建父类对象并且赋值给子类原型,实现子类继承父类属性和方法, 具体内存表现如下:
 
notion image
 
以上方式虽然达到了部分继承的效果,但还是存在一定问题:
  1. name 属性是存在于p 对象上,而且这个属性无法通过打印查看
  1. 这个属性会被多个对象共享,如果是引用类型数据的话就会出问题。
  1. 不能给Person对象传递参数,当前p对象是一次性创建的,所以无法定制化
 

借用构造函数实现继承

由于存在以上的种种问题,因此需要针对这些问题进行优化,这里尝试借用构造函数来实现继承。
 
function Person(name, age, friends){
    this.name = name;
    this.age = age;
    this.friends = friends;
}

Person.prototype.eating = function(){
    console.log(this.name + ' eating')
}

function Student(name, age, friends, sno){
    // 使用call调用父类构造函数来传递参数,并且将当前子类在new 时创建的对象作为this传递给父类,将属性绑定到对应的this 上
    Person.call(this, name, age, friends)
    this.sno = sno
}

// 创建父类对象,并且作为子类的原型对象
// Student.prototype = new Person();
var p = new Person();
Student.prototype = p;

Student.prototype.studying = function(){
    console.log(this.name + ' studying')
}

var s1 = new Student('kevin', 18, ['Alan'], 111);
var s2 = new Student('Tom', 38, ['Bob'], 112);
console.log(s1, s2)
s1.friends.push('Chares')
console.log(s1.friends)
console.log(s2.friends)
s1.eating();
s2.eating();
 
这里核心是在子类中调用了父类的构造函数,将对应的属性绑定到子类对象上,这样解决了参数传递的问题,也解决了属性绑定的问题。实际内存表现如下:
 
notion image
上述方式看似解决了大部分的问题,已经基本达到继承的效果,不过依旧存在几个问题:
  1. 父类构造函数被多次调用,一个是创建子类原型时,一个是子类构造函数中(也就是每次创建子类对象时)
  1. 所有子类实例拥有两份父类属性,一份是自己本身,一份是子类原型对象中
 
这里需要打断一下,根据上面代码可以发现,其实我们核心在做的就是通过父类创建了一个实力对象,这个实力对象的原型指向了父类的原型对象,然后我们再将这个对象赋值给子类的原型对象,通过这种方式实现子类的实力对象的原型也指向了子类的原型对象,总而言之就是创建一个对象,然后让这个对象的原型指向了父类对象,那么这里是否有其他的形式可以实现这一点呢?
这就引入了原型式继承函数
 
var obj = {
    name: 'Kevin',
    age: 18
}

// 原型式继承函数
function protoFn(theObj) {
    function Fn() { }
    Fn.prototype = theObj;
    return new Fn();
}

var newObj = protoFn(obj)

console.log(newObj.__proto__ === obj)
 
通过上面这个函数,可以实现对象的原型指向另外一个对象,在ES6之后,我们还可以通过 Object.create() 或者是 setPrototypeOf 来实现这个函数。
 
var obj = {
    name: 'Kevin',
    age: 18
}

// 原型式继承函数
function protoFn(theObj) {
    var newObj = {}
    // 通过setPrototypeOf 给对象设置原型指向
    Object.setPrototypeOf(newObj, theObj);
    return newObj
}

var newObj = protoFn(obj)
// 也可以直接使用Object.create(), 第一个参数就是目标对象
// newObj = Object.create(obj)

console.log(newObj.__proto__ === obj)
 

寄生组合式继承

这是一个实现继承的分支,因为有了上述的原型式继承函数,就想到了利用原型式继承函数外加工厂模式来创建对象并且将对象的原型指向对应的对象,已实现继承,代码如下:
 
var personObj = {
    runing: function(){
        console.log('runing')
    }
}

function protoFn(theObj){
    function Fn(){}
    Fn.prototype = theObj;
    return new Fn();
}

function createStudent(name){
    var stu = protoFn(personObj);
    stu.name = name;
    stu.studying = function(){
        console.log('studying')
    }
    return stu
}

var s1 = createStudent('Kevin')
var s2 = createStudent('Bob')
 
这种方式感觉实现了继承,但是并没有达到预想的效果,他这里为每一个Student对象都绑定了一个对应的studying,并且工厂函数无法区分类别,因此这种方式并不合适。
 

寄生组合式继承

最终大招,结合上面的原型式继承函数和寄生组合,最终实现寄生组合式继承,通过inherit函数实现将子类原型指向父类原型。
 
// 自己实现Object.create 函数
function objCreate(proto){
    function Fn(){}
    Fn.prototype = proto;
    return new Fn();
}

// 继承函数
function inherit(subType, supType) {
    subType.prototype = objCreate(supType.prototype)
    Object.defineProperty(subType.prototype, 'constructor', {
        configurable: true,
        enumerable: false,
        writable: true,
        value: subType
    })
}

function Person(name, age, friends) {
    this.name = name;
    this.age = age;
    this.friends = friends;
}

Person.prototype.eating = function () {
    console.log(this.name + ' eating')
}

Person.prototype.running = function () {
    console.log(this.name + ' running')
}

function Student(name, age, friends, sno) {
    // 使用call调用父类构造函数来传递参数,并且将当前子类在new 时创建的对象作为this传递给父类,将属性绑定到对应的this 上
    Person.call(this, name, age, friends)
    this.sno = sno
}

// 调用继承函数,实现子类继承父类
inherit(Student, Person)

Student.prototype.studying = function () {
    console.log(this.name + ' studying')
}

var s1 = new Student('kevin', 18, ['Alan'], 111);
var s2 = new Student('Tom', 38, ['Bob'], 112);
console.log(s1, s2)
s1.friends.push('Chares')
console.log(s1.friends)
console.log(s2.friends)
s1.eating();
s2.eating();
 
按照上述方式实现的继承,已经没有了之前存在的几个问题,内存图如下:
 
notion image

© xk_wan 2021 - 2024