prototype

大部分「函数数据类型」的值都具备 “prototype”(原型/显示原型) 属性,属性值本身是一个对象,浏览器默认会为其开辟一个堆内存,用于存储实例可调用的公共属性和方法,在浏览器默认开辟的这个堆内存中「原型对象」,有一个默认的属性 constructor(构造器),属性值就是当前函数/类本身。

在 JS 中「函数数据结构」大致有这几种:

  • 普通函数(匿名函数/实名函数)
  • 箭头函数
  • 构造函数/类
  • 生成器函数 Generator
  • 等等…

但是并不是所有的函数都具备”prototype”(原型/显示原型) 属性,例如:

  • 箭头函数 ```javascript var fn = () => {} fn.prototype // => undefined
  1. - ES6 快捷给对象的某个成员赋值函数值
  2. ```javascript
  3. var obj = {
  4. fn1: function() {},
  5. fn2() {}
  6. }
  7. obj.fn2.prototype // => undefined

proto

每一个「对象数据结构」的值都具备一个属性”proto“(原型链/隐式原型),属性值指向自己所属类的原型 prototype。

  • 普通对象
  • 特殊对象:数组、正则、Error、Date …
  • 函数对象
  • 实例对象
  • 构造函数.prototype

练习题

深圳虾皮一面笔试题

  1. function Fn() {
  2. this.x = 100;
  3. this.y = 200;
  4. this.getX = function () {
  5. console.log(this.x);
  6. }
  7. }
  8. Fn.prototype.getX = function() {
  9. console.log(this.x);
  10. }
  11. Fn.prototype.getY = function() {
  12. console.log(this.y);
  13. }
  14. let f1 = new Fn;
  15. let f2 = new Fn;
  16. console.log(f1.getX === f2.getX);
  17. console.log(f1.getY === f2.getY);
  18. console.log(f1.__proto__.getX === f2.getX);
  19. console.log(f1.__proto__.getY === Fn.prototype.getY);
  20. console.log(f1.getX === Fn.prototype.getX);
  21. console.log(f1.constructor);
  22. console.log(Fn.prototype.__proto__.constructor);
  23. f1.getX();
  24. f1.__proto__.getX();
  25. f2.getY();
  26. Fn.prototype.getY();

全局,变量提升,代码执行,具体的细节我这里就不多赘述了,想知道具体过程的,可以去看我前面的「前端基石」的文章,有详细的介绍。这里主要是捋清楚在示例代码执行过程中的「原型链」,也是借用代码的执行来深入理解 prototype 和 proto

前端基石:原型机原型链查找机制 - 图1

Fn 函数

函数在存储在堆内存中,大致有三部分组成。

  • 作用域
  • 代码字符串
  • 键值对

我们前面说道,「大部分函数类型」都有一个 prototype 属性,属性值本身是一个对象,浏览器默认会为其开辟一个堆内存,用于存储实例可调用的公共属性和方法。
在浏览器默认开辟的这个堆内存中「原型对象」,有一个默认的属性 constructor(构造器),属性值就是当前函数/类本身。

  1. function Fn() {
  2. this.x = 100;
  3. this.y = 200;
  4. this.getX = function () {
  5. console.log(this.x);
  6. }
  7. }

上面的代码执行完成之后,存储结构就如下: Fn 函数的 prototype 指向了 Fn 的原型对象。
前端基石:原型机原型链查找机制 - 图2

Fn.prototype

  1. Fn.prototype.getX = function() {
  2. console.log(this.x);
  3. }
  4. Fn.prototype.getY = function() {
  5. console.log(this.y);
  6. }

向 Fn.prototype 的内存中添加方法 getX、getY,其实可以简单的将 Fn.prototype 理解为一个对象,上述代码就是在给一个对象添加方法。 getX、getY 也都是一个函数,js 也会在堆内存中为他们分片一块内存空间。getX、getY 在内存的存储和 Fn 的存储结构大致一致。这里就简单略过。
前端基石:原型机原型链查找机制 - 图3

实例

  1. let f1 = new Fn;
  2. let f2 = new Fn;

创建构造函数实例,构造函数执行,将函数 Fn 当做一个类,new Fn 也就是创建一个构造函数的实例返回,然后赋值给变量。

一般情况下,new Fn 和 new Fn() 基本一样,但是在一些特殊情况是有区别的。例如:new Fn.getX 和 new Fn().getX,new Fn.getX 是先获取 Fn 的 getX 属性在进行 new 操作,new Fn().getX 是先执行 new Fn() 返回一个实例,在获取实例上的 getX。

在构造函数执行是和普通函数执行一样都会初始化作用域链,但是构造函数执行时,初始化作用域链之前,浏览器会默认先创建一个对象(空对象,Fn 的实例对象)。并且在初始化 this 时,会将 this 创建的空对象,所以在后期代码中执行 this.xx = xx 的时候,实际上就是在往这个对象(实例对象)上添加属性或者方法,这里还需要注意下,构造函数执行,函数内部的私有变量和实例是没有关系的(构造函数的执行流程可看上一篇文章前端基石:构造函数和普通函数)。

