javascript原型、原型链、继承详解

news2024/12/24 11:43:04

一、原型和原型链的基本概念

在JavaScript中,每个对象都有一个原型对象(prototype)。原型对象就是一个普通的对象,在创建新对象时,可以将该对象作为新对象的原型。原型对象可以包含共享的属性和方法,这些属性和方法可以被新对象继承和访问。对象之间通过原型链(prototype chain)互相关联,形成了一个原型的链条。

当访问对象的属性或方法时,JavaScript会首先在对象本身查找,如果找不到,就会沿着原型链向上查找,直到找到对应的属性或方法,或者到达原型链的顶层(Object.prototype)

下面是使用数组创建原型和原型链的示例

在JavaScript中,数组是内置的对象。它的构造函数是Array,所有的数组对象都是通过Array构造函数创建的。所以,我们可以将Array构造函数看作是数组对象的原型。

在创建一个数组时,例如let arr = [1,2,3],实际上是通过Array构造函数创建了一个新的数组对象arr,并将原型链与Array.prototype关联起来。也就是说,arr对象的原型是Array.prototype。

如图:
在这里插入图片描述

根据上面的解释和图例展示,可知,arr.proto 和 Array.prototype是相等的

 let arr = [1, 2, 3]
console.log(arr.__proto__ == Array.prototype) // true

可能这里有的小伙伴还有些不太理解,那我换个方式说明一下;
创建数组的方式还可以是这样,
使用Array()构造函数创建数组:

let arr = new Array(1, 2, 3); // 包含多个元素的数组

此时的这一句代码和图例的对应关系如下:

  • Array --Array构造函数
  • arr – arr对象
  • arr_proto__和Array.protoType是同一个对象 – Array的原型对象

那么这是最基本的构造函数、实例、和原型对象之间的关系,那么原型链又是如何产生的呢?

一起来思考一个问题arr对象是通过Array构造函数new出来的。那么Array构造函数又是从哪里来的呢,它会不会也是通过一个构造函数new出来的呢?

答案是肯定的,那么我们如何获得Array构造函数的构造函数呢?
我们可以看看arr对象是如何知道Array是他的构造函数的

console.log(arr.__proto__.constructor.name) // Array

这段代码的含义是获取了数组对象 arr 的原型对象(也就是 Array.prototype 对象),并访问其 constructor 属性的 name 属性。

  • arr.__proto__ 获取了 arr 的原型对象。
  • constructor 是原型对象的一个属性,它指向创建该对象的构造函数,对于数组来说,它指向 Array 构造函数。
  • name 是函数对象的一个属性,表示函数的名字。

因此,该代码的含义是获取 arr 的原型对象的构造函数的名字,对于数组对象来说,该名字应为 “Array”。

所以我们可以用同样的方式知道Array的原型对象的构造函数的名字

console.log(Array.__proto__.constructor.name) // Function

在这里插入图片描述
如图,控制台打印出来的是Function.

那么我们接着上面的图例扩展,如下:
在这里插入图片描述

在以同样的方式来找function原型对象的构造函数是谁

console.log(Function.prototype.__proto__.constructor.name) // Object

在这里插入图片描述

再以同样的方式来找Object原型对象的构造函数是谁

console.log(Object.prototype.__proto__.constructor.name) 

这次控制台报错了,如下:
在这里插入图片描述

说不能从null上获取属性constructor

从这个报错信息可得Object.prototype.__proto__是null

console.log(Object.prototype.__proto__) // null

这就证实了开头我们介绍的原型链的顶层(Object.prototype)
在这里插入图片描述
最后我们再来看下Array原型对象的构造函数是谁

console.log(Array.prototype.__proto__.constructor.name) // Object

打印出来是Object,Array原型对象的构造函数是Object

也就是说
Array.prototype.proto == Object.prototype

最终就形成了如下闭环
在这里插入图片描述
再回到开头,let arr = [1,2,3]

