一篇文章告诉你JavaScript 如何实现继承

news2024/10/7 20:24:54

一、背景简介

JavaScript 在编程语言界是个特殊种类,它和其他编程语言很不一样,JavaScript 可以在运行的时候动态地改变某个变量的类型。

比如你永远也没法想到像isTimeout这样一个变量可以存在多少种类型,除了布尔值true和false,它还可能是undefined、1和0、一个时间戳,甚至一个对象。

如果代码跑异常,打开浏览器,开始断点调试,发现InfoList这个变量第一次被赋值的时候是个数组:

[{name: 'test1', value: '11'}, {name: 'test2', value: '22'}]

过了一会竟然变成了一个对象:

{test1:'11', test2: '22'}

除了变量可以在运行时被赋值为任何类型以外,JavaScript 中也能实现继承,但它不像 Java、C++、C# 这些编程语言一样基于类来实现继承,而是基于原型进行继承。

这是因为 JavaScript 中有个特殊的存在:对象。每个对象还都拥有一个原型对象,并可以从中继承方法和属性。

提到对象和原型,有如下问题:

  1. JavaScript 的函数怎么也是个对象?
  2. proto和prototype到底是啥关系?
  3. JavaScript 中对象是怎么实现继承的?
  4. JavaScript 是怎么访问对象的方法和属性的?

二、原型对象和对象的关系

在 JavaScript 中,对象由一组或多组的属性和值组成:

{
  key1: value1,
  key2: value2,
  key3: value3,
}

在 JavaScript 中,对象的用途很是广泛,因为它的值既可以是原始类型(number、string、boolean、null、undefined、bigint和symbol),还可以是对象和函数。

不管是对象,还是函数和数组,它们都是Object的实例,也就是说在 JavaScript 中,除了原始类型以外,其余都是对象。

这也就解答了问题1:JavaScript 的函数怎么也是个对象?

在 JavaScript 中,函数也是一种特殊的对象,它同样拥有属性和值。所有的函数会有一个特别的属性prototype,该属性的值是一个对象,这个对象便是我们常说的“原型对象”。

我们可以在控制台打印一下这个属性:

function Person(name) {
  this.name = name;
}
console.log(Person.prototype);

打印结果显示为:

可以看到,该原型对象有两个属性:constructor和proto。

到这里,我们仿佛看到疑惑 “2:proto和prototype到底是啥关系?”的答案要出现了。在 JavaScript 中,proto属性指向对象的原型对象,对于函数来说,它的原型对象便是prototype。函数的原型对象prototype有以下特点:

  • 默认情况下,所有函数的原型对象(prototype)都拥有constructor属性,该属性指向与之关联的构造函数,在这里构造函数便是Person函数;
  • Person函数的原型对象(prototype)同样拥有自己的原型对象,用proto属性表示。前面说过,函数是Object的实例,因此Person.prototype的原型对象为Object.prototype。

我们可以用这样一张图来描述prototype、proto和constructor三个属性的关系:

从这个图中,我们可以找到这样的关系:

  • 在 JavaScript 中,proto属性指向对象的原型对象;
  • 对于函数来说,每个函数都有一个prototype属性,该属性为该函数的原型对象;

三、使用 prototype 和 proto 实现继承

对象之所以使用广泛,是因为对象的属性值可以为任意类型。因此,属性的值同样可以为另外一个对象,这意味着 JavaScript 可以这么做:通过将对象 A 的proto属性赋值为对象 B,即:

A.__proto__ = B

此时使用A.proto便可以访问 B 的属性和方法。

这样,JavaScript 可以在两个对象之间创建一个关联,使得一个对象可以访问另一个对象的属性和方法,从而实现了继承;

四、使用prototype和proto实现继承

以Person为例,当我们使用new Person()创建对象时,JavaScript 就会创建构造函数Person的实例,比如这里我们创建了一个叫“zhangsan”的Person:

var zhangsan = new Person("zhangsan");

上述这段代码在运行时,JavaScript 引擎通过将Person的原型对象prototype赋值给实例对象zhangsan的proto属性,实现了zhangsan对Person的继承,即执行了以下代码:

//JavaScript 引擎执行了以下代码
var zhangsan = {};
zhangsan.__proto__ = Person.prototype;
Person.call(zhangsan, "zhangsan");

我们来打印一下zhangsan实例:

console.log(zhangsan)

结果如下图所示:

可以看到,zhangsan作为Person的实例对象,它的proto指向了Person的原型对象,即Person.prototype。

