带你搞懂JavaScript中的原型和原型链

news2025/2/4 8:54:18

简介

原型和原型链是JavaScript中与对象有关的重要概念,但是部分前端开发者却不太理解,也不清楚原型链有什么用处。其实,学过其他面对对象语言的同学应该了解,对象是由类生成的实例,类与类之间有继承的关系。在ES6之前,JavaScript中并没有class,实现类和继承的方法就是使用原型。在我个人看来,JS中类和原型链的设计和语法由于一些历史或包袱问题而不易用,也不易于理解。因此在ES6中推出了class相关的语法,和其他语言更接近,也更易用。

ES6的class可以看作只是一个语法糖,它的绝大部分功能,ES5都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。(ECMAScript6入门教程 阮一峰)

虽然有了class,但是原型链相关的内容我们依然要掌握。不仅是因为作为前端开发者,我们要深入理解语法。而且在查看源码,以及实现一些复杂的面对对象写法时,依然是有用的。因此在这篇文章中,我们一起搞懂JavaScript中的原型和原型链。(这篇文章并不会涉及class相关语法)

构造函数与原型

构造函数

在JS中创建实例的方法是通过构造函数。在构造函数中通过this实现对实例的操控,比如赋值各种属性和方法。我们看个例子:

// Person构造函数
function PersonFun(name) {
  this.name = name;
  this.getName = function() {
    return this.name;
  }
}
// 创建实例
const p1 = new PersonFun('jz');
console.log(p1.name, p1.getName());
// 输出结果:
// jz jz

我们创建了PersonFun构造函数,使用new关键字创建了实例p1。可以看到,在构造函数中对this增加了属性和方法,最后成为了实例的属性。注意构造方法必须使用new调用。但是这样所有的属性都是实例属性,包括那个getName方法:

const p1 = new PersonFun('jz');
const p2 = new PersonFun('jz');
console.log(p1.getName === p2.getName);
// 输出结果:
// false

原型对象

只用上面的构造函数,依然没有“类”的存在。这时候我们增加原型这一概念,可以理解为是实例对象的类。原型对象可以通过构造函数的prototype属性访问。

// Person构造函数
function PersonFun(name) {
  this.name = name;
}
// Person原型对象
PersonFun.prototype.getName = function() {
  return this.name;
}
// 创建实例
const p1 = new PersonFun('jz');
const p2 = new PersonFun('jz');
console.log(p1.name, p1.getName());
console.log(p1.getName === p2.getName);
// 输出结果:
// jz jz
// true

可以看到,我们没有在构造函数中添加实例对象的属性方法getName,仅仅在原型对象上添加。但实例对象上依然能使用属性方法getName,而且对于不同的实例来说,这个方法是共享的,是同一个。通过原型,我们不仅能共享方法名也能共享属性值:

// Person原型对象
PersonFun.prototype.title = 'hello';
const p1 = new PersonFun('jz');
const p2 = new PersonFun('jz');
console.log(p1.title, p1.title);
p1.title = '你好'
console.log(p1.title, p2.title);
// 输出结果:
// hello hello
// 你好 你好

可以看到,在实例中修改原型上提供的属性,实际上是增加实例中的属性值,因此这个修改是不在实例中共享的。但如果原型提供的属性是个对象,我们修改对象内部的值,这个值是实例间共享的。

PersonFun.prototype.obj = {};
p1.obj.a = 1;
console.log(p2.obj.a);
// 输出结果:
// 1

构造函数/原型的获取

通过上面的描述,我们了解了实例,构造函数和原型对象以及他们之间的关系。那么在代码中,如何获取构造函数和原型对象呢?我们列出了一些方法:

// 构造函数 PersonFun
// 获取原型对象
Person = PersonFun.prototype
// 创建实例对象
p1 = new PersonFun()

// 原型对象 Person
// 获取构造函数
PersonFun = Person.constructor