经过构造函数的执行,创建了两个实例对象 f1、f2,并且在方法资深通过 this.xxx 为实例对象添加了私有属性和方法。

这里有一点需要注意,属性是公有还是私有是一个「相对的」。 例如: Fn.prototype 是原型对象,f1 是实例对象。 getY 相对于原型对象来说是自己的私有方法,但是相对于实例对象来说是实例对象的共有方法。

每一个「对象数据结构」的值都具备一个属性”proto“(原型链/隐式原型),属性值指向自己所属类的原型 prototype。所以实例对象 f1、f2 也都具备这个属性。但是注意”proto“这个属性在新版本浏览器中被叫做[[prototype]]。

前端基石:原型机原型链查找机制 - 图4

Object 内置类

在 js 有很多的内置类,其中有一个内置类「Object 内置类」,所有对象都是 「Object 内置类」的实例。所以最后无论 proto(原型链)如何查询,都最后找到 Object.prototype 上。Object.prototype 也是一个对象,当然也有 proto属性,但是我们说「proto属性值指向自己所属类的原型 prototype」,对于 Object.prototype 来说就是指向“自己”,所以在这里将 proto设置为 null。

前端基石:原型机原型链查找机制 - 图5
将 Object 内置类带入我们当前的场景下,结构如下:
前端基石:原型机原型链查找机制 - 图6

原型链查找机制

  1. console.log(f1.getX === f2.getX);
  2. console.log(f1.getY === f2.getY);
  3. console.log(f1.__proto__.getX === f2.getX);
  4. console.log(f1.__proto__.getY === Fn.prototype.getY);
  5. console.log(f1.getX === Fn.prototype.getX);
  6. console.log(f1.constructor);
  7. console.log(Fn.prototype.__proto__.constructor);
  8. f1.getX();
  9. f1.__proto__.getX();
  10. f2.getY();
  11. Fn.prototype.getY();

在进行结果输出之前我们先理解一个概念「原型链查找机制」。在 js 中 「对象.xxx」进行对象成员访问时:

  1. 先查询属性或者方法是否是对象本身私有的属性和方法
  2. 如果不是私有,基于 proto查找所属类 prototype 上的属性和方法
  3. 如果所属类 prototype 上没有,则基于原型对象上的 proto在向上查找,直到查找到 Object.prototype 为止

这就是「原型链查找机制」,理解了这个概念之后,结合上面画的图,再来进行上诉代码的结果输出:

  1. console.log(f1.getX === f2.getX); => false

f1.getX:getX 是对象 f1 私有的。f2.getX :getX 也是对象 f2 私有的,既然都是私有的,在存储中指向不同的内存地址,所以结果为 false。

  1. console.log(f1.getY === f2.getY); => true

f1.getY:getY 不是 f1 私有的,基于 proto查找所属类 prototype 上的属性和方法,也就是查找 Fn.prototype,Fn.prototype 上存在 getY。同理,f2.getY 通过原型链查找到 Fn.prototype 上的 getY。二者都找到 Fn.prototype 上的 getY ,所以结果是 true。

  1. console.log(f1.__proto__.getX === f2.getX); => false
  2. console.log(f1.__proto__.getY === Fn.prototype.getY); => true
  3. console.log(f1.getX === Fn.prototype.getX); => false

对象.proto.xxx 或者 构造函数.prototype.xxx 都是跳过私有的查找,直接找原型对象上的公有的属性或者方法。
f1.proto.getX 找到的是 原型上共有的 getX,f2.getX 查找到的是自己私有的 getX,它们指向不同的内存空间,所以结果是 false。
f1.proto和 Fn.prototype 都是跳过私有找共有,.getY 都找到共有的 getY,所以结果是 true。
f1.getX 查找私有的 getX,Fn.prototype.getX 查找公有的 getX,所以结果是 false。

  1. console.log(f1.constructor); => [Function: Fn]
  2. console.log(Fn.prototype.__proto__.constructor); => [Function: Object]
  3. f1.getX(); => 100
  4. f1.__proto__.getX(); => undefined
  5. f2.getY(); => 200
  6. Fn.prototype.getY(); => undefined

这一些列的查找其实和前面的例子差不多,按照原型链查找机制进行查找,先找自己私有,在基于 proto进行原型链的查找,直到查找到 Object.prototype 为止,如果还是没有找到,那结果就是 null。当然这里还中和了一些 this 指向的问题,如果你对 this 的指向还不是很清楚,可以去看看我之前的文章前端基石:this的几种基本情况

这里随带给大家推荐一个网站「https://pythontutor.com/」,利用它可以让我们的代码执行可视化。 pythontutor.gif