深入理解JavaScript中的原型与原型链

210 阅读8分钟

在JavaScript的世界中,原型是一种优雅的设计模式,它允许我们将方法共享给所有由同一个构造函数创建的对象。当我们编码时,为了提高效率,经常会利用这些内置在原型中的便捷方法。例如,当我们需要判断一个字符串的起始字符时,可以轻松地调用字符串原型上的startsWith()方法。

let str = "abcdefg"
console.log(str.startsWith('ab'));  //true

这些内置的方法,正是存储在每个对象构造函数的原型属性中。

一. 构造函数

首先,我们先来聊一聊构造函数,构造函数是用来创建和初始化对象。构造函数本身也是函数,但是它们遵循一些特定的命名约定(通常首字母大写)以及使用 new 关键字来调用。常见的构造函数有String()Number()Boolean()Object()Array()Function()等。

如何使用构造函数:通过new关键字调用,比如let str = new String()就调用了 String() 这个构造函数,然后又声明了一个示例化对象str用来接收这个构造函数,str 就拥有了String() 里面的内置好的方法(也就是原型中的所有方法)。

当然也可以通过let str = ''调用,得到的叫字符串字面量,v8引擎会将它执行为let str = new String(),也就是说与上面的作用是一样的,更加简便,一般推荐使用这种。

让我们通过浏览器看一看构造函数中的方法: image.png

显示str有一个长度length:0和一个[[prototype]](这个就是原型),右边的里面的一堆东西,也就是声明出来的示例化对象str能够使用的包含在原型中全部方法了。每个构造函数基本上都有其原型。

二. 原型(prototype)

2.1 原型的概念

原型的概念简而言之,本质上也是一个对象,是一个函数被定义出来天生就具有的属性。 image.png

2.2 原型存在的意义

因为实例对象能访问到函数原型上的属性,所以原型存在的意义就是为了让某一种数据结构拥有更多的方法可用。如下图,实例 p 能够访问在 Person.prototype 上定义的属性str

image.png

2.3原型的作用

可以将一些固定的属性提取到原型上,减少重复的代码执行,我们以雷总的su7为例;

function Car(color, owner) {
  this.name = 'su7'
  this.height = '1400'
  this.length = '5000'
  this.color = color
  this.owner = owner
}

如示代码,以小米su7为对象,构造一个函数用来生产su7car,当阿炜和小朱需要分别提一辆su7时,如下图所示;

ba9dce6304f465c802db332e6e40976.png

调用了两次构造函数Car,而su7的名字,长高不管谁提应该是不变的,不可能在su7加工厂造个大奔出来吧,前面提到原型是一个函数被定义出来天生就具有的属性,而且是可以自己人为的添加并且能够访问到的,所以我们可以将固定不变的属性设置在其函数的原型中,减少重复代码的执行,显得更为的优雅。如下图;

image.png

此时输出: image.png

这时候有车主就会问了,怎么我车的名字,长高没了?因为这三个属性已经定义在了这个构造函数的原型上,不会显示出来。但是当你访问这三个属性时,还是可以访问出来。 image.png

2.4 原型的特征

我们再以上面su7为例,阿炜su7开腻了,想换成大奔,于是; image.png

然后我们先访问阿炜的car1:

image.png

发现把车的名字真改成了大奔,这不是能修改吗?但是我们再看输出car1和car2:

image.png 发现阿炜的车上输出多了一个属性name:'大奔',但是我们再看函数原型上的输出, image.png 所以函数原型上的属性是不能被实例对象修改的,而图示效果是往阿炜的车(实例对象,并不是原型上)上添加了一个属性name:'大奔'覆盖了原型上的值。当访问原型时,原型上的值还是su7,所以示例对象无法修改函数原型上的属性

那么实例对象能往函数原型上添加属性吗?

image.png

输出:image.png 函数原型上并没有多出一个run属性,实例对象无法增加函数原型上的属性

那么能删除吗? image.png 发现只能删除自己添加的属性,而删除函数原型上的属性时报错,即实例对象无法删除函数原型上的属性

综上所述:实例对象无法修改函数原型上的属性(不能增删改),注意是实例对象无法修改,构造函数本身还是可以修改的

三. 对象原型