对于给定的arr对象,它的原型链如下:
arr -> Array.prototype -> Object.prototype -> null

也就是说,arr继承自Array.prototype,Array.prototype继承自Object.prototype,而Object.prototype的原型为null。

二、继承

1. 原型链继承

通过上面arr对象的原型链可以知道:
在JavaScript中,每个对象都有一个内部属性[[Prototype]],它指向其继承的原型对象。原型对象也可以拥有自己的原型,通过这种方式形成了原型链。当我们访问一个对象的属性或方法时,JavaScript引擎会先在对象本身查找,如果找不到则继续在其原型对象上查找,直到找到目标属性或方法,或者到达原型链的末尾。

通过原型和原型链的这种机制,我们可以在一个对象中共享属性和方法,这其实就是原型链的继承。

比如此时的arr可以共享Array.prototype上的方法,比如push()方法,它向数组末尾添加一个或多个元素,并返回新的长度:

let arr = [1, 2, 3];
arr.push(4);
console.log(arr); // 输出:[1,2,3,4]

在原型链中,使用 Object.prototype 作为所有对象的原型。所以,数组对象 arr 可以共享 Object.prototype 上的toString方法,结果会返回数组转为字符串后的形式。

let arr = [1, 2, 3];

// 通过原型链访问共享方法
console.log(arr.toString()); // "1,2,3"

原型链继承的优点简单易用,可以继承父类的属性和方法,并且可以向上查找原型链上的属性和方法。但它也存在一些问题比如所有子类实例共享父类的属性和方法,无法向父类的构造函数传递参数,同时如果某个子类实例修改了继承的属性,会影响到其他子类实例

2. 父类构造函数的实例设为子类原型对象实现继承

在JavaScript中,可以通过将父类构造函数的实例设置为子类构造函数的原型来实现继承。这种方式称为原型继承或者借用构造函数。

以下是实现继承的步骤:

  1. 创建父类构造函数,定义父类的属性和方法。
  2. 创建子类构造函数,并在构造函数中调用父类构造函数,使用.call()方法绑定当前子类的this到父类构造函数上,确保子类可以继承父类的属性。
  3. 创建父类的实例,并将该实例赋值给子类构造函数的prototype。这样,子类的prototype就会指向父类的实例,从而实现继承父类的方法。
  4. 在子类构造函数的原型中添加子类特有的属性和方法。

下面是一个例子来说明这个过程:

// 创建父类构造函数
function Animal(name) {
  this.name = name;
}

// 父类方法
Animal.prototype.sayName = function() {
  console.log('My name is ' + this.name);
}

// 创建子类构造函数
function Dog(name, breed) {
  Animal.call(this, name); // 调用父类构造函数,绑定当前子类的this到父类构造函数上
  this.breed = breed; // 子类特有的属性
}

// 将父类的实例赋值给子类构造函数的原型
//Dog.prototype = Object.create(Animal.prototype);

// 父类构造函数的实例
let animal = new Animal()
// 设为子类的原型对象
Dog.prototype = animal
// 修复constructor指针
Dog.prototype.constructor = Dog

// 子类特有的方法
Dog.prototype.bark = function() {
  console.log('Woof!');
}

// 创建子类实例
var myDog = new Dog('Max', 'Labrador');

// 调用继承的父类方法
myDog.sayName(); // 输出 'My name is Max'

// 调用子类特有的方法
myDog.bark(); // 输出 'Woof!'

在上面的示例中,Animal是父类构造函数,Dog是子类构造函数,Dog通过调用Animal构造函数实现继承父类的属性。然后将Animal的实例赋值给Dog的原型,使得Dog可以继承父类的方法。最后可以通过创建Dog的实例来调用继承的父类方法和子类特有的方法。

但是这样的继承存在一个问题,继承过来的实例属性,如果是引用类型,会被多个子类的实例共享这意味着所有的子类实例对于该属性的修改都会影响到其他子类实例。这是因为引用类型的属性存储在堆内存中,并且多个实例共用同一个引用地址。

