彻底搞懂JavaScript原型和原型链

javascript · 浏览次数 : 0

小编点评

**类和对象的关系** 在面向对象的编程语言中,类和对象的关系是 **铸模和铸件的关系**。 * 类是模具,对象是铸件。 * 类是抽象的概念,对象是具体实现。 **原型编程中的类** 原型编程语言并没有直接支持类的概念。对象都是通过克隆另外一个对象而来,这个被克隆的对象就是原型对象。 **原型对象属性** 原型对象属性用于存储通过该构造函数创建的所有实例的共享属性和方法。原型对象可以作为原型对象使用,用于实现继承效果。 **原型链** 原型链是一种从根对象到原型对象的一条链路。对象可以调用其构造函数的 prototype 原型对象的属性和方法原型对象也是对象,同样也可以调用其构造函数的prototype原型对象的属性和方法一层一层的形成一条链路就是原型链。 **原型链的经典图片** ``` Object.prototype / \ / \ | Function | | \ \ \ / \ / / \ | Foo() | Object() | \ / \ | Function | Function | \ \ Object ```

正文

基于原型编程

在面向对象的编程语言中,类和对象的关系是铸模和铸件的关系,对象总是从类创建而来,比如Java中,必须先创建类再基于类实例化对象。
而在基于原型编程的思想中,类并不是必须的,对象都是通过克隆另外一个对象而来,这个被克隆的对象就是原型对象。
基于原型编程的语言通常遵循下面的规则:

  • 所有的数据都是对象
  • 要得到一个对象,不是通过实例化类,而是找到一个对象作为原型并克隆它
  • 无法自己直接创建一个对象,对象都是克隆产生,必须有一个根对象,然后克隆它才可以创建对象
  • 对象会记住它的原型
  • 如果对象无法响应某个请求,它会把这个请求委托给自己的原型

函数和对象

JavaScript是基于原型的语言,但JavaScript并非严格遵循原型编程的第一条规则,其中基本类型undefined、number、string、boolean就不是对象,其模仿Java,分为了基本类型和对象类型,当然和Java一样,基本类型可以使用包装的形式变为对象。
这里先探讨一个问题,有助于后面理解原型和原型链,那就是“所有的数据都是对象,函数也是对象“,虽然函数是对象,但是函数拥有很多普通对象不具备的特性:

  1. 可执行性:函数可以被调用执行
  2. 闭包:函数可以创建闭包
  3. 构造函数:函数可以作为构造函数,去创建对象实例,理论上,除了 Object.create 创建的对象,都是通过构造函数创建的对象。
  4. 原型对象属性:除了箭头函数,每个函数都有一个 prototype 属性,它是一个对象,用于存储通过该构造函数创建的所有实例的共享属性和方法。

JavaScript原型

从上面可以得知,函数都有 prototype 属性,称之为原型,也称为原型对象,原型对象里面可以存放属性和方法,共享给实例对象使用,用于实现继承效果。
这里的函数指的是构造函数,即可以通过new创建对象的函数,在JavaScript中普通函数都可以是构造函数,除了箭头函数。

箭头函数没有 prototype 属性是为了保持它们的设计简洁性,避免与原型链相关的复杂性和潜在问题。箭头函数同时不能使用new实例化对象。如果你需要定义一个构造器函数,应该使用普通函数而不是箭头函数。

举例说明:

const arr = new Array(1, 2, 3)
arr.reverse()
arr.sort()

在上面的代码中 Array 就是一个构造函数,reverse和sort都是 Array.prototype 原型对象上的函数。
这里有个问题,为何要将这些方法放在 prototype 原型属性上,而不是放在构造函数内部呢?
我们来看下面的代码:

function Person() {
  this.getName = function () {
    console.log('person')
  }
}
Person.prototype.getProtoName = function () {
  console.log('proto person')
}
const person = new Person()
const person1 = new Person()

console.log(person.getName === person1.getName) // false
console.log(person.getProtoName === person1.getProtoName) // true

很明显,虽然在实例化对象上都可以调用这些方法,但两者有本质的不同,将方法放在 prototype 原型对象上,之后实例化的对象使用的都是同一个方法,类似于Java的类方法,而构造函数内的方法,则会重新创建,也就是实例对象方法。

JavaScript原型链

了解完原型之后,再来看原型链就非常简单了。

  1. 对象可以调用其构造函数的 prototype 原型对象的属性和方法
  2. 原型对象也是对象,同样也可以调用其构造函数的prototype原型对象的属性和方法
  3. 一层一层的形成一条链路就是原型链

如图所示:

解读:

  1. 在浏览器中,通常使用对象的 __proto__ 即可找到对象的原型对象,每个对象都有 __proto__ 属性(还是要去除Object.create创建的),用于指向它的原型对象。
  2. 前面基于原型编程有一个规则是,必须有一个根对象,JavaScript中根对象是:Object.prototype,其是一个空对象。

原型链经典图片

最后结合上面的内容,来看看下面的经典原型链图:

