JavaScript原型与原型链深度解析

作者:小括号时间:2026-10-31 00:26:37浏览:280

本文深入解析JavaScript中的原型(prototype)与原型链机制。详细阐述所有对象均源自Object的概念,区分构造函数、实例与函数之间的关系。通过代码示例说明prototype与__proto__的区别,解释原型覆盖导致的constructor丢失问题及其修复方法,并梳理从Function到Object的原型链指向规则,帮助开发者理解JS基于原型的对象创建与继承逻辑。

JavaScript原型基础概念

要理解JS中的prototype,首先必须弄清楚以下几个概念 1. JS中所有的东西都是对象

在JavaScript中,万物皆对象。这意味着无论是基本数据类型包装后的对象,还是复杂的自定义结构,在JS的世界里都被视为对象。


  // ["constructor", "toString", "toLocaleString", "valueOf", "hasOwnProperty", "isPrototypeOf", 
   // "propertyIsEnumerable", "__defineGetter__", "__lookupGetter__", "__defineSetter__",
   // "__lookupSetter__"]
   console.log(Object.getOwnPropertyNames(Object.prototype));

进一步而言,JavaScript中所有的对象最终都由Object衍生而来,即所有对象的原型链终点都指向Object.prototype。此外,还需要理解构造函数与实例(对象)之间紧密且微妙的关系:构造函数通过定义prototype属性来规定其实例的规范,并通过new关键字生成实例,其核心作用是创建对象。值得注意的是,构造函数本身也是Function类的实例,因此它同样拥有__proto__(原型链)属性。

例如Object或function F() {}这样的结构属于构造函数,前者是JS原生API提供的,后者为自定义构造函数。而new Object()或new F()生成的则是实例。对于实例而言,它“只能”通过查看__proto__来追溯自己是基于哪个prototype构建的,而“不能”直接重新定义实例的prototype属性去创造所谓的“实例的实例”。

理论结合实践,只有亲自观察和思考,才能真正领悟原型机制的运作方式。


  // 先来看看构造函数到底是什么
  // function Empty() {}  function Empty() {}
  console.log(Function.prototype, Function.__proto__);
  // Object {}          function Empty() {}
  console.log(Object.prototype, Object.__proto__);
  function F() {}
  // F {}              function Empty() {}
  console.log(F.prototype, F.__proto__);

如果上述概念让你感到困惑,我们可以逐步分解来看。

关于prototype,其输出格式通常表现为“构造函数名 原型”。首先观察Object.prototype的输出结果:Object {},其中前面的Object代表构造函数名称,后面的{}表示原型,这里是一个Object对象的实例(即空对象)。同理,对于F {},F是构造函数名称,其原型也是一个空对象。


  // 再来看看由构造函数构造出来的实例
  var o = new Object(); // var o = {};
  // undefined       Object {}
  console.log(o.prototype, o.__proto__);
  function F() {}
  var i = new F();
  // undefined       F {}
  console.log(i.prototype, i.__proto__);

接下来我们深入一步,尝试定义F的原型,观察具体会发生什么变化。


  function F() {}
  F.prototype.a = function() {};
  var i = new F();
  // undefined       F {a: function}
  console.log(i.prototype, i.__proto__);

通过观察结果可以清晰地看到,实例i是由F构造出来的,其原型变为{a: function}。这意味着在原本的空对象原型基础上,新增了一个名为a的方法。

我们再换一种情况,如果完全覆盖F的原型,结果会如何?


function F() {}
  F.prototype = {
    a: function() {}
  };
  var i = new F();
  // undefined       Object {a: function}
  console.log(i.prototype, i.__proto__);
  

这里会出现一个看似矛盾的现象:实例i似乎是由Object构造出来的。这并非错误,而是因为我们完全覆盖了F的prototype,将其指定为对象{a: function}。这一操作导致原本的constructor信息丢失,转而使用对象{a: function}所指定的constructor。那么,对象{a: function}的constructor究竟是谁呢?


  var o = {a: function() {}} // new了一个Object

由于对象{a: function}本质上是Object的实例,因此其constructor自然指向Object。

为了解决这个问题,我们需要对代码进行修正。


  function F() {}
  F.prototype = {
    a: function() {}
  }
  // 重新指定正确的构造函数
  F.prototype.constructor = F;
  var i = new F();
  // undefined       F {a: function, constructor: function}
  console.log(i.prototype, i.__proto__);

经过修正后,我们可以再次获得正确的原型信息,确保constructor指向正确的构造函数。

原型链机制解析

接下来探讨原型链的概念。

原型链在概念上与面向对象编程(OOP)中的继承关系(链)相似,即一层一层向上查找,直至最终到达Object.prototype。

JavaScript原型基础概念

