本文深入解析 JavaScript 原型链机制,重点厘清 Function 与 Object 的复杂关系。通过图解说明 prototype 与 __proto__ 的区别,结合具体代码示例推导属性访问路径,帮助开发者理解构造函数、实例对象与原型对象之间的指针指向,避免原型链查找误区。
前言与问题引入
前言
在 Segmentfault 社区中曾出现一道关于原型链的题目,引发了广泛讨论。
var F = function(){};
Object.prototype.a = function(){};
Function.prototype.b = function(){};
var f = new F();
题目核心在于:变量 f 能否访问到属性 a 和 b?其背后的原理是什么?
初看此题容易感到困惑,经过深入分析后发现,这主要源于对原型机制理解不够透彻。因此,本文旨在通过总结这一知识点来填补认知空白。
Function与Object的关系解析
Function和Object
在解答具体题目之前,有必要重新梳理原型、原型链的概念,以及 Function 和 Object 之间的特殊关系,这也是本文的核心内容。
原型
每当创建一个函数时,JavaScript 引擎会自动为其生成一个原型对象,该对象可通过函数的 prototype 属性进行访问。
当通过构造函数创建实例对象时,该实例内部会包含一个内部指针(在 ECMA-262 第5版中称为 [[prototype]]),指向构造函数的原型对象。尽管脚本中没有标准的访问方式,但 Firefox、Safari 和 Chrome 等浏览器在对象上实现了 __proto__ 属性,用于访问其构造函数的原型对象。
这里需要强调两点关键区别: 1. 构造函数通过 prototype 属性指向原型对象。 2. 实例对象通过 [[prototype]](浏览器中表现为 __proto__)指向原型对象。
var F = function () {};
var f = new F();
// 假设F的原型对象是 p, 则
// F.prototype === p;
// f.__proto__ === p;
简而言之,prototype 描述的是构造函数与原型对象之间的关系,而 __proto__ 描述的是实例对象与原型对象之间的关系。
原型链机制详解
原型链
所谓的类 A 继承 B,B 继承 C,本质上是 A 的原型对象中包含指向 B 原型对象的指针,而 B 的原型对象中又包含指向 C 原型对象的指针。需要注意的是,这种联系仅存在于原型对象之间,构造函数 A、B、C 本身并无直接继承关系,因此这种机制被称为“原型链”。
假设 a 是类 A 的实例,其原型链结构如文中图示的紫色线条所示,而橙色线条则连接了构造函数与其对应的原型对象。

从图中可以看出,原型链的终点是 Object.prototype.__proto__,即 null。当访问 a 的某个属性或方法时,JavaScript 引擎首先查找 a 自身是否存在该属性;若不存在,则沿着原型链向上查找,直到找到该属性或到达 null(此时返回 undefined)。
Function 和 Object
Function 和 Object 之间的关系较为复杂,容易混淆:
Object 是一个构造函数,既然它是函数,那么它就是 Function 的实例对象;Function 也是一个构造函数,但 Function.prototype 是一个对象,既然它是对象,那么它就是 Object 的实例对象。
结论是:一切对象都是 Object 的实例,一切函数都是 Function 的实例。
具体而言,Object 是 Function 的实例,而 Function.prototype 是 Object 的实例。
二者的关系可通过图示清晰展示。

如图所示,Object 作为构造函数,拥有 prototype 属性指向 Object.prototype;同时作为实例对象,它拥有 Object.__proto__ 指向 Function.prototype。Function 作为构造函数,拥有 prototype 属性指向 Function.prototype;同时 Function 本身也是函数,因此也是 Function 的实例,故拥有 Function.__proto__ 指向 Function.prototype。由此可得 Function.__proto__ === Function.prototype 为 true。
读者可在 Chrome 浏览器控制台中进行验证,具体结果如图示。

原题解析与扩展思考
原题解析
解决原型链问题的最佳方法是绘制关系图。基于前文的分析,绘制该图并不困难,如下所示:

f 的原型链由蓝色线条表示,因此 f 可以访问到 a,但无法访问到 b。
如果不绘图,初看可能会误以为 f 可以访问 b。这种误解可能源于错误地认为 F.prototype 指向 Function.prototype。实际上,F.prototype 是一个普通对象而非函数,因此它的原型对象不会是 Function.prototype。
因此,处理原型链问题时,务必通过绘图来理清关系。
原题扩展
在原题中,f 只能访问 a,无法访问 b。但构造函数 F 本身既可以访问 a,也可以访问 b。如果将题目修改为如下形式,F.b() 的执行结果是什么?原因何在?建议读者自行思考。
var F = function(){};
Object.prototype.a = function(){};
Function.prototype.b = function(){ console.log('F.__proto__') };
F.prototype.b = function (){console.log('F.prototype');};
函数双重身份总结
总结
阅读至此,是否发现函数具有一个特殊性质?
普通对象仅有一个 __proto__ 属性,用于访问其构造函数的原型对象;而函数则兼具函数和对象的双重身份。
作为函数,它天生拥有 prototype 属性,指向其原型对象(即函数名.prototype)。
作为 Function 的实例对象,它拥有 __proto__ 属性,指向 Function.prototype。
通常情况下,这两个属性指向不同的对象;但对于 Function 而言,这两个属性指向相同的目标,即都指向 Function.prototype。
对于函数 A() 而言,A.prototype 中的方法供其实例对象调用,A 自身并不直接使用;当 A 作为实例运行时,调用的是 A.__proto__ 中的方法。也就是说,当 A 作为构造函数使用时,遵循 A.prototype 这条链,将方法和属性赋给其实例;当 A 作为对象使用时,遵循 A.__proto__ 这条链。在不同场景下,明确其身份即可避免错误。
整篇文章叙述可能略显冗长,不足之处恳请指正。至于题目命名,确实难以确定。
希望本文能给坚持阅读的你带来一些收获。
感谢大家对本站的支持,后续将继续更新相关资料,帮助大家深入学习和理解这部分知识!