// 实例对象 p1
// 获取原型对象
Person = p1.__proto__
Person = Object.getPrototypeOf(p1)
// 获取构造函数
PersonFun = p1.constructor

其中的__proto__最好使用Object.getPrototypeOf代替:

__proto__并不是语言本身的特性,这是各大厂商具体实现时添加的私有属性,虽然目前很多现代浏览器的JS引擎中都提供了这个私有属性,但依旧不建议在生产中使用该属性,避免对环境产生依赖。(ECMAScript6入门教程 阮一峰)

字面量的原型

字面量对象的原型

如果我们创建对象的时候,没有使用构造函数,而是直接是用大括号,以字面量的形式创建的对象,那么它的原型是什么?它有没有构造函数呢?是有的。我们一起来看一下:

const obj = { a: 1 };
console.log(obj.__proto__)
console.log(obj.constructor)
// 输出结果:
// {constructor: ƒ, __defineGetter__: ƒ, …}
// ƒ Object() { [native code] }

输出了一些奇怪的东西,我们还是不知道字面量对象的原型是什么。我们换个角度想一想,以字面量形式创建的对象,是不是就相当于直接使用new Object()形式创建的对象?这里的Object也是一个构造函数。我们来试验下:

const obj1 = { a: 1 };
const obj2 = new Object({ b: 2 });
console.log(obj1.__proto__ === obj2.__proto__)
console.log(Object.prototype === obj1.__proto__)
console.log(obj1.constructor === Object)
// 输出结果:
// true
// true
// true

可以看到,使用字面量形式和new Object()形式,创建出来对象的原型是一样的。既然Object是个构造函数,那么Object.prototype即是Object实例的原型对象。

至于obj.constructor实际上就是构造函数Object()。它是JS内部生成的,因此这里展示[native code]

new Function()

在JS中函数实际上也是个对象。既然它是个对象,那么它应该也有构造函数和原型吧。我们来试验下:

// Person构造函数
function PersonFun(name) {
  this.name = name;
}
console.log(PersonFun.__proto__)
console.log(PersonFun.constructor)
// 输出结果:
// ƒ () { [native code] }
// ƒ Function() { [native code] }

又输出了一些奇怪的东西,其中还有个Function()。我们继续联想下:对象可以用new Object()形式创建,那么函数是不是也可以?可以的!使用new Function()可以创建函数。我们来看下:

const fun = new Function('a', 'b', 'return a + b');
console.log(fun(1, 2));
// 输出结果:
// 3

new Function()可以使用字符串作为代码执行的函数体,感觉有点像eval。但是eval是局部作用域,new Function()一直都是全局作用域。我们来看下例子:

const a = 1;
function envir() {
  const a = 2;
  eval('console.log(a)');
  const fun = new Function('console.log(a)');
  fun();
}
envir();
// 输出结果:
// 2
// 1

可以看到,eval输出的是局部作用域中的a值2,而new Function()虽然在局部作用域的位置中,但是内部获取到的依然是全局的变量。不过这些区别和我们要讨论的原型链无关,因此不再继续讨论。

字面量函数的原型

了解了new Function(),我们再回来看看字面量函数的原型。

// Person构造函数
function PersonFun(name) {
  this.name = name;
}
const fun = new Function('a', 'b', 'return a + b');
console.log(PersonFun.__proto__ === fun.__proto__)
console.log(PersonFun.__proto__ === Function.prototype)
console.log(PersonFun.constructor === Function)
// 输出结果:
// true
// true
// true

与对象类似,Function.prototype是函数的原型,我们函数字面量的原型都是它。函数的构造函数即是Function()。(构造函数与普通函数并无区别,都是函数)。在上面的输出中,函数的原型对象Function.prototype也是一个函数:ƒ () { [native code] }。关于这点我们会在后面讨论。

JS中的原型关系

了解了字面量相关的原型,现在我们再来刨根问底,看看JS中对象的原型关系。

对象的原型关系

首先看下Object原型的关系。

对象的尽头

