tech-interview-for-developer:JavaScript Object Prototype 原型机制与原型链深度解析

原创2026-10-01 23:31:361,130 阅读
文章标签:教程知识库

tech-interview-for-developer:JavaScript Object Prototype 原型机制与原型链深度解析

本篇文章面向准备技术面试的 JavaScript 开发者,基于 tech-interview-for-developer 仓库中 Language/[Javasript] Object Prototype.md](Language/[Javasript] Object Prototype.md) 一篇展开。Prototype(原型)是 JavaScript 对象从其他对象继承属性与方法的底层机制,也是面试中常被追问的 "prototype-based language"、"prototype chain"、"__proto__ 与 prototype 的区别" 等问题的核心。读完本文,你将理解原型对象与原型链的运作方式、new 实例化背后的属性查找流程,并能把原型机制正确应用到基于构造函数的对象设计中(例如将方法放在 prototype 上以节省内存),同时掌握与 [Closure、ES2015+ 语法 等知识点的关联。

一、什么是 Prototype?为什么 JavaScript 被称为 prototype-based language?

1.1 原型是 JavaScript 对象间继承的机制

简单来说,Prototype 是 JavaScript 对象从另一个对象继承(inherit)属性的机制。它不依赖 class/extends 关键字,而是通过对象与对象之间的关联来实现复用与继承。

1.2 prototype-based language 的含义

JavaScript 经常被描述为 prototype-based language。这类语言的特点如下:

  • 语言本身就支持继承(inheritance);
  • 每个对象都拥有一个 prototype object(原型对象);
  • 原型对象相当于一个 template object(模板对象),其他对象可以从它那里继承(共享)method(方法)和 property(属性)。

也就是说,JavaScript 的“继承”并非类与类之间的层级,而是“实例对象 → 原型对象”的链式关联。

1.3 prototype chain(原型链)

关键点在于:对象的 prototype object 本身也是一个对象,它同样拥有自己的 prototype object,这种层层向上的关联被称为 prototype chain(原型链)。

原型链的顶端是 Object.prototype,而 Object.prototype 的原型是 null,因此原型链是一条有限链路,这正是后续属性查找能“走到尽头”的原因。

1.4 连接发生在哪里

在 JavaScript 中,连接(link)是在 object instance(对象实例)与 prototype 之间建立的,具体通过两种属性体现:

  • __proto__ 属性:实例对象上指向其原型对象的内部属性([[Prototype]] 的浏览器可访问形式);
  • constructor(构造函数)的 prototype 属性:构造函数本身持有的、供 new 出来的实例共享的原型对象。

面试易错点:实例对象通过 __proto__ 访问原型,而函数对象通过 prototype 持有“将来实例共享的原型对象”。两者指向的是同一个对象,但所在位置不同。现代代码更推荐用 Object.getPrototypeOf() 读取原型,而不是直接依赖 __proto__。

二、理解原型对象:从构造函数到实例

2.1 定义构造函数

看下面的示例(原文档中的核心示例):

function Person(first, last, age, gender, interests) {

  // property and method definitions
  this.name = {
    'first': first,
    'last' : last
  };
  this.age = age;
  this.gender = gender;
  //...see link in summary above for full definition
}

这里 Person 是一个普通函数,但我们用 new 来调用它时,它就扮演**构造函数(constructor)**的角色:

  • this 指向新创建的实例;
  • 实例上的 name、age、gender 等属性被逐个初始化;
  • 同时,JavaScript 引擎会建立「实例 → Person.prototype」的原型关联。

2.2 创建对象实例

let person1 = new Person('Bob', 'Smith', 32, 'male', ['music', 'skiing']);

执行 new Person(...) 时,引擎实际完成四件事(可以从语言规范与常见 polyfill 实现推断):

  1. 创建一个新的空对象;
  2. 将该对象的原型(__proto__ / [[Prototype]])指向 Person.prototype;
  3. 以该对象为 this 调用 Person 函数体,初始化属性;
  4. 如果构造函数返回对象则返回该对象,否则返回新建的实例对象。

因此 person1 拥有自己的 name、age、gender、interests 属性,同时通过原型链可以访问到 Person.prototype 上的一切成员。

三、属性查找:调用 person1.valueOf() 时到底发生了什么

这是理解原型链最直观的实验。假设我们调用:

person1.valueOf()

valueOf() 并没有定义在 Person 构造函数里,为什么调用不会报错?按原文档给出的查找流程,引擎的解析过程如下:

  1. 先检查实例自身:浏览器首先检查 person1 这个 object 自身是否拥有 valueOf() 方法,即检查它是否定义在构造函数 Person() 中;
  2. 再检查原型对象:实例自身没有,就检查 person1 的 prototype object(即 Person.prototype)上是否有该方法;
  3. 沿原型链逐级上溯:如果 prototype object 上也没有,就继续检查它的 prototype object(即原型对象的原型对象),一直向上查找,直到某个 prototype object 为 null 为止;
  4. 若整条链上都找不到,则返回 undefined(调用时会抛出 TypeError)。

在这个例子中,valueOf 最终会在 Object.prototype 上被找到并执行,因为它一路从 Person.prototype 查到 Object.prototype。这也是为什么“所有对象都能调用 toString、valueOf、hasOwnProperty 等通用方法”——它们都定义在原型链顶端的 Object.prototype 上。

这个查找过程可以用一句话总结:属性访问总是先查自身(own property),查不到就沿原型链向上逐级查找。判断属性是“自身属性”还是“继承属性”,可以用 Object.prototype.hasOwnProperty() 精确区分。

四、用 hasOwnProperty 验证原型链

配合 Closure 篇中的编程思路,可以用下面的代码实际验证上面描述的原型链行为:

function Person(first, last, age) {
  this.name = { first, last };
  this.age = age;
}

let person1 = new Person('Bob', 'Smith', 32);

console.log(person1.hasOwnProperty('name'));   // true  —— 自身属性
console.log(person1.hasOwnProperty('valueOf'));// false —— 继承自 Object.prototype
console.log('valueOf' in person1);             // true  —— 原型链上可以访问到
console.log(person1.__proto__ === Person.prototype);          // true
console.log(Person.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__);                      // null —— 原型链终点

输出清晰地印证了三点:

  • name 是 person1 的自身属性;
  • valueOf 不是自身属性,但通过原型链可以访问(in 运算符会沿链查找);
  • 原型链终点确实是 null。

五、原型机制的实际应用:把方法放到 prototype 上

5.1 为什么要把方法放在 prototype 上?

在 Closure 一文的 "Performance consideration" 小节中,明确指出:

当不需要 closure 时创建 closure 会对内存和速度造成负面影响。创建新的 object/class 时,method 应该放在 object 的 prototype 上,而不是 constructor 中,因为 constructor 每次被调用时 method 都会被重新赋值(reassign)。

也就是说,如果每个实例都持有自己的函数副本,会造成不必要的内存开销;而把方法定义在 Person.prototype 上,所有实例共享同一个函数,只需一份内存。

5.2 反例:方法定义在构造函数里

function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
  this.getName = function() {
    return this.name;
  };

  this.getMessage = function() {
    return this.message;
  };
}

