关于前端:深入理解JavaScript继承

28次阅读

共计 10560 个字符,预计需要花费 27 分钟才能阅读完成。

JavaScript 的继承是基于原型实现,在前文 原型 里,笔者讲到了原型继承,并具体介绍了显式原型继承和隐式原型继承各自的两种办法。当初咱们以继承的视角切入,并以案例的模式来介绍 8 种常见的 JavaScript 继承办法

在看这篇之前,倡议先看 new,或者记住一句话,new 关键字所造成的原型链关系是: 实例.__proto__ === 构造函数.prototype

原型链继承

function Person(){this.brain = 'smart'}

Person.prototype.getBrain = function () {console.log(this.brain)
}

Person.prototype.age = 100;
Person.prototype.like = {color: 'red',}

function JoestarFamily(name) {
    this.name = name
    this.sayName = function() {console.log(this.name)
    }
}

JoestarFamily.prototype = new Person()
// 等同于 JoestarFamily.prototype === 实例.__proto__ === Person.prototype
var johnny = new JoestarFamily('johnny')
// 等同于 johnny.__proto__ === JoestarFamily.prototype
// 也就是说 johnny.__proto__.__proto__ === Person.prototype
var elaine = new JoestarFamily('elaine')

console.log(johnny, elaine)

原型链继承应用 new 关键字,将 JoestarFamily 的原型指向 Person 的实例,即子类的原型指向父类的实例,等同于 JoestarFamily.prototype === 实例.__proto__ === Person.prototype ,当它 new JoestarFamily 后,相当于 johnny.__proto__ === JoestarFamily.prototype,依照「等价交换准则」JoestarFamily.prototype 为独特值,换算后能够得出:johnny.__proto__.__proto__ === Person.prototype

其原型链构造如下:

兴许你会感到奇怪,怎么 JoestarFamily 无了,因为 JoestarFamily.prototype 间接赋值给了 new Person,其 JoestarFamily.prototype 上的 constructor 属性也没了

前文咱们在 JavaScript 由什么组成 中讲过根本类型能间接拷贝,而援用类型则不能间接拷贝,而是拷贝援用地址,所以咱们写了一篇——拷贝的机密 来实现对象的拷贝

在原型链继承中,如果原型上有对象(如 like 对象),所有实例都会跟着批改:

johnny.age = 1;
console.log(johnny) // 1
console.log(elaine.age) // 100

johnny.like.color = 'yellow';
console.log(johnny.like.color) // yellow
console.log(elaine.like.color) // yellow

长处:

  • 父类 / 父类原型新增属性和办法,子类实例可拜访
  • 简略,易于实现

毛病:

  • 无奈实现多继承
  • 原型对象的援用属性都被多个实例共享,不论是公有还是共有属性
  • 创立子类实例,无奈像父类构造函数传参

借用构造函数继承(经典继承)

此办法的关键在于,在子类的构造函数中通过 call/apply 之类的办法调用父类的构造函数

其原理是 this 的利用

function Person(brain) {
    this.brain = brain;

    this.others = {
        other1: 1,
        other2: 2
    };
    this.setBrain = function () {console.log("set brain");
    }
}

Person.prototype.getBrain = function () {console.log(this.brain)
}

Person.prototype.age = 100;
Person.prototype.like = {color: 'red',}

function JoestarFamily(name) {
    this.name = name
    this.sayName = function() {console.log(this.name)
    }
    Person.call(this, "smart")
}

var johnny = new JoestarFamily('johnny')
var elaine = new JoestarFamily('elaine')

console.log(johnny, elaine)

看到没,所有的属性和办法都在实例上,Person 构造函数中的属性和办法和 JoestarFamily 上的属性和办法都作用在实例 johnny 和 elaine

我的了解是「拿来主义」:

call 也好,apply 也罢,它们的作用都是为了批改 this 的指向。在这里,构造函数 JoestarFamily 中调用 Person.call(this, "smart"),意思就是:

function JoestarFamily(name) {
    this.name = name
    this.sayName = function() {console.log(this.name)
    }
    
    this.brain = "smart";

    this.others = {
        other1: 1,
        other2: 2
    };
    this.setBrain = function () {console.log("set brain");
    }
}

如此一来,两个实例的属性互不烦扰,就不存在批改原型链上的对象值而影响到其余实例

johnny.others.other1 = 123;
console.log(johnny.others.other1) // 123
console.log(elaine.others.other1) // 1

留神:所谓继承,是继承父类属性和办法。如果你在子类原型上增加对象属性,并批改对象属性中的某值,照样会影响所有的实例

但子类实例 johnny 和 elaine 却无奈继承 Person.prototype 上的属性和办法(毕竟没有继承,只是拿了 Person 中的属性和办法),如下:

