为什么必须掌握 js 原型面试题?
在 JavaScript 的面试中,js 原型面试题几乎是必考的核心板块。无论是初级还是高级前端岗位,面试官都会通过考察候选人对原型链、继承机制以及对象创建方式的理解,来评估其底层功底。JavaScript 作为一种基于原型的语言(Prototype-based Language),其核心机制与传统的类继承语言(如 Java、C++)有着本质的区别。许多开发者虽然日常使用 ES6 的 class 语法,但在面对底层原理问题时往往束手无策。本文将系统性地梳理 js 原型面试题 的高频考点,从基础概念到深层机制,为您提供一份详尽的备考指南。
通过本文,您不仅能掌握常见的 js 原型面试题 答案,更能深入理解 JavaScript 引擎如何处理对象属性和方法查找,从而在编程实践中写出更高效、更优雅的代码。
基础概念:Prototype 与 __proto__
在深入 js 原型面试题 之前,必须厘清两个核心概念:prototype 和 __proto__。这是理解原型链的基石。
prototype (原型对象)
这是函数特有的属性。每个函数都有一个默认的 prototype 属性,指向一个对象。这个对象被称为“原型对象”。当函数被用作构造函数(通过 new 关键字调用)时,新创建的对象会隐式地将构造函数的 prototype 属性作为自己的原型。
__proto__ (隐式原型)
这是对象拥有的属性(尽管它是非标准的,但所有主流浏览器都支持)。它指向创建该对象的构造函数的 prototype。通过 __proto__,对象能够访问到原型对象上的属性和方法。在现代 JavaScript 中,推荐使用 Object.getPrototypeOf() 来获取对象的隐式原型。
代码示例解析
function Person(name) {
this.name = name;
}
// 在 Person 的原型上添加方法
Person.prototype.sayName = function() {
console.log("My name is " + this.name);
};
const person1 = new Person("Alice");
console.log(person1.__proto__ === Person.prototype); // true
console.log(person1.sayName === Person.prototype.sayName); // true
在上述代码中,person1 是一个普通对象,它没有自己的 sayName 方法。当调用 person1.sayName() 时,JavaScript 引擎会在 person1 上查找,找不到则通过 __proto__ 指向的 Person.prototype 上查找,最终找到了该方法。这就是原型查找机制的基本原理。
核心机制:原型链 (Prototype Chain)
js 原型面试题 中,关于原型链的题目往往占据最大比重。原型链是 JavaScript 实现继承的主要机制。每个对象都有一个原型,原型本身也是一个对象,因此原型也有自己的原型,如此层层向上,直到找到一个对象的原型为 null 为止。null 没有原型,并作为这个原型链中的最后一个环节。
属性查找规则
当访问一个对象的属性时,JavaScript 引擎会执行以下步骤:
- 首先在该对象自身属性中查找。
- 如果找到,则返回该属性的值。
- 如果未找到,则沿着原型链向上查找,即检查该对象的原型对象。
- 如果在原型对象中找到,则返回。
- 如果仍未找到,则继续向上查找原型的原型,直到原型链末端(
null)。 - 如果全程未找到,则返回
undefined。
| 查找层级 | 说明 | 示例 |
|---|---|---|
| 实例对象 | 对象自身的属性 | obj.prop |
| 直接原型 | 构造函数的 prototype | obj.__proto__ |
| 原型原型 | Object.prototype | obj.__proto__.__proto__ |
| 链末端 | null | 终止查找 |
经典面试题:原型链指向
面试官常问:Function.prototype 和 Object.prototype 的关系是什么?
console.log(Object instanceof Function); // true
console.log(Function instanceof Object); // true
console.log(Function instanceof Function); // true
// 解析:
// 1. Function 是一个函数,所以它有 __proto__ 指向 Function.prototype
// 2. Object 也是一个函数(构造函数),所以它也有 __proto__ 指向 Function.prototype
// 3. Function.prototype 是一个对象(且是函数),所以它有 __proto__ 指向 Object.prototype
// 4. Object.prototype 的 __proto__ 指向 null
继承模式:从构造函数到组合继承
理解 js 原型面试题 的最终目的是为了解决代码复用问题,即继承。JavaScript 中有多种继承模式,每种模式都有其优缺点。
SubType.prototype = new SuperType();
基本思想:利用原型让一个引用类型继承另一个引用类型的属性和方法。
function SuperType() {
this.colors = ['red', 'blue'];
}
function SubType() {}
SubType.prototype = new SuperType();
const instance1 = new SubType();
instance1.colors.push('green');
const instance2 = new SubType();
console.log(instance2.colors); // ['red', 'blue', 'green']
// 缺点:引用属性被所有实例共享,无法向 SuperType 传参
SuperType.call(this);
基本思想:在子类构造函数内部调用父类构造函数,使用 call 或 apply 改变 this 指向。
function SuperType(name) {
this.name = name;
this.colors = ['red', 'blue'];
}
function SubType(name) {
SuperType.call(this, name); // 继承实例属性
}
// 优点:解决了引用属性共享问题,可以传参
// 缺点:方法都在构造函数中定义,无法函数复用
原型链 + 构造函数
基本思想:使用原型链实现对原型属性和方法的继承,通过构造函数实现对实例属性的继承。
function SuperType(name) {
this.name = name;
this.colors = ['red', 'blue'];
}
SuperType.prototype.sayName = function() {
console.log(this.name);
};
function SubType(name, age) {
SuperType.call(this, name); // 第二次调用 SuperType
this.age = age;
}
SubType.prototype = new SuperType(); // 第一次调用 SuperType
SubType.prototype.constructor = SubType;
// 优点:融合了两种模式的优点,既保留了方法复用,又保证了实例属性独立
ES6 类语法与原型
ES6 引入了 class 关键字,使得 JavaScript 的面向对象编程更加直观。但需要注意的是,class 本质上仍然是原型继承的语法糖。理解这一点对于回答高级 js 原型面试题 至关重要。
语法对比
// ES5 写法
function Person(name) {
this.name = name;
}
Person.prototype.sayHi = function() {
console.log('Hi');
};
// ES6 写法
class Person {
constructor(name) {
this.name = name;
}
sayHi() {
console.log('Hi');
}
}
两者在内存中的结构是完全相同的。
内部机制
ES6 类的所有方法都定义在类的原型对象上,因此可以通过 Object.getPrototypeOf() 访问。
class Person {}
const p = new Person();
console.log(Object.getPrototypeOf(p) === Person.prototype); // true
静态成员
使用 static 关键字定义的方法,不会被实例继承,而是直接通过类来调用。静态方法通常用于工具函数或工厂方法。
class Person {
static create(name) {
return new Person(name);
}
}
高频 js 原型面试题 深度解答
Q1: 请手写一个 instanceof 运算符
解析: instanceof 的作用是判断一个对象是否为某个构造函数的实例。其核心逻辑是遍历左操作数的原型链,看是否能找到右操作数的 prototype 属性。
function instanceofTest(left, right) {
let proto = Object.getPrototypeOf(left);
const prototype = right.prototype;
while (true) {
if (proto === null) {
return false;
}
if (proto === prototype) {
return true;
}
proto = Object.getPrototypeOf(proto);
}
}
Q2: 如何判断一个对象是否为空?
虽然这看起来不是原型题,但常与原型遍历结合考察。
function isEmptyObject(obj) {
return Object.keys(obj).length === 0;
}
Q3: Object.create(null) 创建的对象有什么特点?
解析: 使用 Object.create(null) 创建的对象没有原型链,即其 __proto__ 为 null。因此,它不能访问 Object.prototype 上的方法(如 toString, hasOwnProperty)。这种对象常被称为“字典”或“纯净对象”,在需要避免原型污染或键名冲突的场景下非常有用,例如在 jQuery 的 $.extend 中用于合并配置项时,防止读取到原型上的属性。
const obj = Object.create(null);
obj.name = 'Alice';
console.log(obj.name); // 'Alice'
console.log(obj.toString()); // TypeError: obj.toString is not a function
Q4: 解释一下 new 操作符的内部实现
解析: new 操作符在执行时做了四件事:
- 创建一个空对象。
- 将空对象的原型指向构造函数的
prototype。 - 将空对象赋值给构造函数内部的
this,并执行构造函数代码。 - 返回该对象(如果构造函数没有返回其他对象)。
function newOperator(Constructor, ...args) {
// 1. 创建新对象
const obj = {};
// 2. 连接原型链
Object.setPrototypeOf(obj, Constructor.prototype);
// 3. 绑定 this 并执行
const result = Constructor.apply(obj, args);
// 4. 返回对象
return (result !== null && typeof result === 'object') ? result : obj;
}