首先看看对象的尽头。上面讲过字面量对象的原型即是Object.prototype。它也是个对象,那么它有没有原型呢?我们试一下:

const obj = { a: 1 };
console.log(obj.__proto__)
console.log(obj.__proto__.__proto__)
// 输出结果:
// {constructor: ƒ, __defineGetter__: ƒ, …}
// null

答案是没有的,Object.prototype是没有原型的。

自定义构造函数与原生对象的关系

我们的自定义构造函数与对应的实例原型和Object.prototype有关系么?我们试验下:

// Person构造函数
function PersonFun(name) {
  this.name = name;
}
// Person原型对象
PersonFun.prototype.getName = function() {
  return this.name;
}
console.log(PersonFun.prototype.__proto__);
console.log(PersonFun.prototype.__proto__ === Object.prototype);
console.log(PersonFun.prototype.constructor === Object);
// 输出结果:
// {constructor: ƒ, __defineGetter__: ƒ, …}
// true
// false

可以看到,Person构造函数对应实例的原型对象,它的原型即是Object.prototype。但是它与字面量对象不同的是,它的constructor属性表示的是它对应实例的构造函数,而不是字面量对象的Object()

原生类型的原型关系

在前面我们聊过了函数的原型,即是Function.prototype。但当时我们输出它,发现它是一个函数,那么它究竟是什么?它还有没有原型?

// Person构造函数
function PersonFun(name) {
  this.name = name;
}
console.log(PersonFun.__proto__);
console.log(PersonFun.__proto__.__proto__);
console.log(PersonFun.__proto__.__proto__ === Object.prototype);
console.log(PersonFun.__proto__.prototype);
// 输出结果:
// ƒ () { [native code] }
// {constructor: ƒ, __defineGetter__: ƒ, …}
// true
// undefined

可以看到,直接打印函数的原型也是一个函数,里面是[native code],即它也是由JS内部生成的。它的再深一层原型,居然又是Object.prototype。函数的原型虽然也是个函数,但是它并没有更深一层的prototype。

这时候我们返回去看看对象原型的构造函数,即Object()。作为一个函数,它的原型是什么?

console.log(Object);
console.log(Object.__proto__);
console.log(Object.__proto__ === Function.prototype);
// 输出结果:
// ƒ Object() { [native code] }
// ƒ () { [native code] }
// true

看来这些原生类型的构造函数的原型,都同一个来源。我们再试一下其他的原生类型:

console.log(Number);
console.log(Number.__proto__);
console.log(Number.__proto__ === Function.prototype);
console.log(Array);
console.log(Array.__proto__);
console.log(Array.__proto__ === Function.prototype);
console.log(String);
console.log(String.__proto__);
console.log(String.__proto__ === Function.prototype);
new Function.prototype();
// 输出结果:
// ƒ Number() { [native code] }
// ƒ () { [native code] }
// true
// ƒ Array() { [native code] }
// ƒ () { [native code] }
// true
// ƒ String() { [native code] }
// ƒ () { [native code] }
// true
// Uncaught TypeError: Function.prototype is not a constructor

果然如此,原生类型的构造函数的原型都是同一个。而如上面实验得出的结论,这个原型是一个函数,它没有构造函数,它的原型是Object.prototype。我还尝试直接用这个构造函数原型创建实例,结果提示这不是一个构造函数。

原型链

有了上面这些关系,我们发现不同类型对象的原型似乎都是有关系的,好像有一条线可以把他们穿起来。这条线就是我们所说的原型链。在文章一开始的简介中说过,原型和原型链是JavaScirpt中实现类和继承的一种方式。原型就相当于实例的类,继承就像是原型链。因此,原型的特点也很像父类,即实例可以访问原型的属性,也可以覆盖原型属性。在浏览器的控制台中,我们打印一个对象,展示的[[Prototype]]即是它的原型。

原型链示意图