上面的 getName 与 getMessage 在构造函数每次被调用时都会重新创建一遍,new MyObject() 生成 100 个实例就会有 100 份同名函数,浪费内存。

5.3 正确做法:方法定义在 prototype 上

function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
}
MyObject.prototype = {
  getName: function() {
    return this.name;
  },
  getMessage: function() {
    return this.message;
  }
};

5.4 更稳妥的写法:逐个挂到 prototype 上

原文档同时提醒:把整个 prototype 重新声明(全部覆盖)并不是推荐做法,因为这会切断默认的 constructor 指向。更稳妥的方式是逐个向原型对象添加方法:

function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
}
MyObject.prototype.getName = function() {
  return this.name;
};
MyObject.prototype.getMessage = function() {
  return this.message;
};

5.5 计数器示例:从 closure 版到 prototype 版

Closure 篇给出了一个非常完整的对照:先是一个基于 closure / IIFE 的计数器,每次调用工厂函数都会新建 increment、decrement、value 三个函数,产生额外开销;随后给出了推荐的原型版实现:

function makeCounter() {
    this.publicCounter = 0;
}

makeCounter.prototype = {
    changeBy : function(val) {
        this.publicCounter += val;
    },
    increment : function() {
        this.changeBy(1);
    },
    decrement : function() {
        this.changeBy(-1);
    },
    value : function() {
        return this.publicCounter;
    }
}
var counter1 = new makeCounter();
var counter2 = new makeCounter();

