原型与原型链:面试中的关键问题深入剖析
一、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 中,函数、原型和对象构成一个三角关系:
- 函数也是对象 :每个函数都是
Function构造函数的实例,因此函数具备__proto__指向Function.prototype。 - 函数的
prototype属性 :只有函数(非箭头函数)拥有prototype属性。当函数作为构造函数(使用new调用)时,生成的实例对象的__proto__会指向该函数的prototype。 - 普通对象的
__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 的原因
-
constructor可被随意修改:javascriptfunction Dog() {} const dog = new Dog(); dog.constructor = Array; console.log(dog instanceof Dog); // true(正确) console.log(dog.constructor === Dog); // false(被篡改)若
instanceof依赖constructor,结果会被误导。 -
原型链中可能存在多个构造函数 :
instanceof关心的是整个链条上是否存在目标原型,而不是某个对象的constructor值。例如,[] instanceof Object为true,但[].constructor === Object为false(实际上是Array)。 -
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。
优点
-
纯粹的委托继承:无需构造函数,直接指定原型。适用于单纯的共享行为,不涉及构造逻辑。
-
避免构造函数副作用 :传统
new会执行构造函数,可能产生不必要的初始化代码。Object.create不会调用任何函数。 -
更自然的原型链 :常用于实现"原型式继承"(道格拉斯·克罗克福德提出),比
new更直观地表达"继承自那个对象"。 -
支持
null原型对象 :创建完全空的对象(无原型链),用于纯字典存储(避免toString等属性冲突):javascriptconst pureDict = Object.create(null); -
与寄生式组合继承完美配合 :寄生式组合继承中的
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 底层)的基础。