不仅我们自定义的类型有原型链的关系,JS内部的原生类型也存在原型链,且可以和我们自定义的类型串起来。这里我们用一个图片描述原型链之间的关系(图片来源MollyPages.org):

在这里插入图片描述

原型链文字版

假设我们有这样一些对象,我们来搞清楚它们的原型关系。

  • 构造函数 PersonFun
  • 实例对象 p1
  • 原型对象(类) Person
// 构造函数 生成 实例对象
p1 = new PersonFun()
// 构造函数 -> 原型对象
Person = PersonFun.prototype

// 实例对象 -> 构造函数
PersonFun = p1.constructor
// 实例对象 -> 原型对象
Person = p1.__proto__
Person = Object.getPrototypeOf(p1) // 推荐

// 原型对象 -> 构造函数
PersonFun = Person.constructor

再看一下字面量的原型关系,以及更深层次的关系:

  • 字面量对象 obj1
  • 字面量函数 fun1
// 字面量对象 -> Object构造函数
Object = obj1.constructor
// 字面量对象 -> Object原型
Object.prototype = obj1.__proto__
// Object原型的原型 为 null
null = Object.prototype.__proto__

// 字面量函数 -> Function构造函数
Function = fun1.constructor
// 字面量函数 -> Function原型
Function.prototype = fun1.__proto__
// Function原型作为一个构造函数时的实例原型 -> undefined
undefined = Function.prototype.prototype
// Function原型的原型 为 Object原型
Object.prototype = Function.prototype.__proto__

然后我们就可以完整的得到原型链:

// 构造函数链
// 实例对象 -> 构造函数 -> Function构造函数
PersonFun = p1.constructor
Function  = p1.constructor.constructor
Function  = p1.constructor.constructor.constructor

// 原型对象链
// 实例对象 -> 原型对象 -> Object原型 -> null
Person            = p1.__proto__
Object.prototype  = p1.__proto__.__proto__
null              = p1.__proto__.__proto__.__proto__

// 字面量对象的原型对象链
// 字面量函数 ->  Object原型 -> null
Object.prototype  = obj1.__proto__
null              = obj1.__proto__.__proto__

// 字面量函数的原型对象链
// 字面量函数 -> Function原型 -> Object原型 -> null
Function.prototype  = fun1.__proto__
Object.prototype    = fun1.__proto__.__proto__
null                = fun1.__proto__.__proto__.__proto__

// Number对象的原型链
const n1 = new Number(1);
// Number对象 -> Number原型 -> Object原型 -> null
Number.prototype  = n1.__proto__
Object.prototype  = n1.__proto__.__proto__
null              = n1.__proto__.__proto__.__proto__

总结

通过原型链,我们可以了解JS中一些原生对象的原理和机制,比如为什么Function的实例也是对象,Number的实例也是对象,因为这些对象的原型都继承了Object原型,因此可以使用对象类型的方法。

使用原型链,也可以实现很多类的继承模式,后面有机会我们可以讨论一下。总体看来,虽然使用原型链确实可以实现类和继承的等面对对象特性,但是相比于其他语言更晦涩且不容易理解。

参考

  • Class的基本语法 ECMAScript6入门教程 阮一峰
    https://es6.ruanyifeng.com/#docs/class
  • Class 的继承 ECMAScript6入门教程 阮一峰
    https://es6.ruanyifeng.com/#docs/class-extends
  • 一文搞懂JS原型与原型链(超详细,建议收藏)
    https://juejin.cn/post/6984678359275929637
  • 你可能不太理解的JavaScript - 原型与原型链
    https://juejin.cn/post/7254443448563040311
  • js从原型链到继承——图解来龙去脉
    https://juejin.cn/post/7075354546096046087
  • Javascript Object Layout
    http://www.mollypages.org/tutorials/js.mp

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

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

相关文章

html网页设计 01marquee标签广告滚动(1)

<!DOCTYPE html> <html><head><meta charset"utf-8"><title></title></head><body><!-- scrollamount:数字越大&#xff0c;滚动越快direction:滚动方向滚动的类型behaior"slide",文字滚动到边界后就会…