这时,我们再补充下上图中的关系:

从这幅图中,我们可以清晰地看到构造函数和constructor属性、原型对象(prototype)和proto、实例对象之间的关系,这是很多容易混淆。根据这张图,我们可以得到以下的关系:

  1. 每个函数的原型对象(Person.prototype)都拥有constructor属性,指向该原型对象的构造函数(Person);
  2. 使用构造函数(new Person())可以创建对象,创建的对象称为实例对象(lily);
  3. 实例对象通过将proto属性指向构造函数的原型对象(Person.prototype),实现了该原型对象的继承。

那么现在,关于proto和prototype的关系,我们可以得到这样的答案:

  • 每个对象都有proto属性来标识自己所继承的原型对象,但只有函数才有prototype属性;
  • 对于函数来说,每个函数都有一个prototype属性,该属性为该函数的原型对象;
  • 通过将实例对象的proto属性赋值为其构造函数的原型对象prototype,JavaScript 可以使用构造函数创建对象的方式,来实现继承。

所以一个对象可通过proto访问原型对象上的属性和方法,而该原型同样也可通过proto访问它的原型对象,这样我们就在实例和原型之间构造了一条原型链。红色线条所示:

五、通过原型链访问对象的方法和属性

当 JavaScript 试图访问一个对象的属性时,会基于原型链进行查找。查找的过程是这样的:

  1. 首先会优先在该对象上搜寻。如果找不到,还会依次层层向上搜索该对象的原型对象、该对象的原型对象的原型对象等(套娃告警);
  2. JavaScript 中的所有对象都来自Object,Object.prototype.proto === null。null没有原型,并作为这个原型链中的最后一个环节;
  3. JavaScript 会遍历访问对象的整个原型链,如果最终依然找不到,此时会认为该对象的属性值为undefined。

我们可以通过一个具体的例子,来表示基于原型链的对象属性的访问过程,在该例子中我们构建了一条对象的原型链,并进行属性值的访问:

var o = {a: 1, b: 2}; // 让我们假设我们有一个对象 o, 其有自己的属性 a 和 b:
o.__proto__ = {b: 3, c: 4}; // o 的原型 o.__proto__有属性 b 和 c:

当我们在获取属性值的时候,就会触发原型链的查找:

console.log(o.a); // o.a => 1
console.log(o.b); // o.b => 2
console.log(o.c); // o.c => o.__proto__.c => 4
console.log(o.d); // o.c => o.__proto__.d => o.__proto__.__proto__ == null => undefined

综上,整个原型链如下:

{a:1, b:2} ---> {b:3, c:4} ---> null, // 这就是原型链的末尾,即 null

可以看到,当我们对对象进行属性值的获取时,会触发该对象的原型链查找过程。

既然 JavaScript 中会通过遍历原型链来访问对象的属性,那么我们可以通过原型链的方式进行继承。

也就是说,可以通过原型链去访问原型对象上的属性和方法,我们不需要在创建对象的时候给该对象重新赋值/添加方法。比如,我们调用lily.toString()时,JavaScript 引擎会进行以下操作:

  1. 先检查lily对象是否具有可用的toString()方法;
  2. 如果没有,则``检查lily的原型对象(Person.prototype)是否具有可用的toString()方法;
  3. 如果也没有,则检查Person()构造函数的prototype属性所指向的对象的原型对象(即Object.prototype)是否具有可用的toString()方法,于是该方法被调用。

由于通过原型链进行属性的查找,需要层层遍历各个原型对象,此时可能会带来性能问题:

  1. 当试图访问不存在的属性时,会遍历整个原型链;
  2. 在原型链上查找属性比较耗时,对性能有副作用,这在性能要求苛刻的情况下很重要。

因此,我们在设计对象的时候,需要注意代码中原型链的长度。当原型链过长时,可以选择进行分解,来避免可能带来的性能问题。

六、其他方式实现继承

除了通过原型链的方式实现 JavaScript 继承,JavaScript 中实现继承的方式还包括经典继承(盗用构造函数)、组合继承、原型式继承、寄生式继承,等等。

  • 原型链继承方式中引用类型的属性被所有实例共享,无法做到实例私有;
  • 经典继承方式可以实现实例属性私有,但要求类型只能通过构造函数来定义;
  • 组合继承融合原型链继承和构造函数的优点,它的实现如下:
