深入理解 JavaScript 原型

197 阅读4分钟

前言

本文已参与掘金创作者训练营第三期「话题写作」赛道,详情查看:掘力计划|创作者训练营第三期正在进行,「写」出个人影响力。

JavaScript 中有个特殊的存在:对象。每个对象还都拥有一个原型对象,并可以从中继承方法和属性。说到对象和原型,你可能会碰到一些奇怪的问题,例如:js的函数怎么也是个对象、 proto 和prototype到底是什么、js的对象是怎么实现继承的、js是怎么访问对象的方法和属性的。接下来我们就带着这些问题来深入理解 JavaScript 原型。

一、原型对象和对象是什么关系

在js中,每个对象可以看作是多个属性(方法)的集合

  {
    key1: [1,23],
    key2: func,
    key3: {'key':true}
  }

在js中,对象值既可以是原始数据类型(number、string、boolean、null、undefined、bigint和symbol),也可以是对象和函数。(说到这有人认为null也是对象,因为typeof null 是'object',其实但是这只是 JS 存在的一个悠久 Bug)。 不管是对象、函数或是数组,它们都是Object的实例,因此在js中,除了原始数据类型,其他的都是对象。 在js中,函数也是一种特殊的对象,它同样拥有属性和值,所有的函数会有一个特别的属性prototype,该属性的值是一个对象,这个对象便是我们常说的”原型对象“。 我们可以在控制条里输出一下这个属性看看:

  function Studen(name){
    this.name = name
  }
  console.log(Studen.prototype)

打印结果:

1.jpg

在打印的结果中可以看到打印出了两个属性,一个是constructor,另一个是 proto (图片里面表示的是[[prototype]],其实[[prototype]]和 proto 意义相同,均表示对象的内部属性,有的浏览器显示[[prototype]],有的显示 proto ) 看到这里,我们之前的问题” proto 和prototype到底是什么“有了眉目,在js中__protp__指向对象的原型对象,对于函数来说,他的原型对象便是prototype。函数的原型对象 prototype 有以下特点: -所有函数的原型对象都有构造函数(constructor)属性,这个构造函数在我们的例子里面就是Studen函数 -Studen函数的原型对象也有自己的原型对象( proto ),之前说过函数事对象的实例,因此Person.prototype的原型对象为Object.prototype。 我们可以用一张图来表示他们的关系

2.jpg

从图中可以看出, proto 属性指向对象的原型对象,每一个函数都有一个 prototype 属性,这个属性是这个函数的原型对象

二、使用 proto 和prototype实现继承

在js中,如果你将A对象的 proto 指向B对象的 prototype 属性,即A.proto = B.prototype, 那么A对象的__proto__便可以访问B的原型对象的属性和方法,这个就是就是的原型链继承。 我们继续以Studen这个方法为例:

  let zs = new Studen('zs')

这个其实是一个语法糖,对原型继承进行了简写,实际上在js引擎上执行了以下代码:

  let zs = {}
  zs.__proto__ = Studen.prototype
  Studen.call(zs,'zs')

然后看一下zs的实例

3.jpg

可以看到zs的 proto 指向了 Student.prototype

我们把这个逻辑加到刚才的流程图里:

4.jpg

我们从图中可以获取到以下的内容:

  • 每个函数的原型对象都有一个构造函数
  • 通过构造函数创建实例对象(new Studen('zs'),
  • 该实例对象的 proto 属性指向该函数的原型对象

三、通过原型链访问对象的方法和属性

当js中的对象访问属性的时候,会先在对象身上找这个属性,如果没找到就去该对象的原型对象上找,如果都没找到就去改对象的原型对象的原型对象上找...最后会找到Object对象,Objec.prototype.proto 指向null,而null是原型链的最后一个环节,都没找的时候就会认为这个属性是undefined。

既然js中会通过遍历原型链来访问对象的属性,那么我们就可以通过原型链的方式进行继承。但是由于每次找属性的时候,需要遍历各个原型链对象,这就会导致性能问题:

  • 当查找不存在的属性的时候会遍历整个的原型链
  • 在原型链查找属性比较耗时,会产出性能消耗

因此,我们在设计对象的时候,要最注意原型链的长度,如果太长了,就可以选择其他方式解决,避免有可能带来的性能问题。

总结

本文带领大家简单的了解了一下JavaScript的原型,虽然ES6之后的语法采用了class/extends的语法糖来表示类和继承,但其本质仍然是原型链继承。理解原型可以对 JavaScript 的内部运行进行初步的窥探。