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

2023-10-27

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

在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构造函数的属性和方法。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

javascript原型、原型链、继承详解 的相关文章

随机推荐

  • PHP实现一个简单的登录和注册,以及实现方法和页面

    下面是一个简单的PHP代码示例 实现了登录和注册功能 首先 创建一个名为index php的文件 用于显示登录和注册表单 h2 登录 h2
  • Linux 安装JDK17

    1 官网下载JDK17 这里我们下载的是 x64 Compressed Archive版本 2 解压tar 文件 进入文件下载目录 自己定义 我这里都放在software cd usr local softwar 解压tar文件 tar v
  • 【毕业项目】自主设计HTTP

    博客介绍 运用之前学过的各种知识 自己独立做出一个HTTP服务器 自主设计WEB服务器 背景 目标 描述 技术特点 项目定位 开发环境 WWW介绍 网络协议栈介绍 网络协议栈整体 网络协议栈细节 与http相关的重要协议 HTTP背景知识补
  • 最强自动化测试框架Playwright(11)- 录制视频

    视频 使用playwright 您可以录制测试视频 录制视频 视频在测试结束时在浏览器上下文关闭时保存 如果手动创建浏览器上下文 请确保等待 browser context close context browser new context
  • 分布式任务调度可选方案

    1 除了基于jvm的java之处 新接触一个JVM语言 SCALA 一种同时面向脚本和面向函数的语言 spark大数据框架是基于scala语言 照着网络教程 简单的写了几个例子 感觉object class与java语境中还是有一定的差异
  • 2023美赛F题完整数据代码模型成品文章思路-- Green GDP

    论文摘要 模型和其他部分内容如下 摘要 现行的以GDP为核心的国民经济核算体系 由于忽略非市场产出 环境破坏 资源浪费方面的有关计算 这样的指标并不完整 由于经济活动中 对资源消耗和对环境的负面影响越来越大 而长期忽略这种负面影响的后果 高
  • Hexo在多台电脑上提交和更新

    文章目录 1 博客搭建 2 创建一个新文件夹new 用于上传hexo博客目录 3 github上创建hexo分支并设置为默认分支 创建hexo分支 将hexo分支设置为默认分支 4 进入新建的文件夹中git clone 再上传相关文件至he
  • navicat for mysql 连接 mysql 出现1251错误

    navicat for mysql下载地址 链接 https pan baidu com s 1Nh2ippFKHrWXnzPx hda8g 密码 fumf 客户端使用navicat for mysql 本地安装了mysql 8 0 但是在
  • CVE-2023-35843:NocoDB任意文件读取漏洞复现

    文章目录 NocoDB 存在任意文件读取漏洞CVE 2023 35843 0x01 前言 0x02 漏洞描述 0x03 影响范围 0x04 漏洞环境 0x05 漏洞复现 1 访问漏洞环境 2 构造POC 3 复现 0x06修复建议 Noco
  • zookeeper选举流程源码分析

    zookeeper选举流程源码分析 选举的代码主要是在QuorumPeer java这个类中 它有一个内部枚举类 用来表示当前节点的状态 public enum ServerState LOOKING FOLLOWING LEADING O
  • 关于vue项目的node、node-sass、sass-loader的版本问题

    后续博客全部搬迁至个人博客 欢迎访问 最近遇到一个问题 在下载vue项目的node modules的包时 node sass和sass loader版本总是不匹配 当两者匹配时 node和node sass版本又不匹配 导致我的服务一直起不
  • 简历中场景

    场景一 消息的发送 接收 利用rabbitmq的单通道模式 实现专家端发送消息 老师端监听消息 流程 subject accept server中producer消息 mq message中consumer监听消息 并保存在数据库中 调用m
  • 安装potobuf(make check通过)

    很多文章中给出的方法是在github上下载项目 然后创建build再安装googletest 但是在最后的make check时一直报错 如果是python中使用 直接sudo pip3 install i https pypi tuna
  • Spring 3整合Quartz 2实现定时任务一:常规整合

    最近工作中需要用到定时任务的功能 虽然Spring3也自带了一个轻量级的定时任务实现 但感觉不够灵活 功能也不够强大 在考虑之后 决定整合更为专业的Quartz来实现定时任务功能 首先 当然是添加依赖的jar文件 我的项目是maven管理的
  • Cannot run program “D:\Environment\jdk1.8\bin\java.exe”解决方法

    Cannot run program D Environment jdk1 8 bin java exe in directory D Project Java Idea project docker springboot CreatePr
  • Scratch的广播与消息

    在事件积木中 有一块触发积木叫当接收到 消息1 对应地 有两块积木 广播 消息1 广播 消息1 并等待 广播 消息机制就是编程中的全局事件 当一个消息被广播时 所有角色 包含广播者自身 都会接收到该消息 只要一个角色有该消息的接收脚本 即可
  • 【Linux】进程程序替换 &&简易mini_shell实现

    文章目录 替换原理 替换函数 替换函数的使用 简易shell实现程序 替换原理 目前 我们使用fork创建子进程 为了用if else让子进程执行父进程代码的一部分 如果想让子进程执行一个全新的程序 进程不变 仅仅替换当前进程的代码和数据
  • python怎么自学

    其实0基础选择python学习入行的不在少数 Python近段时间一直涨势迅猛 在各大编程排行榜中崭露头角 得益于它多功能性和简单易上手的特性 让它可以在很多不同的工作中发挥重大作用 正因如此 目前几乎所有大中型互联网企业都在使用 Pyth
  • 图像识别(九)

    大家好啊 我是董董灿 很多同学在做深度学习时 都会遇到难以理解的算法 SoftMax肯定是其中一个 初学者大都对它一知半解 只知道SoftMax可以用来做分类 输出属于某个类别的概率 但是 为什么要用SoftMax呢 这个算法又是如何将神经
  • javascript原型、原型链、继承详解

    一 原型和原型链的基本概念 在JavaScript中 每个对象都有一个原型对象 prototype 原型对象就是一个普通的对象 在创建新对象时 可以将该对象作为新对象的原型 原型对象可以包含共享的属性和方法 这些属性和方法可以被新对象继承和