function Parent(name) {
  // 私有属性,不共享
  this.name = name;
}
// 需要复用、共享的方法定义在父类原型上
Parent.prototype.speak = function() {
  console.log("hello");
};
function Child(name) {
  Parent.call(this, name);
}
// 继承方法
Child.prototype = new Parent();

组合继承模式通过将共享属性定义在父类原型上、将私有属性通过构造函数赋值的方式,实现了按需共享对象和方法,是 JavaScript 中最常用的继承模式。

虽然在继承的实现方式上有很多种,但实际上都离不开原型对象和原型链的内容,因此掌握proto和prototype、对象的继承等这些知识,是我们实现各种继承方式的前提条件。

七、总结

关于 JavaScript 的原型和继承,常常会在我们面试题中出现。随着 ES6/ES7 等新语法糖的出现,可能更倾向于使用class/extends等语法来编写代码,原型继承等概念逐渐变淡。

其次JavaScript 的设计在本质上依然没有变化,依然是基于原型来实现继承的。如果不了解这些内容,可能在我们遇到一些超出自己认知范围的内容时,很容易束手无策。


相关内容拓展:(技术前沿)

近10年间,甚至连传统企业都开始大面积数字化时,我们发现开发内部工具的过程中,大量的页面、场景、组件等在不断重复,这种重复造轮子的工作,浪费工程师的大量时间。

针对这类问题,低代码把某些重复出现的场景、流程,具象化成一个个组件、api、数据库接口,避免了重复造轮子。极大的提高了程序员的生产效率。

推荐一款程序员都应该知道的软件JNPF快速开发平台,采用业内领先的SpringBoot微服务架构、支持SpringCloud模式,完善了平台的扩增基础,满足了系统快速开发、灵活拓展、无缝集成和高性能应用等综合能力;采用前后端分离模式,前端和后端的开发人员可分工合作负责不同板块,省事又便捷。

免费体验官网:https://www.jnpfsoft.com/?csdn

还没有了解低代码这项技术可以赶紧体验学习!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/953692.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

大数据平台数据脱敏是什么意思?有哪些方案?

大数据平台包含了海量多样化数据,所以保障大数据平台数据安全非常重要,数据脱敏就是手段之一。今天我们就来简单聊聊大数据平台数据脱敏是什么意思?有哪些方案? 大数据平台数据脱敏是什么意思? 大数据平台数据脱敏简…

Golang数据结构和算法

Golang数据结构和算法 数据的逻辑结构和物理结构常见数据结构及其特点算法的时间复杂度和空间复杂度Golang冒泡排序Golang选择排序Golang插入排序Golang快速排序Golang归并排序Golang二分查找Golang sort包Golang链表Golang container/list标准库Golang栈stackGolang二叉搜索树…

域内密码喷洒

在Kerberos阶段认证的AS-REQ阶段,请求包cname对应的值是用户名,当用户名存在时候,密码正确和错误两种情况下,AS-REP返回包不一样,所以可以利用这一点对域用户名进行密码喷洒攻击 域内密码喷洒工具 Kerbrute kerbrut…

62.C++ deque容器

目录 1.deque容器基本概念 2.deque与vector区别: 3.deque构造函数 4.deque赋值操作 5.deque大小操作 6.deque双端操作和删除 7.deque数据存取 8.deque插⼊操作 9.deque删除操作 1.deque容器基本概念 deque是⼀种双向开⼝的连续线性空间。所谓的双向开⼝&…

找不到vcruntime140.dll,无法继续执行代码。可行的解决方法分享

在日常使用电脑的过程中,我们可能会遇到各种问题,其中之一就是提示“由于找不到 VCRUNTIME140.dll,无法继续执行代码。重新安装程序可能会解决此问题。”,这一般是什么原因导致了这个问题,我们要如何解决? …

14:00面试,14:10就出来了,问的问题有点变态......

从小厂出来,没想到在另一家公司又寄了。 到这家公司开始上班,加班是每天必不可少的,看在钱给的比较多的份上,就不太计较了。没想到5月一纸通知,所有人不准加班,加班费不仅没有了,薪资还要降40%…

亚马逊云科技GenAI菁英创造营,致力于大模型时代高校AI人才培养

大语言模型(LLM)产业的蓬勃发展将改变数字产业生态,助力AI工业化进程、变革海量应用交互方式、创造数字产业新的增长空间。 “GenAI Talent Program”由亚马逊云科技特别打造,该计划致力于大模型时代高校AI人才培养,通…

36种水果和蔬菜识别(pytorch框架,深度卷积网络模型,可以实现照片连续识别和视频识别)