以下是一个例子来具体说明这个问题:

function Person(name) {
  this.name = name;
  this.hobbies = ['reading', 'swimming'];
}

Person.prototype.sayHello = function() {
  console.log('Hello, my name is ' + this.name);
};

function Student(name, grade) {
  this.grade = grade;
}
Student.prototype = new Person();
Student.prototype.constructor = Student

var student1 = new Student('Alice', 5);
var student2 = new Student('Bob', 6);

// 修改student1的hobbies
student1.hobbies.push('playing basketball');

console.log(student1.hobbies);  // ['reading', 'swimming', 'playing basketball']
console.log(student2.hobbies);  // ['reading', 'swimming', 'playing basketball']

在上面的例子中,我们定义了一个Person构造函数,它有一个name属性和一个hobbies数组属性。然后,我们又定义了一个Student构造函数,它通过调用Person构造函数来继承name属性,并添加了一个grade属性。

接着,我们将Student的原型对象设为一个Person的实例,从而实现了继承。

最后,我们创建了两个Student实例student1student2。然后,我们修改了student1hobbies,添加了一个新的爱好playing basketball。结果发现,student2hobbies数组也被修改了,它也包含了playing basketball

这是因为hobbies是一个数组,是引用类型属性。当student1修改hobbies时,它实际上是修改了父类构造函数的实例中的hobbies数组,而这个实例也被student2共享。所以,student2hobbies也会被修改。

为了解决这个问题,可以使用其他的继承方式,比如原型继承组合继承寄生组合继承等,这些方式避免了引用类型属性被共享的问题。

3. 寄生组合继承

1. call和apply用法介绍

在JavaScript中,callapply是两个用于调用函数的方法。

call方法的语法是:function.call(thisArg, arg1, arg2, ...ARGUMENTS)。它接收一个参数列表,并将每个参数传递给函数。第一个参数thisArg是可选的,用于指定函数中的this值。如果不传递thisArg,默认为全局对象(在浏览器中是window对象)。call方法会立即调用函数。

例如,考虑下面的例子:

function greet(name) {
  console.log(`Hello, ${name}! My name is ${this.name}.`);
}

const person = {
  name: 'Alice'
};

greet.call(person, 'Bob');

在这个例子中,call方法将person对象作为第一个参数传递给了greet函数。函数执行后,this.name将会是person对象的name属性。输出将会是Hello, Bob! My name is Alice.

apply方法的语法是:function.apply(thisArg, [argsArray])。它与call方法类似,不同之处在于它接收一个包含多个参数的数组作为参数列表。apply方法也会立即调用函数。

例如,考虑下面的例子:

function add(a, b) {
  return a + b;
}

const numbers = [3, 4];

console.log(add.apply(null, numbers));

在这个例子中,apply方法将numbers数组作为第二个参数传递给了add函数。函数执行后,ab将分别为34,并返回它们的和7

总结一下,callapply方法都用于调用函数,并且允许绑定函数中的this值。它们的主要区别在于传递参数的方式不同:call方法接收参数列表,而apply方法接收参数数组。

2. 完美继承(寄生组合)

在JavaScript中,我们可以使用callapply方法来实现构造函数之间的继承。这种方式也称为借用构造函数或伪经典继承。

假设有两个构造函数ParentChild,我们想要让Child继承Parent的属性和方法。

首先,创建Parent构造函数:

function Parent(name) {
  this.name = name;
}

Parent.prototype.sayHello = function() {
  console.log(`Hello, my name is ${this.name}`);
}

然后,创建Child构造函数,我们可以使用callapply方法来继承Parent的属性和方法。

function Child(name, age) {
  // 继承属性
  Parent.call(this, name); // 组合式继承
  this.age = age;
}

// 继承方法
Child.prototype = Object.create(Parent.prototype); // 寄生继承