alert(counter1.value());  // 0.

counter1.increment();
counter1.increment();
alert(counter1.value()); // 2.

counter1.decrement();
alert(counter1.value()); // 1.
alert(counter2.value()); // 0.

这里的思路正是本篇文章所讲的原型机制:

  • makeCounter 是构造函数,this.publicCounter 是每个实例自己的状态;
  • changeBy、increment、decrement、value 全部定义在 makeCounter.prototype 上,被 counter1、counter2 共享;
  • 两个实例的状态互不影响,但方法只有一份。

六、与 ES2015+ 的衔接:class 语法是原型机制的语法糖

在 ES2015+ 요약 정리 一篇介绍 ES2015+ 新增语法的背景下,可以这样理解现代写法:ES2015 引入的 class 语法本质上是基于原型机制的语法糖,class 中声明的方法最终仍会被放到构造函数的 prototype 对象上,extends 实现的继承底层仍是原型链。因此,理解本文的 prototype 机制,是正确理解 class、super、Object.create、Object.setPrototypeOf 等现代特性的前提。

七、面试常见追问速查

问题 答案要点
什么是 prototype-based language? 通过对象与原型对象之间的关联实现继承的语言,原型对象是共享方法/属性的模板对象
什么是 prototype chain? 对象 → 其原型对象 → 原型的原型…逐级向上,直到 null 的链式结构
__proto__ 与 prototype 的区别? __proto__ 是实例指向原型的内部属性;prototype 是构造函数持有、供实例共享的原型对象
属性查找的顺序? 先查自身属性,再沿原型链逐级向上,查到 null 为止
为什么所有对象都有 valueOf? 因为原型链顶端 Object.prototype 上定义了这些通用方法
方法应定义在构造函数里还是 prototype 上? 定义在 prototype 上,避免每次实例化都重新创建函数、浪费内存

八、延伸阅读

  • [Language/[Javascript] Closure.md](Language/[Javascript] Closure.md):闭包机制、性能考量与把方法迁移到 prototype 上的完整对照示例;
  • [Language/[Javascript] ES2015+ 요약 정리.md](Language/[Javascript] ES2015+ 요약 정리.md):const/let、箭头函数、class 相关现代语法背景;
  • [Language/[Javascript] 데이터 타입.md](Language/[Javascript] 데이터 타입.md):理解对象与基本数据类型的区别,有助于加深对“对象才有原型链”的认识;
  • README.md:仓库总目录,可继续浏览计算机基础、数据结构、网络等其他面试主题。

说明:本文内容以 [Language/[Javasript] Object Prototype.md](Language/[Javasript] Object Prototype.md) 为骨架,原型链查找流程、构造函数示例、__proto__ 与 prototype 的连接关系均来自该文档;原型链终点为 null、hasOwnProperty 判定等细节为对该机制的标准行为描述,可结合上文代码在浏览器控制台或 Node.js 中自行验证。

登录后查看全文
tech-interview-for-developer