Skip to main content

原型与原型链

“JavaScript 是一门基于原型的语言。在 JS 中,对象并不是通过类(Class)实例化出来的,而是通过克隆另外一个对象(即它的原型)来创建的。对象通过 __proto__ 隐式指针连接起来的链条,就构成了原型链,这是 JS 实现继承的底层机制。”

基于原型 vs 基于类 (Java/C++)

在 Java 或 C++ 等传统的面向对象语言中,类(Class)是创建对象的模板。类定义了对象的属性和方法,对象是类的实例。这就像是用模具铸造零件,零件一旦造好,就和模具没有动态联系了。

而在 JavaScript 中,根本没有真正的类(ES6 的 class 只是语法糖)。JS 的核心思想是“万物皆对象”。当你想要一个新对象时,你不需要先定义一个类,而是直接找到一个现有的对象作为“原型”,然后基于它克隆出一个新对象。

  • 如果你在新对象上找不到某个属性,引擎就会顺着一根隐形的链条(__proto__)去它的“原型对象”上找。
  • 这种机制非常灵活,你甚至可以在运行时动态改变一个对象的原型。

原型 (Prototype)

每个函数(箭头函数除外)被创建时,引擎都会自动为它添加一个 prototype 属性,这个属性指向一个对象,我们称之为显式原型对象

  • 这个原型对象的作用是:存放所有由该构造函数创建的实例所共享的属性和方法
  • 原型对象默认拥有一个 constructor 属性,反向指回该构造函数本身。

原型链 (Prototype Chain)

每个对象(通过 new 调用的实例或对象字面量)被创建时,都会拥有一个内部隐藏属性 [[Prototype]](在浏览器中通常暴露为 __proto__ 属性,即隐式原型)。

  • 实例对象的 __proto__ 始终指向创建该对象的构造函数的 prototype
  • 因为原型对象本身也是一个对象,它也有自己的 __proto__,指向更上一级的原型对象。
  • 这样一层一层向上查找,直到最终指向 Object.prototype。而 Object.prototype.__proto__ === null,这就到达了原型链的终点。

面试核心记忆点:

  • 找原型:对象的 __proto__ === 构造函数的 prototype
  • 找源头:所有普通对象最终都继承自 Object.prototypeObject.create(null) 创造出的对象除外,它没有 __proto__ 属性,是一个真正的纯净对象。
  • 找构造函数:所有函数(包括 ObjectFunction 本身)最终都继承自 Function.prototype

根据规范不建议直接使用 __proto__,推荐使用 Object.getPrototypeOf()

instanceOf 的原理

比如 A instanceof B,会先沿着 A 的原型链一直向上找,如果能找到一个 __proto__ 等于 B 的 prototype,则返回 true;如果找到终点还没找到则返回 false

function _instanceOf(A, B) {
// typeof null === 'object',需要做额外的基础类型判断
if (A === null || (typeof A !== "object" && typeof A !== "function")) {
return false;
}
let O = B.prototype;
A = Object.getPrototypeOf(A);
while (true) {
if (A === null) {
return false;
} else if (O === A) {
return true;
} else {
A = Object.getPrototypeOf(A);
}
}
}

ObjectFunction 的鸡和蛋的问题

Object instanceof Function; // true
Function instanceof Object; // true
  • Object 本身是个构造函数,是 Function 的实例,即 Object.__proto__ === Function.prototype
  • Function.__proto__ 指向了 Function.prototypeFunction.prototype 继承自 Object.prototype,Function.prototype.__proto__ === Object.prototype

js 引擎查找对象属性/方法

先通过作用域链查找对象本身的属性或方法,之后沿着__proto__构成的原型链向上查找原型对象中的属性和方法,直到找到一个名字匹配的属性/方法或到达原型链的末端,如果找不到就会返回 undefined

为了判断一个对象是否包含自定义属性而不是原型链上的属性, 需要使用继承自 Object.prototype 的 hasOwnProperty 方法

面试题

// Object.prototype 是原型链的顶端
console.log(Object.prototype.__proto__ === null); // true

// Function可以理解为制造一切函数的机器,且自身也是由自己制造的,所以
console.log(Function.__proto__ === Function.prototype); // true

// Object从「对象」角度来讲,是由构造函数 Function 生产的
console.log(Object.__proto__ === Function.prototype); // true

// Function虽然由自身生产,但生产“机器”总要一个模板吧,这个模板对象(Function.prototype.__proto__)就是 Object.prototype
console.log(Function.prototype.__proto__ === Object.prototype); // true

// Object作为一个机器可以看做是有由Function制造出来的
// 而Function作为一个对象可以看做是由Object制造出来的。
console.log(Object instanceof Function); // true
console.log(Function instanceof Object); // true

// String 作为「对象」,是有构造函数 Function 生产的
console.log(String.__proto__ === Function.prototype); // true
// String.prototype 是个对象,是有 Object 生产的
console.log(String.prototype.__proto__ === Object.prototype); // true

function Person() {}
// 自定义构造函数作为「对象」,也是 Function 生产的
console.log(Person.__proto__ === Function.prototype); // true
console.log(Person.prototype.__proto__ === Object.prototype); // true
// Person 和 Object 一样,都是构造函数,都是有 Function 生产出来的对象
console.log(Person.__proto__ === Object.__proto__);

function Student() {
Person.call(this);
}
Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor === Student;
console.log(Student.__proto__ === Function.prototype); // true
console.log(Student.prototype.__proto__ === Person.prototype); // true

面试题:手写 new 操作符

当代码 new Foo(...) 执行时,引擎底层做了以下几件事:

  1. 创建一个全新的空对象。
  2. 将这个新对象的隐式原型(__proto__)指向构造函数的 prototype 对象。
  3. 将构造函数内部的 this 绑定到这个新对象上,并执行构造函数。
  4. 如果构造函数返回了一个非空对象,则返回该对象;否则,返回刚创建的新对象。
function myNew(Constructor, ...args) {
// 1. & 2. 创建一个新对象,并继承构造函数的 prototype
const obj = Object.create(Constructor.prototype);
// 3. 执行构造函数,绑定 this 到新对象
const result = Constructor.apply(obj, args);
// 4. 如果构造函数返回了对象,则返回该对象;否则返回 obj
return result !== null &&
(typeof result === "object" || typeof result === "function")
? result
: obj;
}

面试题:实现寄生组合式继承

在 ES6 classextends 出现之前,寄生组合式继承被认为是 JavaScript 中最理想的继承范式。它避免了调用两次父类构造函数,并且只创建了一份父类属性的副本。

function Parent(name) {
this.name = name;
this.colors = ["red", "blue"];
}
Parent.prototype.getName = function () {
return this.name;
};

function Child(name, age) {
// 1. 继承父类的实例属性(借用构造函数)
Parent.call(this, name);
this.age = age;
}

// 2. 继承父类的原型方法(寄生式)
// Object.create() 创建一个空对象,其隐式原型指向 Parent.prototype
Child.prototype = Object.create(Parent.prototype);

// 3. 修复丢失的 constructor 指向
Child.prototype.constructor = Child;

Child.prototype.getAge = function () {
return this.age;
};

参考