1.效果视频:36种水果和蔬菜识别(pytorch框架,深度卷积网络模型,可以实现照片连续识别和视频识别)_哔哩哔哩_bilibili 2.项目文件夹 第一个文件夹(data): 装载的是原始图像 第二个文…

Spring Cloud 微服务2

Eureka 注册中心,服务的自动注册、发现、状态监控 Ribbon 负载均衡,Eureka中已经集成了负载均衡组件 Hystrix 熔断器,用于隔离访问远程服务、第三方库,防止出现级联失败。 Feign 远程调用,将Rest的请求进行隐藏&a…

全面解析MES系统中的报工操作

一、报工操作的定义: 报工操作是指在生产过程中,操作员通过MES系统记录和提交生产工序的相关信息,如工时、产量、质量等。报工操作将生产过程中的实际情况反馈给MES系统,实现生产数据的实时采集和记录。 二、报工操作的流程&…

骨传导耳机头晕,想吐怎么办?骨传导耳机会不会脑震荡

在解答骨传导耳机佩戴会头晕的这个现象,我先简单介绍一下,什么是骨传导耳机? 骨传导耳机可以理解为以人类骨头为媒介,声音通过骨头的振动传递到人的听觉神经再传递到听觉中枢,它是声音的一种传导方式,只不…

单片机通用学习-​什么是寄存器?​

什么是寄存器? 寄存器是一种特殊的存储器,主要用于存储和检查微机的状态。CPU寄存器用于存储和检查CPU的状态,具体包括计算中途数据、程序因中断或子程序分支时的返回地址、计算结果为零时的负值、计算结果为零时的信息、进位值等。 由于CP…

4、DVWA——文件包含

文章目录 一、文件包含概述二、low2.1 源码分析2.2 通关分析 三、medium3.1 源码分析3.2 通关思路 四、high4.1 源码分析4.2 通关思路 五、impossible 一、文件包含概述 文件包含是指当服务器开启allow_url_include选项时,就可以通过php的某些特性函数(i…

金融风控数据分析-信用评分卡建模

本文引用自: 金融风控:信用评分卡建模流程 - 知乎 (zhihu.com) 在原文的基础上加上了一部分自己的理解,转载在CSDN上作为保留记录。 本文涉及到的数据集可直接从天池上面下载: Give Me Some Credit给我一些荣誉_数据集-阿里云…

Python 分析HTTP的可靠性

在这篇文章中,我们将介绍如何使用 Python 来分析代理服务提供商的可靠性。代理服务在许多场景中都非常有用,例如突破地理限制、保护隐私和提高网络安全性。然而,并非所有的代理服务提供商都是可靠的。因此,我们将使用 Python 来测…

存储方案作为产品——Midgard探索

作者 | greatstone94 导读 互联网业务大多是围绕数据展开,获取、生产数据,投入到产品中为用户服务。百度的搜索业务正是典型的数据密集业务,数据规模大,使用方式多样,极为关注如何构建高效低成本的存储系统。 然而软硬…

算法工程题(非递减顺序 排列)

* 题意说明: * 给你两个按 非递减顺序 排列的整数数组 nums1 和 nums2,另有两个整数 m 和 n , * 分别表示 nums1 和 nums2 中的元素数目。 * 请你 合并 nums2 到 nums1 中,使合并后的数组同样按 非递减顺序 排列。…

flex实现瀑布流布局可用于uniapp项目和vue项目

flex布局实现瀑布流 介绍 flex实现瀑布流布局,可用于vue项目,uniapp项目。 实现方式 将列表分为两列或者多列,在循环时分别处理列表数据显示。 预览地址 http://diy.hsycms.com/waterfall/index.htmlhttps://gitee.com/link?targethttp%3A%2F%2Fd…

gitlab升级

1.下载需要的版本 wget -c https://mirrors.tuna.tsinghua.edu.cn/gitlab-ce/yum/el7/gitlab-ce-15.7.6-ce.0.el7.x86_64.rpm --no-check-certificate gitlab-ce-15.4.6-ce.0.el7.x86_64.rpm gitlab-ce-15.7.6-ce.0.el7.x86_64.rpm gitlab-ce-15.9.7-ce.0.el7.x86_64.rpm g…

node 如何下载任意版本

开门见山啦 第一步:打开node官网 Node.js 第二步:点击下载 进入下面的页面,然后往下滑,点击 All download options 查看以往所有的版本号: 这样就可以按自己的需求下载对应的node版本啦 或者 : 最简单…