Child.prototype.sayAge = function() {
  console.log(`I am ${this.age} years old`);
}

在上述例子中,通过 Parent.call(this, name); 使用了call方法,在Child构造函数中调用了Parent构造函数,并将this关键字指向Child对象,这样Child对象就拥有了Parent构造函数的属性。

通过 Child.prototype = Object.create(Parent.prototype);,我们创建了一个空对象作为Child的原型,并将Parent的原型作为新对象的原型,这样Child对象就能够访问到Parent原型上的方法了。

这一步其实也就是所谓的寄生继承,可以拆解为:

function Temp(){} // 临时构造函数
Temp.prototype = Parent.prototype
let childPrototype = new Temp()
Child.prototype = childPrototype
childPrototype.constructor = Child

最后,我们可以创建Child对象并调用其方法:

let child = new Child('Alice', 20);
child.sayHello(); // 输出:Hello, my name is Alice
child.sayAge();  // 输出:I am 20 years old

通过使用callapply方法组合继承继承属性,寄生继承继承方法,我们成功实现了Child构造函数继承了Parent构造函数的属性和方法。

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

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

相关文章

Appium自动化-ADB连接手机提示unauthorized

目录 开头: 问题: 调研: 重启大法 终极大法 总结: 开头: 当使用ADB(Android Debug Bridge)连接手机时,如果提示"unauthorized"(未授权)错误&a…

javaee HttpSessionListener监听器统计在线人数

先创建ServletContextListener 在全局对象application中设置count属性 package com.yyy.listener;import java.util.ArrayList;import javax.servlet.ServletContext; import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; import javax…

易基因|表观遗传学与脑卒中:DNA甲基化的作用及衰老对血脑屏障修复的影响

大家好,这里是专注表观组学十余年,领跑多组学科研服务的易基因。 脑卒中(俗称中风)是导致死亡和长期残疾的主要原因,尤其是对于老龄人来说。脑卒中的平均生存时间为6-7年,许多患者存在身体残疾和晚期认知功…

技术岗/算法岗面试如何准备?5000字长文、6个角度以2023秋招经历分享面试经验

技术岗/算法岗面试流程是什么样的?技术面都干什么?Coding 机试如何准备?技术面考察哪些知识,如何准备?项目八股如何准备?简历要注意什么?怎么做? 大家好,我是卷了又没卷…

uniapp 适配全面屏