Redis分布式缓存超详细总结!

文章目录 前言一、Redis持久化解决数据丢失问题1.RDB&#xff08;Redis Database Backup file&#xff09;持久化&#xff08;1&#xff09;执行RDB&#xff08;2&#xff09;RDB方式bgsave的基本流程&#xff08;3&#xff09;RDB会在什么时候执行&#xff1f;save 60 1000代表…

Vulnhub-DC-9 靶机复现完整过程

一、搭建环境 kali的IP地址是&#xff1a;192.168.200.14 DC-9的IP地址暂时未知 二、信息收集 1、探索同网段下存活的主机 arp-scan -l #2、探索开放的端口 开启端口有&#xff1a;80和22端口 3、目录扫描 访问80 端口显示的主页面 分别点击其他几个页面 可以看到是用户…

13、C++异常处理

13、c异常处理 抛出异常捕获异常未抛出异常时的流程抛出异常时的流程捕获异常匹配顺序异常说明异常处理构造函数中的异常析构函数中的异常标准库异常类 抛出异常 throw 异常对象可以抛出基本类型的对象&#xff0c;如:throw -1;throw "内存分配失败!";也可以抛出类类…

【C语言:动态内存管理】

文章目录 前言1.malloc2.free3.calloc4.realloc5.动态内存常见错误6.动态内存经典笔试题分析7.柔性数组8.C/C中的内存区域划分 前言 文章的标题是动态内存管理&#xff0c;那什么是动态内存管理&#xff1f;为什么有动态内存管理呢&#xff1f; 回顾一下以前学的知识&#xff…

C++ Core Guidelines解析 ( 好书推荐 )

C Core Guidelines是Bjarne和 Herb Sutter发起编写的一个开源项目&#xff0c;汇聚了 C社区多年来积累的宝贵经验&#xff0c;是非常全面的编程最佳实践指导&#xff0c;包括代码风格、函数、类、错误处理、性能优化等&#xff0c;可以说是C社区的集大成者。用Effective Modern…

电脑主板支持的cpu型号汇总

一、如何选择不同的主板和对应CPU 1、看针脚&#xff1a;网上有相应的参数&#xff0c;只要CPU能安装到主板中&#xff0c;基本就兼容&#xff0c;这主要取决CPU插槽和主板插槽十分一致。 2、看型号&#xff1a;桌面处理器&#xff0c;只有Intel和AMD两大平台&#xff0c;他们对…

dToF直方图之美_deadtime死区时间

上节在激光雷达多目标测距中有个问题为什么激光雷达不用做pile up算法,有人会有疑问,我看过很多人的简历,都把pile up量产校正算法写为最为自豪重要的算法攻坚,可能会吸引一波人的眼球。这要是在两三年前是值得被肯定的,但是如今随着dToF非常多量产项目落地,pile up研究不…

【Hive】——安装部署

1 MetaData&#xff08;元数据&#xff09; 2 MetaStore &#xff08;元数据服务&#xff09; 3 MetaStore配置方式 3.1 内嵌模式 3.2 本地模式 3.3 远程模式 4 安装前准备 <!-- 整合hive --><property><name>hadoop.proxyuser.root.hosts</name><v…

BUUCTF-[GYCTF2020]FlaskApp flask爆破pin

这道题不需要爆破也可以getshell ssti都给你了 {{((lipsum.__globals__.__builtins__[__import__](so[::-1])[popen]("\x63\x61\x74\x20\x2f\x74\x68\x69\x73\x5f\x69\x73\x5f\x74\x68\x65\x5f\x66\x6c\x61\x67\x2e\x74\x78\x74")).read())}} 但是学习记录一下pin…

ETLCloud详解,如何实现最佳实践及问题排查

