资讯专栏INFORMATION COLUMN

JavaScript 原型继承之精髓

xingqiba / 2242人阅读

摘要:它的原型也是对象。只要你完全抛开面向对象的继承思路来看的原型继承,你会发现它轻便但强大。最后写出来的代码会是这样请注意,只有函数才有属性,它是用来做原型继承的必需品。

一篇文章让你搞清楚 JavaScript 继承的本质、prototype__proto__constructor 都是什么。

很多小伙伴表示不明白 JavaScript 的继承,说是原型链,看起来又像类,究竟是原型还是类?各种 prototype__proto__constructor 内部变量更是傻傻搞不清楚。其实,只要明白继承的本质就很能理解,继承是为了代码复用。复用并不一定得通过类,JS 就采用了一种轻量简明的原型方案来实现。Java/C++ 等强类型语言中有类和对象的区别,但 JS 只有对象。它的原型也是对象。只要你完全抛开面向对象的继承思路来看 JS 的原型继承,你会发现它轻便但强大。

目录

继承方案的设计要求

被复用的对象:prototype

优雅的 API:ES6 class

简明的向上查找机制:__proto__

构造函数又是个啥玩意儿

双链合璧:终极全图

总结

参考

继承方案的设计要求

前面我们讲,继承的本质是为了更好地实现代码复用。再仔细思考,可以发现,这里的「代码」指的一定是「数据+行为」的复用,也就是把一组数据和数据相关的行为进行封装。为什么呢?因为,如果只是复用行为,那么使用函数就足够了;而如果只是复用数据,这使用 JavaScript 对象就可以了:

const parent = {
  some: "data",
}
const child = {
  ...parent,
  uniq: "data",
}

因此,只有数据+行为(已经类似于一个「对象」的概念)的封装,才是继承技术所必须出现的地方。为了满足这样的代码复用,一个继承体系的设计需要支持什么需求呢?

存储公用的数据和函数

覆盖被继承对象数据或函数的能力

向上查找/调用被继承对象函数的数据或函数的能力

优雅的语法(API)

增加新成员的能力

支持私有数据

「支持私有数据」,这个基本所有方案都没实现,此阶段我们可以不用纠结;而「增加新成员的能力」,基本所有的方案都能做到,也不再赘述,主要来看前四点。

被复用的对象:prototype

JavaScript 的继承有多种实现方式,具体有哪些,推荐读者可阅读:[JavaScript 语言精粹][]一书 和 这篇文章。这里,我们直接看一版比较优秀的实现:

function Animal(name) {
  this.name = name
  this.getName = function() {
    return this.name
  }
}

function Cat(name, age) {
  Animal.call(this, name)
  this.age = age || 1
  this.meow = function() {
    return `${this.getName()}eowww~~~~~, I"m ${this.age} year(s) old`
  }
}

const cat = new Cat("Lily", 2)
console.log(cat.meow()) // "Lilyeowww~~~~~, I"m 2 year(s) old"

这个方案,具备增添新成员的能力、调用被继承对象函数的能力等。一个比较重大的缺陷是:对象的所有方法 getName meow,都会随每个实例生成一份新的拷贝。这显然不是优秀的设计方案,我们期望的结果是,继承自同一对象的子对象,其所有的方法都共享自同一个函数实例。

怎么办呢?想法也很简单,就是把它们放到同一个地方去,并且还要跟这个「对象」关联起来。如此一想,用来生成这个「对象」的函数本身就是很好的地方。我们可以把它放在函数的任一一个变量上,比如:

Animal.functions.getName = function() {
  return this.name
}
Cat.functions.meow = function() {
  return `${this.getName()}eowww~~~~~, I"m ${this.age} year(s) old`
}

但这样调用起来,你就要写 animal.functions.getName(),并不方便。不要怕,JavaScript 这门语言本身已经帮你内置了这样的支持。它内部所用来存储公共函数的变量,就是你熟知的 prototype。当你调用对象上的方法时(如 cat.getName()),它会自动去 Cat.prototype 上去帮你找 getName 函数,而你只需要写 cat.getName() 即可。兼具了功能的实现和语法的优雅。

最后写出来的代码会是这样:

function Animal(name) {
  this.name = name
}
Animal.prototype.getName = function() {
  return this.name
}

function Cat(name, age) {
  Animal.call(this, name)
  this.age = age || 1
}
Cat.prototype = Object.create(Animal.prototype, { constructor: Cat })
Cat.prototype.meow = function() {
  return `${this.getName()}eowww~~~~~, I"m ${this.age} year(s) old`
}