从上图可以看出左边是实例对象,中间是构造函数,右边是原型对象,图中还包含了一些其它内容:
函数是一个函数,同时也是一个对象:
构造函数作为一个函数时,拥有原型对象属性 prototype
同时函数也是一个对象,函数都是由构造函数 Function 构造出来的,包括 Function 函数本身,可以看上图中 function Foo()function Object()function Function()__proto__ 都是指向 Funtion.prototype,这是函数比较特殊的一点。
这个地方有点绕,再理一下,函数的 prototype 属性是这个函数自己的属性,而函数的 __proto__ 指向的是其构造函数的原型对象,可以理解为父级的属性,两者是不同的。

总结

  1. JavaScript是基于原型编程,创建对象是通过克隆对象的形式,不是通过类创建。
  2. 函数都拥有 prototype 原型属性,实例化对象的 __proto__ 属性指向这个原型属性,对象可以直接调用原型对象的方法和属性,不用写 __proto__ 再调用,两者效果一致。
  3. 对象的 __proto__ 指向构造函数的 prototype,构造函数的 prototype 同样是对象,其 __proto__ 指向上一层原型对象,直到 Object.prototype,形成原型链。

与彻底搞懂JavaScript原型和原型链相似的内容:

彻底搞懂JavaScript原型和原型链

基于原型编程 在面向对象的编程语言中,类和对象的关系是铸模和铸件的关系,对象总是从类创建而来,比如Java中,必须先创建类再基于类实例化对象。 而在基于原型编程的思想中,类并不是必须的,对象都是通过克隆另外一个对象而来,这个被克隆的对象就是原型对象。 基于原型编程的语言通常遵循下面的规则: 所有的数

带你彻底搞懂递归时间复杂度的Master公式

网上找到的Master公式推导过程都太过于复杂了,为此我特地找到一种小白也能看懂的推导过程。看完这篇文章后,你会对递归的时间复杂度深谙于心,打死都不会忘记。

彻底理解闭包实现原理

前言 闭包对于一个长期写 Java 的开发者来说估计鲜有耳闻,我在写 Python 和 Go 之前也是没怎么了解,光这名字感觉就有点"神秘莫测",这篇文章的主要目的就是从编译器的角度来分析闭包,彻底搞懂闭包的实现原理。 函数一等公民 一门语言在实现闭包之前首先要具有的特性就是:First class

手动实现Transformer

Transformer和BERT可谓是LLM的基础模型,彻底搞懂极其必要。Transformer最初设想是作为文本翻译模型使用的,而BERT模型构建使用了Transformer的部分组件,如果理解了Transformer,则能很轻松地理解BERT。 一.Transformer模型架构 1.编码器 (

[转帖]面渣逆袭:二十二图、八千字、二十问,彻底搞定MyBatis!

https://cdn.modb.pro/db/334793 大家好,我是老三,面渣逆袭系列继续,这节我们的主角是MyBatis,作为当前国内最流行的ORM框架,是我们这些crud选手最趁手的工具,赶紧来看看面试都会问哪些问题吧。 基础 1.说说什么是MyBatis? MyBatis logo 先吹

彻底理解Linux的DISPLAY变量的作用

背景 最近遇到个两年前遇到的问题,使用virt-manager提示(virt-manager:873): Gtk-WARNING **: 14:53:28.147: cannot open display: :1,当时专门运维的同事帮忙临时调了下DISPLAY变量,好像是将:1改成了SSH用户本地I

彻底搞清楚vue3的defineExpose宏函数是如何暴露方法给父组件使用

前言 众所周知,当子组件使用setup后,父组件就不能像vue2那样直接就可以访问子组件内的属性和方法。这个时候就需要在子组件内使用defineExpose宏函数来指定想要暴露出去的属性和方法。这篇文章来讲讲defineExpose宏函数是如何暴露出去这些属性和方法给父组件使用。注:本文中使用的vu

彻底搞清楚vue3的defineExpose宏函数是如何暴露方法给父组件使用

众所周知,当子组件使用setup后,父组件就不能像vue2那样直接就可以访问子组件内的属性和方法。这个时候就需要在子组件内使用defineExpose宏函数来指定想要暴露出去的属性和方法

[转帖]彻底理解并解决服务器出现大量TIME_WAIT

https://zhuanlan.zhihu.com/p/567088021?utm_id=0 首先我们说下状态 TIME_WAIT 出现的原因 TCP的新建连接,断开连接的流程和各个状态,如下图所示 由上图可知:TIME_WAIT 是主动断开连接的一方会出现的,客户端,服务器都有可能出现 当客户端

彻底弄懂ip掩码中的网络地址、广播地址、主机地址

本文为博主原创,转载请注明出处: 概念理解: IP掩码(或子网掩码)用于确定一个IP地址的网络部分和主机部分。它是一个32位的二进制数字,与IP地址做逻辑与运算,将IP地址划分为网络地址和主机地址两部分。 在理解IP地址段中的网络地址、广播地址和主机地址之前,首先需要了解IP地址的构成。IP地址由网