johnny.getBrain() // Uncaught TypeError: johnny.getBrain is not a function
johnny.age // undefined

长处:

  • 解决了原型链中子类实例共享父类援用属性的问题
  • 创立子类实例,能够向父类传递参数
  • 能够实现多继承(call 多个父类对象)

毛病:

  • 实例并不是父类的实例,只是子类的实例

    • johnny instanceof JoestarFamily 为 true
    • johnny instanceof Person 为 false
    • 因为只是借用父类的函数和办法而非继承它
  • 只能继承父类的属性和办法,不能继承父类原型属性和办法
  • 占用内存,每个子类都有父类的属性和办法(截然不同),影响性能

原型链 + 借用构造函数的组合继承

既想又要,鱼和熊掌皆想获得。既想应用原型链(提取公共办法至原型上,缩小内存开销),又想让实例调用原型对象属性时不影响其余实例

怎么做呢?

function Person(brain) {
    this.brain = brain;

    this.others = {
        other1: 1,
        other2: 2
    };
    this.setBrain = function () {console.log("set brain");
    }
}


Person.prototype.getBrain = function () {console.log(this.brain)
}

Person.prototype.age = 100;
Person.prototype.like = {color: 'red',}

function JoestarFamily(name) {
    this.name = name
    this.sayName = function() {console.log(this.name)
    }
    Person.call(this, "smart")
}

JoestarFamily.prototype = new Person();
// 等同于 JoestarFamily.prototype  === 实例.__proto__ === Person.prototype
JoestarFamily.prototype.constructor = JoestarFamily; // 原型的 constructor 指回原来的构造函数

JoestarFamily.prototype.sayHello = function() {}

var johnny = new JoestarFamily('johnny')
var elaine = new JoestarFamily('elaine')

console.log(johnny, elaine)

它的原型链关系图如下:

如此,就看到了一个构造清晰的继承模式

它与原型链继承比:因为在子类构造函数中调用 call 获取到父类构造函数中的属性(借用构造函数继承),所以实例化时会当初本身属性上找,这些值是独一份的;

johnny.others.other1 = 123
console.log(johnny.others.other1); // 123
console.log(elaine.others.other1); // 1

与借用构造函数继承比:JoestarFamily 的原型继承了 Person 的原型,能应用 Person 原型上的属性和办法

johnny.getBrain() // smart
johnny.age // 100

这种形式联合了原型链继承和借用构造函数继承的长处,是 JavaScript 中最常见的继承模式,不过也存在毛病,就是无论什么时候都会调用两次父类构造函数

一次是设置子类原型时:

JoestarFamily.prototype = new Person();

一次是创立子类实例的时候:

var johnny = new JoestarFamily('johnny')
//  Person.call(this, "smart")

长处:

  • 能够继承父类的属性和办法,也能够继承父类原型的属性和办法
  • 不存在援用数据共享问题
  • 能够传参给父类构造函数
  • 函数能够复用

毛病:

  • 调用了两次构造函数,生成了两份实例(造成不必要地内存开销)

原型式继承

笔者在 原型 曾介绍过,原型继承分为显式原型继承和隐式原型继承,隐式原型继承是语言外部帮咱们实现,而显式原型继承则须要咱们入手实现

Object.create

function Person(brain) {
    this.brain = brain;

    this.others = {
        other1: 1,
        other2: 2
    };
    this.setBrain = function () {console.log("set brain");
    }
}

Person.prototype.getBrain = function () {console.log(this.brain)
}

Person.prototype.age = 100;
Person.prototype.like = {color: 'red',}

function JoestarFamily(name) {
    this.name = name
    this.sayName = function() {console.log(this.name)
    }
}

JoestarFamily.prototype = Object.create(Person.prototype); // 从新赋值原型,原先 JoestarFamily.prototype 上的 constructor 被抹除

JoestarFamily.prototype.constructor = JoestarFamily // 指回构造函数

var johnny = new JoestarFamily('johnny')
var elaine = new JoestarFamily('elaine')
console.log(johnny, elaine)

原型链的关系如:

关键在于 Object.create 和 new 的不同,笔者还是要不厌其烦地多说一句:

  • new 带来的原型链关系是: 实例.__proto__ === 构造函数.prototype
  • Object.create 的则是: 实例.__proto__ === 传入的对象

这个案例中传入的对象是 Person.prototype,所以就有了 johnny.__proto__ === Person.prototype,它继承自传入的对象,而不像前三种继承,继承构造函数的原型(因为 new)

console.log(johnny.__proto__) // === Person.prototype
console.log(johnny.others.other1) // Cannot read properties of undefined (reading 'other1')

如果咱们传入 Person,会有另一番微妙关系

所以通过 Object.create 创立的对象,它能继承传入对象的属性和办法

上诉例子中应用构造函数,构造函数中的属性和办法由 this 管制,所以没法被继承