请注意,只有函数才有 prototype 属性,它是用来做原型继承的必需品。

优雅的 API:ES6 class

然鹅,上面这个写法仍然并不优雅。在何处呢?一个是 prototype 这种暴露语言实现机制的关键词;一个是要命的是,这个函数内部的 this,依靠的是作为使用者的你记得使用 new 操作符去调用它才能得到正确的初始化。但是这里没有任何线索告诉你,应该使用 new 去调用这个函数,一旦你忘记了,也不会有任何编译期和运行期的错误信息。这样的语言特性,与其说是一个「继承方案」,不如说是一个 bug,一个不应出现的设计失误。

而这两个问题,在 ES6 提供的 class 关键词下,已经得到了非常妥善的解决,尽管它叫一个 class,但本质上其实是通过 prototype 实现的:

class Animal {
  constructor(name) {
    this.name = name
  }

  getName() {
    return this.name
  }
}

class Cat extends Animal {
  constructor(name, age) {
    super(name)
    this.age = age || 1
  }

  meow() {
    return `${this.getName()}eowww~~~~~, I"m ${this.age} year(s) old`
  }
}

如果你没有使用 new 操作符,编译器和运行时都会直接报错。为什么呢,我们将在[下一篇文章][]讲解

extends 关键字,会使解释器直接在底下完成基于原型的继承功能

现在,我们已经看到了一套比较完美的继承 API,也看到其底下使用 prototype 存储公共变量的地点和原理。接下来,我们要解决另外一个问题:prototype 有了,实例对象应该如何访问到它呢?这就关系到 JavaScript 的向上查找机制了。

简明的向上查找机制:__proto__
function Animal(name) {
  this.name = name
}
Animal.prototype.say = function() {
  return this.name
}
const cat = new Animal("kitty")

console.log(cat) // Animal { name: "kitty" }
cat.hasOwnProperty("say") // false

看上面

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/98785.html

相关文章

  • 体验js美第八课-面向对象创建和继承终结篇

    摘要:概述到这里我们讲说面向对象的系列部分的最后一个课程,面向对象必须掌握两个东西一个是对象的创建一个是继承。只需要记住一句话,属性放在构造函数里面,方法放在原型上。 概述 到这里我们讲说js面向对象的系列部分的最后一个课程,面向对象必须掌握两个东西一个是对象的创建一个是继承。这节课我们重点说说这两个问题最后我们说下在ES6里面面向对象怎么玩。 1对象的创建 我们第一节课已经就会用了,单体模...

    jzzlee 评论0 收藏0
  • 理解 JavaScript(四)

    摘要:其工作原理我已经在第一篇做了大部分的阐述我尚未提及的是在创建新对象的时候,会赋予新对象一个属性指向构造器的属性。 第四篇拖了很久了,真是有点不好意思。实话实说,拖延很久的原因主要是没想好怎么写,因为这一篇的主题比较有挑战性:原型和基于原型的继承——啊~我终于说出口了,这下没借口拖延了== 原型 我(个人)不喜欢的,就是讲原型时上来就拿类做比较的,所以我不会这样讲。不过我的确讲过构造器函...

    cuieney 评论0 收藏0
  • JavaScript 闯关记

    摘要:对象数组初始化表达式,闯关记之上文档对象模型是针对和文档的一个。闯关记之数组数组是值的有序集合。数组是动态的,根闯关记之语法的语法大量借鉴了及其他类语言如和的语法。 《JavaScript 闯关记》之 DOM(下) Element 类型 除了 Document 类型之外,Element 类型就要算是 Web 编程中最常用的类型了。Element 类型用于表现 XML 或 HTML 元素...

    mj 评论0 收藏0
  • 进击的 JavaScript(八) 继承

    摘要:也就是说,并不知道,等是属于哪个对象的哪个构造函数或者类。构造函数模式与原型模式相结合的模式。给新建的对象,添加属性,建立与构造函数之间的联系。另一种就是构造函数继承了。 前面讲完原型链,现在来讲继承,加深理解下。 一、对象的相关知识 什么是对象? 就是一些无序的 key : value 集合, 这个value 可以是 基本值,函数,对象。(注意 key 和 value 之间 是冒号 ...

    ddongjian0000 评论0 收藏0

发表评论

0条评论

xingqiba

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<