1、manifest.json 文件修改 app-plus 下 添加 "safearea": {"background": "#00000000","bottom": {"offset": "auto"}},2、部分页面设置全屏(登录页面) methods: {//设置页面全屏onShow(…

SpringBoot(二)starter介绍

做Java后端的同学可能都知道,在SpringBoot诞生之前,还有传统的Spring。这种Spring项目想要运行,需要导入各种依赖,而且还要在 XML 配置文件中一顿配置,非常痛苦。但通过上篇博客我们可以看到,SpringBoot项目…

事务与隔离级别

事务四要素 原子性(Atomicity):要么全部完成,要么全部不完成;一致性(Consistency):一个事务单元需要提交之后才会被其他事务可见;隔离性(Isolation&#xff…

azure databricks因为notebook 日志打多或者打印图片太多,往下拉卡死怎么处理

1、同事碰到个问题,databricks 页面卡死不动了 2、我。。。。。。。。测试了下搞不定,找azure的工程师,特此笔记如下图 !](https://img-blog.csdnimg.cn/5db9756d0e224d15a9a607561b47591f.png)

怎样自定义starter模块和使用

一、 自定义Starter模块 在实际开发中,经常会定义一些公共组件,提供给各个项目团队使用。而在SpringBoot的项目中,一般会将这些公共组件封装为SpringBoot的starter(起步依赖)。 1.1实例–阿里云OSS的配置 前言:我们在使用阿里云…

windows下MySQL 5.7.31的安装

文章目录 安装步骤检查是否安装成功配置环境变量 安装步骤 双击安装包mysql-installer-community-5.7.31.0.msi选择自定义安装“custom”,点击next “Developer Default”是开发者默认“Server only”仅作为服务器安装“Clientonly”仅作为客户端安装“Full”是完…

华为OD机试真题 Python 实现【红黑图】【2023Q1 200分】,附详细解题思路

一、题目描述 众所周知红黑树是一种平衡树,它最突出的特性就是不能有两个相邻的红色节点。 那我们定义一个红黑图,也就是一张无向图中,每个节点可能有红黑两种颜色,但我们必须保证没有两个相邻的红色节点。 现在给出一张未染色的…

晶振的作用,高速晶振优缺点

前言 (1)我们都知道晶振是一款MCU的心脏,因为长期用这种抽象的概念进行解释,导致很多人不知道这个心脏的实际作用。因此,我在这里详细的介绍一下晶振对于MCU的实际作用。 (2)接下来我将会在MCU处…

一零六一、Jupyter notebook文件默认路径修改方法

1 .打开 Anaconda Prompt,输入命令 jupyter notebook --generate-config 根据上面运行处的路径打开 C:\Users\WW.jupyter\jupyter_notebook_config.py文件,可以使用记事本打开。 2 .直接CtrlF 搜索 ,找到 #c.NotebookApp.notebook_dir ‘’…

Spring 2023面试题(2)--Spring mvc 运行流程

1. Spring MVC的运行流程主要包括以下步骤: 用户发送请求到前端控制器(DispatcherServlet)。前端控制器接收到请求后,初始化处理器映射器(HandlerMapping)和处理器适配器(HandlerAdapter&#…

Crypto(小学期培训)

你被骗了 url编码直接解 进入网站、找到时间 flag{2020-01-01 07:43:23} 梅开二度 凯撒密码 y和h相差17 三羊开泰 词频分析 这是??? 010编辑器打开 上面是密文 下面是密码表64个,想到base64 找到原来的数值 与之对应&a…

JVM GC ROOT分析与垃圾收集器原理分析(三)

目录 一、GC ROOT 1、虚拟机栈中的本地变量 2、static 成员 3、常量引用 4、本地方法栈中的变量 5、类加载器 6、线程 二、回收算法 1、标记和清除 2、复制算法 3、标记整理 三、垃圾收集器 1、新生代-复制算法 2、老年代-标记清除/整理 3、垃圾收集器分类 1、…

Python如何向一个空列表中append列表

最近在做回溯组合问题时–力扣链接,遇到了向一个空列表中append多个列表。 于是,我原来的代码是: def main(n,k):result []temp []def backtrack(n,k,startIndex):if(len(temp)k):result.append(temp)returnfor i in range(startIndex,n1)…

group by rollup

group by rollup rollup与group by组合使用可对分组结果进行进一步的汇总(相当于对分组结果加一行小计)。 mysql : SELECT CASE WHEN GROUPING(姓名)1 THEN 总计 ELSE 姓名 END AS 姓名, SUM(数值1) 统计数值1, SUM(数值2) 统计数值2 FROM 表名 GROUP BY 姓名 WITH ROLLUPora…

SpringCloud微服务项目实战(一)---搭建SpringBoot项目

目录 SpringBoot与SpringCloud的关系环境配置开始搭建SpringBoot项目问题总结 SpringBoot与SpringCloud的关系 Spring Boot主内,能够快速搭建,快速开发单个微服务,搞定了数据层访问、RESTful 接口、日志组件、内置容器等等基础功能Spring Cl…

shardingsphere-proxy 实现mysql单库分表

1、docker安装mysql5.7版本 拉取mysql的镜像 docker pull mysql:5.7创建mysql的配置目录,日志目录,数据存储的目录 mkdir -p /home/sunyuhua/docker/mysql/conf mkdir -p /home/sunyuhua/docker/mysql/logs mkdir -p /home/sunyuhua/docker/mysql/dat…