长处:

  • 易于了解继承

毛病:

  • 原型从新赋值后须要将属性 constructor 从新赋值回来
  • 不能实现(子)类与(父)类的继承,只能实现对象的继承
  • 原型对象的援用属性会被多实例共享,不论是公有还是共有属性
  • 构造函数中的属性和办法无奈被继承

Object.setPrototypeOf

世人皆知李清照,无人念我朱淑真

想必 Object.setPrototypeOf 会像朱淑真那样说一句,我只不过晚出世几年,”才华“不比 Object.create 差,为什么没人记得我

function Person(brain) {
    this.brain = brain;

    this.others = {
        other1: 1,
        other2: 2
    };
    this.setBrain = function () {console.log("set brain");
    }
}

Person.prototype.getBrain = function () {console.log(this.brain)
}

Person.prototype.age = 100;
Person.prototype.like = {color: 'red',}

function JoestarFamily(name) {
    this.name = name
    this.sayName = function() {console.log(this.name)
    }
}

Object.setPrototypeOf(JoestarFamily.prototype, Person.prototype);

var johnny = new JoestarFamily('johnny')
var elaine = new JoestarFamily('elaine')
console.log(johnny, elaine)

其原型链关系图:

它与 Object.create 不同的是,它能传入两个对象,这样,咱们就能让子类原型继承自父类原型,实现继承

长处:

  • 易于了解继承

毛病:

  • 原型对象的援用属性会被多实例共享,不论是公有还是共有属性
  • 构造函数中的属性和办法无奈被继承

寄生式继承

故名思意,创立一个函数,该函数外部以某种形式加强对象,最初返回对象

function createObj(obj) {var clone = Object.create(obj)
    clone.sayHello = function() {console.log('hello')
    }
    return clone
}
let person = {
    name: 'johnny',
    age: 22
}

let anotherPerson = createObj(person)
anotherPerson.sayHello()

原型式继承的一种拓展

长处:

毛病:

  • 只是实用对象,与构造函数继承无关
  • 和借用构造函数模式一样,无奈实现函数复用,每次创建对象都会创立一遍办法

寄生组合式继承

原型链 + 借用构造函数的组合继承中,最大的毛病是会调用两次父类构造函数,如果解决这个问题那是不是就成了最佳继承呢?

不应用 joestarFamily.prototype = new Person(),而是用显式原型继承让 JoestarFamily.prototype 拜访到 Person.prototype 呢

function Person(brain) {
    this.brain = brain;

    this.others = {
        other1: 1,
        other2: 2
    };
    this.setBrain = function () {console.log("set brain");
    }
}


Person.prototype.getBrain = function () {console.log(this.brain)
}

Person.prototype.age = 100;
Person.prototype.like = {color: 'red',}

function JoestarFamily(name) {
    this.name = name
    this.sayName = function() {console.log(this.name)
    }
    Person.call(this, "smart")
}

var F = function () {} // 外围代码
F.prototype = Person.prototype // 外围代码

JoestarFamily.prototype = new F()

JoestarFamily.prototype.constructor = JoestarFamily; // 原型的 constructor 指回原来的构造函数

JoestarFamily.prototype.sayHello = function() {}

var johnny = new JoestarFamily('johnny')

console.log(johnny)

它的原型链关系图和组合继承一样:

不同的是,它少了因为 JoestarFamily 的原型上少了因 new Person 而产生的 brain、others、setBrain 等 Person 构造函数的内置属性和办法

其实现的秘诀在于这几行代码

var F = function () {} // 创立一个空构造函数
F.prototype = Person.prototype // 将 Person 原型赋值给空构造函数的原型
// 即 F.prototype 领有了 Person.prototype 上所有的属性和办法,包含 constructor,getBrain,age,like,__proto__
JoestarFamily.prototype = new F()
// new F,等于 JoestarFamily.prototype.__proto__ === F.prototype

这个办法就是援用了 Object.create 的外围代码,其本质是不必 new 的个性,而是用显式原型继承的法子,这样就不必因应用 new 而产生副作用

显式原型继承不止一种,你 Object.create 能做的,我 Object.setPrototypeOf 也能实现

Object.setPrototypeOf(JoestarFamily.prototype, Person.prototype)

- var F = function () {} 
- F.prototype = Person.prototype 
- JoestarFamily.prototype = new F()

留神到没有,new 是会有副作用的,它不仅会建设原型链关系,而且会执行构造函数中的代码,将其赋予内存中生成的一个对象,并返回它成为实例

而像显式原型继承则只做关系(原型链)的链接,比拟纯正

长处:同组合继承

毛病:暂无

类继承

除了以上几种继承外,ES6 的类继承,是模仿类继承而呈现的一语法糖,它的底层实现还是基于 prototype