最关键的是要弄清楚JavaScript中哪些东西是(实例)对象。这很简单,因为JS中所有东西都是对象。同时需要明确,任何一个对象都拥有一个原型。

我们可以通过以下代码来证明这一点。


  Object // 这是一个函数, 函数是 Function 的实例对象, 那么就是由 Function 构造出来的
  Object.__proto__ == Function.prototype // 那么Object的原型, true
  // 这个是一个普通对象了, 因此属于 Object 的实例
  Function.prototype.__proto__ == Object.prototype // true
  // 这已经是原型链的最顶层了, 因此最终的指向 null
  Object.prototype.__proto__ == null // true

  Function // 这也是一个函数, 没错吧!
  Function.__proto__ == Function.prototype // true
  
  function A() {} // 这是一个自定义的函数, 终归还是一个函数, 没错吧! 
  A.__proto__ == Function.prototype // 任何函数都是 Function 的实例, 因此A的原型是?
  var a = new A()
  a.__proto__ == A.prototype // 实例a是由A构造函数构造出来的, 因此a的原型是由A的prototype属性定义的
  A.prototype.__proto__ == Object.prototype // 普通对象都是 Object 的示例

Prototype与__proto__的关系

关于Prototype和__proto__的区别:每一个对象都包含一个__proto__属性,它指向该对象的原型。类似地,每一个函数都包含一个prototype属性,这个prototype对象的作用是什么?

让我们看下面的代码,这里使用构造函数来创建一个对象(此前使用的是字面量形式创建对象)。


function Foo(){};
var foo = new Foo();
console.log(foo.__proto__);

试想一下,这个foo对象的__proto__会指向哪里?

JavaScript原型基础概念

这是一个包含constructor属性的对象吗?如果看不太懂,不妨将函数Foo的prototype属性打印出来,进行对比即可明白。


function Foo(){};
var foo = new Foo();
console.log(foo.__proto__);
console.log(Foo.prototype);
console.log(foo.__proto__ === Foo.prototype);

JavaScript原型基础概念

结果显示,new出来的对象foo的__proto__确实指向函数Foo的prototype。


foo.__proto__ --> Foo.prototype

JavaScript这样设计的意义何在?回顾之前提到的观点,在JS的世界中,对象不是根据类(模具)创建的,而是从原型(另一个对象)衍生出来的。

当我们执行new操作创建新对象时,虽然不深入new的具体实现细节,但可以肯定一点:新对象的__proto__会指向一个原型对象。

针对刚才这段代码进行分析。


function Foo(){};
var foo = new Foo();

foo.__proto__到底应该指向谁?它不能指向Foo这个函数本身,尽管函数也是对象,但这会在后续详细讨论。如果foo.__proto__直接指向Foo函数,虽然技术上可行,但并不合适。因为Foo是一个包含大量逻辑代码的函数,而foo作为一个对象,继承函数的逻辑处理没有意义,它真正需要继承的是“原型对象”的属性。

因此,每个函数会自动生成一个prototype对象,由该函数new出来的对象的__proto__就指向这个函数的prototype。


foo.__proto__ --> Foo.prototype

原型机制总结与图示

总结部分,虽然文字描述可能仍显抽象,但配合图表会更清晰。我曾参考过其他资料中的图表,但觉得解释不够透彻,因此自己绘制了一张图。如果觉得这张图有帮助,请给予认可(绘制此图确实耗费了大量精力)。

JavaScript原型基础概念

结合这张图,我们需要记住以下几个关键事实:

1. 每个对象中都包含一个__proto__属性。

在JS世界中,不存在类(模具)的概念,对象是从另一个对象(原型)衍生出来的。因此,每个对象中会有一个__proto__属性指向它的原型对象。(参考左上角用字面量形式定义的对象obj,它在内存中开辟空间存放自身属性的同时,生成一个__proto__指向其原型——顶层原型对象。)

2. 每个函数都有一个prototype属性。

“构造函数”之所以叫构造函数,是因为它负责构造对象。根据上述第一条事实,构造出的新对象的__proto__属性指向哪里?总不能指向构造函数自身,尽管它也是对象,但你并不希望新对象继承函数的属性与方法。因此,在每个构造函数中都有一个prototype属性,指向一个对象,作为该构造函数创建的新对象的原型。

3. 函数也是对象。

每个函数都有一些通用的属性和方法,例如apply()/call()等。但这些通用方法是如何继承的?函数又是如何创建出来的?试想,一切皆对象,包括函数也是对象,且是通过构造函数创建的对象。根据上述第二条事实,每个函数也会有__proto__指向其构造函数的prototype。而这个构造函数的函数就是Function,JS中的所有函数都是由Function构造出来的。函数的通用属性与方法就存放在Function.prototype这个原型对象上。

作者最新文章