对象原型通常被称为隐式原型__proto__,而上面提到的原型则是显示原型prototype

Number()构造函数为例子,形象地了解显示原型prototype与隐式原型__proto__

image.png

c189d5da0c0f21e404aa0b55fec633c.png 通过观察发现,函数上的隐式原型与对象上的原型一模一样,当查找Number对象上的一个属性时,如果Number的prototype(原型)上没有找到,因为它(Number.prototype)也是一个对象,则会往Number.prototype的原型上找,也就是Number.prototype.prototype上查找。相当于继承了创建该函数的对象原型(如上图,Number()构造函数其实是由一个对象的构造函数创建的)。所以Number构造函数对象的隐式原型(对象原型)会继承Object对象构造函数的显示原型(函数原型)。

综上:

  • v8在查找对象上的一个属性时,如果该属性不是对象拥有的,那么v8就会去对象的对象原型上查找。
  • 对象的隐式原型会被赋值成创建该对象的构造函数的显示原型。

四. 原型链

了解原型链之前需知道下面图的原理:

image.png

  • 使用构造函数创建一个实例对象时,实例对象的内部属性prototype(通过__proto__访问)被设置为构造函数的prototype属性。
  • 构造函数本身是一个函数,因此它的__proto__属性指向Function.prototype
  • Function作为一个特殊的构造函数,它的__proto__属性也指向Function.prototype,而Function.prototype的__proto__属性指向Object.prototype。

image.png

当访问实例对象上的属性或方法时,JavaScript首先在实例对象自身上查找。如果找不到,它会查找实例对象的__proto__(即构造函数的prototype)。如果仍然找不到,它会继续沿着原型链向上查找,直到到达Object.prototype。如果最终在原型链上都没有找到,则返回undefined。

再看如下:

let arr = []    //相当于let arr = new Array()
arr.__proto__ === Array.prototype
Array.prototype.__proto__ === Object.prototype

arr.__proto__ === Array.prototype: 当你创建一个数组,比如 let arr = [],这个数组实例 arr 的内部原型(prototype),可以通过__proto__属性访问,指向了 Array.prototype。这意味着数组实例继承了 Array.prototype 上的所有属性和方法。Array.prototype.__proto__ === Object.prototype Array.prototype 本身也是一个对象,因此它也有一个原型。Array.prototype 的原型是 Object.prototype,这意味着数组构造函数的原型对象继承了 Object.prototype 上的所有属性和方法。

最后放张图片可以自行分析理解一下;

image.png

五. new的原理

在JavaScript中,new 关键字与原型(prototype)紧密相关,因为 new 用于创建一个构造函数的实例,而原型链则用于实现继承机制。那么new的执行的原理是怎么样的呢?

new的执行过程

  1. 创建一个新对象
  2. 将构造函数里的this指向这个对象
  3. 让构造函数中的逻辑正常执行(相当于往 this 对象上添加了属性)
  4. 让新创建的对象的 __proto__ = 构造函数的 prototype
  5. return 前面步骤中创建的新对象

以下面代码为例:一个构造函数 Car,其显式原型上定义了 run 属性,值为 ‘running’。在 Car 构造函数内部,为每个实例定义了 name 和 height 属性。” 另外,需要注意的是,在实例 car 上直接访问 run 属性时,实际上是通过原型链访问到 Car.prototype.run 的run='running'

Car.prototype.run = 'running'
function Car() {
  this.name = 'su7'
  this.height = 1400
}

let car = new Car()

new执行过程如下图所示:

image.png 先在构造函数中创建一个obj的对象,然后将该函数绑定的this绑定到obj这个对象上,然后执行下面this.name = 'su7' this.height = 1400这俩行代码,往obj这个对象中添加这俩个属性,然后就需要继承函数的显示原型,将其赋值到obj这个对象的隐式原型里(obj.__proto__ = Car.prototype),最后再判断如果原函数有return返回且是返回一个引用类型,则返回引用类型。否则则返回创建出来的obj对象。

六. 所有的对象都拥有隐式原型吗?

有一个极特殊的情况:let c = Object.create(null),没有隐式原型。

当你使用 Object.create(null) 时,你创建的是一个“纯净”的对象,它不继承任何东西,这是因为 null 并不是一个对象,所以它没有原型链。

image.png