原型与原型链:面试中的关键问题深入剖析

原型与原型链:面试中的关键问题深入剖析

一、instanceof 的底层机制

instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。其底层算法可概括为:

javascript 复制代码
function myInstanceOf(instance, constructor) {
  let proto = Object.getPrototypeOf(instance);
  while (proto !== null) {
    if (proto === constructor.prototype) return true;
    proto = Object.getPrototypeOf(proto);
  }
  return false;
}

核心要点:

  • 判断依据是原型链上的对象 ,而非对象的 constructor 属性。
  • 右操作数必须是一个构造函数(拥有 prototype 属性)。
  • 如果原型链被手动修改(例如通过 Object.setPrototypeOf),instanceof 的结果会随之改变。

二、function、原型与对象之间的关系

在 JavaScript 中,函数、原型和对象构成一个三角关系:

  1. 函数也是对象 :每个函数都是 Function 构造函数的实例,因此函数具备 __proto__ 指向 Function.prototype
  2. 函数的 prototype 属性 :只有函数(非箭头函数)拥有 prototype 属性。当函数作为构造函数(使用 new 调用)时,生成的实例对象的 __proto__ 会指向该函数的 prototype
  3. 普通对象的 __proto__ :指向其构造函数的 prototype

关系示例:

javascript 复制代码
function Person(name) { this.name = name; }
const p = new Person('Alice');

// p.__proto__ === Person.prototype
// Person.prototype.constructor === Person
// Person.__proto__ === Function.prototype
// Function.prototype.__proto__ === Object.prototype

三、寄生式组合继承与 extends 继承的性能差异

寄生式组合继承(手动实现)

javascript 复制代码
function inherit(subType, superType) {
  const prototype = Object.create(superType.prototype);
  prototype.constructor = subType;
  subType.prototype = prototype;
}

function Super() { this.a = 1; }
function Sub() { Super.call(this); this.b = 2; }
inherit(Sub, Super);
  • 优点 :只调用一次父类构造函数(在 Sub 内部通过 Super.call(this)),避免在子类原型上创建多余的父类实例属性。
  • 性能:原型链较短,属性查找快;内存占用小(父类实例属性只存在于子类实例中,不在原型上)。

extends 继承(ES6 class)

javascript 复制代码
class Super { constructor() { this.a = 1; } }
class Sub extends Super { constructor() { super(); this.b = 2; } }
  • 底层:Babel 编译后近似于寄生式组合继承,但原生引擎实现更优化。
  • 性能差异
    • 构造速度extends 在 V8 等引擎中有专门优化,通常比手写寄生式组合继承快 10-20%。
    • 属性访问:几乎无差别。
    • 内存:二者相同,都不会将父类实例属性挂载到子类原型上。

结论 :优先使用 extends,除非需要兼容 ES5 环境。手写寄生式组合继承在现代引擎中略慢,但差距可忽略。

四、原型与构造函数的关系 & instanceof 为何不依赖 constructor

原型与构造函数的关系

  • 每个函数(构造函数)都有一个 prototype 属性,指向其原型对象。
  • 原型对象默认拥有一个 constructor 属性,指回该函数。
  • 实例对象的 __proto__ 指向构造函数的 prototype,因此实例可通过 __proto__.constructor 访问构造函数。

instanceof 不用 constructor 的原因

  1. constructor 可被随意修改

    javascript 复制代码
    function Dog() {}
    const dog = new Dog();
    dog.constructor = Array;
    console.log(dog instanceof Dog); // true(正确)
    console.log(dog.constructor === Dog); // false(被篡改)

    instanceof 依赖 constructor,结果会被误导。

  2. 原型链中可能存在多个构造函数instanceof 关心的是整个链条上是否存在目标原型,而不是某个对象的 constructor 值。例如,[] instanceof Objecttrue,但 [].constructor === Objectfalse(实际上是 Array)。

  3. constructor 不是标准原型链遍历的必由之路Object.getPrototypeOf 直接获取原型,不依赖 constructor 属性,更可靠。

五、隐式原型(__proto__)与显式原型(prototype)的关系与差异

特性 __proto__(隐式原型) prototype(显式原型)
拥有者 所有对象(包括函数) 只有函数(非箭头函数)
作用 指向构造函数的原型,构成原型链 定义通过该函数创建的实例的公共原型
标准性 非标准(但 ES6 定义了 Object.getPrototypeOf / setPrototypeOf 替代) 标准规范,所有函数自动拥有
关系 实例.__proto__ === 构造函数.prototype prototype 本身也是一个对象,拥有自己的 __proto__

关键差异

  • prototype 是构造函数专属,用于设置继承;__proto__ 是实例属性,用于读取原型链。
  • 修改 prototype 会影响所有未来实例的 __proto__;修改单个实例的 __proto__ 只影响该实例(不推荐)。
  • 函数也是对象,所以函数既有 __proto__(指向 Function.prototype),又有 prototype(指向其原型对象)。

六、Object.create 方式继承的优点

Object.create(proto, [propertiesObject]) 创建一个新对象,其 __proto__ 指向 proto

优点

  1. 纯粹的委托继承:无需构造函数,直接指定原型。适用于单纯的共享行为,不涉及构造逻辑。

  2. 避免构造函数副作用 :传统 new 会执行构造函数,可能产生不必要的初始化代码。Object.create 不会调用任何函数。

  3. 更自然的原型链 :常用于实现"原型式继承"(道格拉斯·克罗克福德提出),比 new 更直观地表达"继承自那个对象"。

  4. 支持 null 原型对象 :创建完全空的对象(无原型链),用于纯字典存储(避免 toString 等属性冲突):

    javascript 复制代码
    const pureDict = Object.create(null);
  5. 与寄生式组合继承完美配合 :寄生式组合继承中的 Object.create(superType.prototype) 正是利用该特性来隔离父类原型与子类原型,避免重复调用父类构造函数。

示例对比

javascript 复制代码
// 传统构造函数继承(需要两步)
function Parent() { this.name = 'parent'; }
function Child() { Parent.call(this); }
Child.prototype = new Parent(); // 副作用:Parent 被调用一次

// Object.create 方式
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child; // 无调用副作用,性能更优

总结Object.create 是 JavaScript 原型继承的核心工具,它提供了更纯粹、更灵活的原型链操作方式,是现代继承模式(包括 ES6 extends 底层)的基础。

相关推荐
光影少年15 小时前
react的 useReducer 使用场景、替代 useState 的情况
前端·react.js·掘金·金石计划
To_OC15 小时前
徒手撸极简前后端分离Demo!吃透原生JS动态渲染底层
前端·javascript
HYCS15 小时前
用pixi.js实现fabric.js(四):StaticCanvas
前端·javascript·canvas
烬羽15 小时前
《读<JavaScript语言精粹>第3章,我整理了6个必须掌握的对象核心知识点》
前端
GuWenyue15 小时前
从零搭建用户管理系统!60分钟搞定RESTful接口+Bootstrap语义化首页
前端·后端
超人气王15 小时前
JavaScripts入门篇————js原型的底层原理
前端·javascript
蜡笔小电芯15 小时前
【Electron】第1章—新建工程(基于 Electron + Vite + JavaScript)
前端·javascript·electron
_xaboy15 小时前
开源Vue组件 FormCreate 使用组件内部方法校验
前端·vue.js·开源
审判长烧鸡15 小时前
【AI问答/前端】前端满天过海局(一)
前端·vue·浏览器