class Person {constructor(brain) {
        this.brain = brain;
        this.others = {
            other1: 1,
            other2: 2
        };
        this.setBrain = function () {console.log("set brain");
        }
    }
    getBrain() {console.log(this.brain)
    }
    age = 100
    like = {color: 'red'}
}

class JoestarFamily extends Person{constructor(name) {super('smart')
        this.name = name
        this.sayName = function() {console.log(this.name)
        }
    }
    sayHello() {}
}
var johnny = new JoestarFamily('johnny')

console.log(johnny)

原型链关系图:

class 继承相比传统继承(以寄生组合继承为比照),它的不同点是

  • 构造函数也继承了:JoestarFamily.__proto__ === Person
  • 父类原型对象的属性继承无奈继承,如 Person.prototype.agePerson.prototype.like

class 的职责是充当创建对象的模板,通常来说,data 数据由 instance 承载,而行为 / 办法则写在 class 里

也就是说,基于 class 的继承,继承的是行为和构造,但没有继承数据

而基于 prototype 的继承,则继承了数据、构造和行为三者

而为什么 class 不能继承数据呢?这是为了投合 class 的根本行为,成心将其屏蔽

行为 / 办法能够专用,放在原型对象中,而数据则是独一份的,则可在构造函数中,这样就能解决大多数场景了,毕竟其余语言都是这样做的

案例剖析

来两题练练腿

依照如下要求实现 Person 和 Student 对象

  • Student 继承 Person
  • Person 蕴含一个实例变量 name,蕴含一个实例办法 printName
  • Student 蕴含一个实例变量 score,蕴含一个实例办法 printScore
  • Person 和 Student 之间共享一个办法 printCommon
function Person(name) {
    this.name = name
    this.printName = function() {console.log(this.name)
    }
}

Person.prototype.commonMethods = function() {console.log('共享方法')
}

function Student(name, score) {
    this.score = score
    this.printScore = function() {console.log(this.score)
    }
    Person.call(this, name)
}

var F = function() {}
F.prototype = Person.prototype

Student.prototype = new F()

var johnny = new Person('johnny')
var elaine = new Student('elaine', 99)
console.log(johnny.commonMethods === elaine.commonMethods)

这题较简略,再来一题,绘制原型链关系图

class A {}
class B extends A {}

const b = new B();

如果单单这几个原型链关系图的话还是简略的:

但如果要做全的话

这里就波及到 Object 和 Function 鸡生蛋蛋生鸡问题,具体能够看看这篇文章理解一二——JavaScript 中的始皇(后续文章更新)

总结

依照咱们在 原型 中所说,继承可分为显式原型继承和隐式原型继承,显式原型继承别离是 Object.create、Object.setPrototypeOf,隐式原型继承则是 new、对象字面量

这节咱们从传统继承了解角度看,别离解说了原型链继承、借用构造函数继承、组合继承(原型链 + 借用构造函数)、原型式继承(Object.create、Object.setPrototypeOf)、寄生式继承、寄生式组合继承(原型式 + 借用构造函数)、类继承等多种继承形式

也明确原型式继承就是显式原型继承

而与原型相干的继承如:原型链继承、Object.create、Object.setPrototypeOf 继承,有一个通病,即构造函数中的属性和办法无奈被继承,,并且它们原型对象的援用属性会被实例共享

而惟一的解决方案就是借用构造函数继承,即在子类构造函数中调用 this 指针,而组合继承和寄生组合继承都能实现完满的原型链关系,两者的区别在于组合继承调用 了两次构造函数,其起因是因为 new 的副作用,而寄生组合继承能胜过一筹的起因就是显式原型继承是不会产生副作用,只做简略的原型关系关联

尽管当初原型的知识点在前端已显得不那么重要,理由也很简略,JavaScript 的各种框架开始摈弃混入模式,而转向了组合模式(把办法提取到独立的类和辅助对象中,而后把它们组合起来,但不应用继承)。”组合胜过继承“的设计模式曾经让很多人遵循了,没必要了解原型也很失常,组合模式的运行也让函数式编程开始风行,不过这是后话

参考资料

  • JS:深刻了解 JavaScript- 继承
  • 深刻了解 JavaScript 原型
  • 如何答复面试中的 JavaScript 原型链问题

系列文章

  • 深刻了解 JavaScript- 开篇
  • 深刻了解 JavaScript-JavaScript 是什么
  • 深刻了解 JavaScript-JavaScript 由什么组成
  • 深刻了解 JavaScript- 所有皆对象
  • 深刻了解 JavaScript-Object(对象)
  • 深刻了解 JavaScript-new 做了什么
  • 深刻了解 JavaScript-Object.create
  • 深刻了解 JavaScript- 拷贝的机密
  • 深刻了解 JavaScript- 原型

正文完
 0