ETLCloud介绍 ETLCloud是新一代全域数据集成平台&#xff0c;领先于市场同类产品的数据集成平台(DataOps)&#xff0c;只需单击几下即可完成数据清洗转换、传输入仓等操作&#xff0c;具备高效、智能、一站式的全域数据集成优势&#xff0c;如&#xff1a; 毫秒级实时数据同步 …

MMLM之Gemini:《Introducing Gemini: our largest and most capable AI model》的翻译与解读

MMLM之Gemini&#xff1a;《Introducing Gemini: our largest and most capable AI model》的翻译与解读 导读&#xff1a;2023年12月6日&#xff0c;Google重磅发布大规模多模态模型Gemini&#xff0c;表示了Google语言模型发展到了一个新阶段&#xff0c;其多模态和通用能力明…

力扣1445 连续字符

目录 ​编辑 题目 示例 示例1 示例2 提示 详细解读 题目 给你一个字符串 s &#xff0c;字符串的「能量」定义为&#xff1a;只包含一种字符的最长非空子字符串的长度。 请你返回字符串 s 的 能量。 解题思路 这个问题的解法相对比较简单&#xff0c;可以通过遍历字…

int(1) 和 int(10) 的区别

int(1) 和 int(10) 的区别 最近遇到个问题&#xff0c;有个表的要加个user_id字段&#xff0c;user_id字段可能很大&#xff0c;于是我提mysql工单alter table xxx ADD user_id int(1)。领导看到我的sql工单&#xff0c;于是说&#xff1a;这int(1)怕是不够用吧&#xff0c;接…

Python finally-资源回收

有些时候&#xff0c;程序在 try 块里打开了一些物理资源&#xff08;例如数据库连接、网络连接和磁盘文件等&#xff09;&#xff0c;这些物理资源都必须被显式回收。 Python 的垃圾回收机制不会回收任何物理资源&#xff0c;只能回收内存中对象所占用的内存。 那么在哪里回收…

【智能家居】八、监控摄像采集、人脸识别比对进行开门功能点

一、使用 fswebcam 测试 USB 摄像头 二、根据demo来实现功能点 三、功能点编写编译运行实现 一、使用 fswebcam 测试 USB 摄像头 a. 安装 fswebcam orangepiorangepi:~$ sudo apt update orangepiorangepi:~$ sudo apt-get install -y fswebcamb. 安装完 fswebcam 后可以使用…

036.Python面向对象_self_cls_super

我 的 个 人 主 页&#xff1a;&#x1f449;&#x1f449; 失心疯的个人主页 &#x1f448;&#x1f448; 入 门 教 程 推 荐 &#xff1a;&#x1f449;&#x1f449; Python零基础入门教程合集 &#x1f448;&#x1f448; 虚 拟 环 境 搭 建 &#xff1a;&#x1f449;&…

AMC8美国数学竞赛历年真题集在线练习操作指南和2024年备考建议

今天是2023年12月10日&#xff0c;距离2024年的AMC8美国数学竞赛的举办还有40天时间。据六分成长了解&#xff0c;有一些孩子报名参加了AMC8的机构培训班系统学习&#xff0c;也有一些孩子选择了自己自学备考。 有家长问AMC8的培训是否一定要参加机构的培训班学习&#xff1f;…

Aho Corasick Algorithm

文章目录 前言介绍实现参考 前言 Aho Corasick Algorithm又叫AC自动机&#xff0c;该算法是一个匹配算法&#xff0c;用来匹配文本Text中多个patterns分别出现的次数&#xff1b; 我们定义n为patterns的总长度&#xff1b;m为Text的长度&#xff1b; 问题&#xff1a;在ahis…

C语言算法与数据结构,旅游景区地图求最短路径

背景&#xff1a; 本次作业要求完成一个编程项目。请虚构一张旅游景区地图&#xff0c;景区地图包括景点&#xff08;结点&#xff09;和道路&#xff08;边&#xff09;&#xff1a;地图上用字母标注出一些点&#xff0c;表示景点&#xff08;比如&#xff0c;以点 A、B、C、…