在JavaScript的知识体系中,原型与原型链是贯穿始终的核心机制,也是前端面试中区分基础是否扎实的“分水岭”。不同于传统面向对象语言的“类”继承,JavaScript通过原型实现了对象间的属性和方法共享,这种独特的设计思想不仅支撑着数组、函数等内置对象的运作,更藏着诸多易混淆的面试考点。今天这篇文章,我们就从面试视角出发,把原型、原型链的本质、核心用法以及高频考题一次性讲透。
一、为什么需要原型?—— 从对象复用的痛点说起
在理解原型之前,我们先思考一个问题:如何高效创建多个具有相同属性和方法的对象?比如创建一批“学生”对象,每个学生都有“姓名”“年龄”属性和“学习”方法。最直接的方式是使用对象字面量或构造函数:
// 方式1:对象字面量(重复代码过多)
const student1 = {
name: '张三',
age: 18,
study() {
console.log(`${this.name}正在学习`);
}
};
const student2 = {
name: '李四',
age: 19,
study() { // 方法重复定义,浪费内存
console.log(`${this.name}正在学习`);
}
};
// 方式2:构造函数(方法仍重复创建)
function Student(name, age) {
this.name = name;
this.age = age;
this.study = function() { // 每个实例都会创建一个新的study函数
console.log(`${this.name}正在学习`);
};
}
const student3 = new Student('王五', 20);
const student4 = new Student('赵六', 21);
这两种方式都存在明显缺陷:方法重复创建。每个学生对象的“study”方法逻辑完全相同,却被重复定义在每个实例中,导致内存浪费。如果创建成千上万的学生对象,这种浪费会非常严重。
为了解决“属性/方法复用”的问题,JavaScript引入了“原型(prototype)”机制:将所有实例共享的属性和方法存放在一个公共对象中,每个实例都能访问这个公共对象的内容,从而实现复用。这个公共对象就是原型。
二、原型核心解析:三个关键对象与关联属性
原型体系的核心围绕三个关键对象展开:构造函数、实例对象、原型对象。它们之间通过两个特殊属性建立关联:prototype(构造函数的属性)和__proto__(实例对象的属性,ES6后标准化为Object.getPrototypeOf())。
1. 构造函数与prototype属性
在JavaScript中,任何函数都可以作为构造函数(通过new关键字调用)。每个构造函数都有一个内置的prototype属性,该属性指向一个原型对象。这个原型对象就是所有通过该构造函数创建的实例的“公共仓库”。
// 构造函数
function Student(name, age) {
this.name = name; // 实例私有属性
this.age = age; // 实例私有属性
}
// 向构造函数的prototype添加共享方法
Student.prototype.study = function() {
console.log(`${this.name}正在学习`);
};
// 向构造函数的prototype添加共享属性
Student.prototype.school = 'XX大学';
// 创建实例
const student1 = new Student('张三', 18);
const student2 = new Student('李四', 19);
// 实例访问共享方法和属性
student1.study(); // 张三正在学习
student2.study(); // 李四正在学习
console.log(student1.school); // XX大学
console.log(student2.school); // XX大学
// 验证两个实例的study方法是同一个
console.log(student1.study === student2.study); // true
通过上述代码可以发现:将study方法和school属性添加到Student.prototype后,所有Student实例都能访问到这些内容,且方法只被创建一次,实现了复用。
面试高频考点:构造函数的prototype属性的作用?答案是:用于存储该构造函数所有实例的共享属性和方法,实现属性/方法的复用,减少内存开销。
2. 实例对象与__proto__属性
当通过构造函数创建实例时,实例对象会自动生成一个内置属性__proto__(隐式原型),这个属性指向构造函数的prototype所指向的原型对象。正是通过__proto__,实例才能“找到”原型对象中的共享内容。
// 验证实例的__proto__指向构造函数的prototype
console.log(student1.__proto__ === Student.prototype); // true
console.log(student2.__proto__ === Student.prototype); // true
// ES6标准方法:获取实例的原型对象
console.log(Object.getPrototypeOf(student1) === Student.prototype); // true
需要注意的是,__proto__是实例的属性,而prototype是构造函数的属性,二者指向同一个原型对象,这是实例与原型对象建立关联的关键。
3. 原型对象与constructor属性
原型对象并非“孤立”的,它自身也有一个内置属性constructor(构造器),这个属性指向对应的构造函数。这就形成了一个闭环:构造函数 → 原型对象 → 构造函数。
// 验证原型对象的constructor指向构造函数
console.log(Student.prototype.constructor === Student); // true
// 实例可以通过__proto__访问到constructor
console.log(student1.__proto__.constructor === Student); // true
// 实例也可以直接访问constructor(通过原型链查找)
console.log(student1.constructor === Student); // true
这个闭环的作用非常重要:当我们不知道实例的构造函数时,可以通过instance.constructor快速获取,也可以用于修复被覆盖的原型对象的constructor属性(后续会讲)。
三、原型链:对象属性查找的“追溯机制”
原型对象本身也是一个对象,它同样有自己的__proto__属性,指向它的原型对象。这样一层一层追溯下去,就形成了一条链式结构,这就是原型链。原型链的尽头是Object.prototype,它的__proto__属性指向null,表示“没有更上层的原型了”。
1. 原型链的结构与属性查找规则
我们以Student实例为例,画出完整的原型链结构:
student1 → Student.prototype → Object.prototype → null
// 解释:
// student1的__proto__ → Student.prototype
// Student.prototype的__proto__ → Object.prototype
// Object.prototype的__proto__ → null
当我们访问一个对象的属性或方法时,JavaScript会遵循以下查找规则:
- 首先在对象自身上查找,如果找到则直接使用;
- 如果自身没有,则通过
__proto__查找其原型对象; - 如果原型对象上也没有,则继续通过原型对象的
__proto__向上追溯,直到Object.prototype; - 如果在
Object.prototype上仍未找到,则返回undefined(如果是方法则报错“XXX is not a function”)。
// 实例自身有name属性
console.log(student1.name); // 张三(自身查找)
// 实例自身没有study方法,查找原型对象Student.prototype
student1.study(); // 张三正在学习(原型对象查找)
// 实例自身和Student.prototype都没有toString方法,查找Object.prototype
console.log(student1.toString()); // [object Object](Object.prototype查找)
// 查找不存在的属性,追溯到null后返回undefined
console.log(student1.gender); // undefined
2. 原型链的核心作用:实现继承
原型链最核心的作用是实现JavaScript的“继承”特性。通过改变原型对象的指向,让一个构造函数的原型指向另一个构造函数的实例,就能实现属性和方法的继承。
// 父构造函数:Person
function Person(name) {
this.name = name;
this.eat = function() {
console.log(`${this.name}正在吃饭`);
};
}
// 父构造函数的原型方法
Person.prototype.sleep = function() {
console.log(`${this.name}正在睡觉`);
};
// 子构造函数:Student
function Student(name, age) {
// 继承父构造函数的私有属性
Person.call(this, name); // 改变Person的this指向为Student实例
this.age = age;
}
// 核心:让Student的原型指向Person的实例,实现原型链继承
Student.prototype = new Person();
// 修复constructor指向(因为Student.prototype被覆盖后,constructor指向了Person)
Student.prototype.constructor = Student;
// 子构造函数的原型方法
Student.prototype.study = function() {
console.log(`${this.name}正在学习`);
};
// 创建子实例
const student = new Student('张三', 18);
// 访问自身属性
console.log(student.age); // 18
// 访问继承自Person的私有方法
student.eat(); // 张三正在吃饭
// 访问继承自Person.prototype的方法
student.sleep(); // 张三正在睡觉
// 访问自身原型方法
student.study(); // 张三正在学习
// 验证原型链:student → Person实例 → Person.prototype → Object.prototype → null
console.log(student.__proto__ === Student.prototype); // true
console.log(Student.prototype.__proto__ === Person.prototype); // true
面试必考点:原型链继承时为什么要修复constructor?因为将Student.prototype = new Person()后,Student的原型对象变成了Person的实例,其constructor指向Person,这会导致通过student.constructor获取的构造函数不准确,修复后才能保证原型链的闭环正确。
四、常见原型相关API与实战技巧
除了上述核心概念,JavaScript还提供了多个操作原型的API,这些API在开发和面试中都高频出现,必须熟练掌握。
1. Object.getPrototypeOf() 与 Object.setPrototypeOf()
ES6标准化的原型操作方法,用于获取和设置对象的原型,替代了非标准的__proto__。
const obj = {};
const protoObj = { a: 1 };
// 设置obj的原型为protoObj
Object.setPrototypeOf(obj, protoObj);
// 获取obj的原型
console.log(Object.getPrototypeOf(obj) === protoObj); // true
console.log(obj.a); // 1(通过原型链查找)
注意:Object.setPrototypeOf()会直接修改对象的原型,可能影响性能,开发中应谨慎使用,优先通过构造函数的prototype定义原型。
2. Object.create(proto[, propertiesObject])
创建一个新对象,其原型指向指定的proto对象,第二个参数可选,用于定义新对象的自身属性。这是实现“原型式继承”的常用方法。
// 原型对象
const animalProto = {
eat() {
console.log(`${this.name}正在吃东西`);
}
};
// 创建新对象,原型为animalProto
const cat = Object.create(animalProto, {
name: {
value: '小花',
writable: true // 允许修改name属性
}
});
cat.eat(); // 小花正在吃东西
console.log(Object.getPrototypeOf(cat) === animalProto); // true
3. hasOwnProperty() 与 in 运算符
这两个方法常用于判断属性是否属于对象自身(而非原型链上的属性),是原型链查找的重要辅助工具。
- hasOwnProperty() :仅判断属性是否为对象自身的属性,不包括原型链上的属性;
- in 运算符:判断属性是否存在于对象或其原型链上。
function Student(name) {
this.name = name;
}
Student.prototype.school = 'XX大学';
const student = new Student('张三');
// hasOwnProperty:仅判断自身属性
console.log(student.hasOwnProperty('name')); // true(自身属性)
console.log(student.hasOwnProperty('school')); // false(原型链属性)
// in 运算符:判断自身或原型链属性
console.log('name' in student); // true
console.log('school' in student); // true
console.log('toString' in student); // true(Object.prototype上的属性)
面试考点:如何遍历对象自身的所有属性?答案:结合for...in循环和hasOwnProperty(),过滤掉原型链上的属性。
for (const key in student) {
if (student.hasOwnProperty(key)) {
console.log(key, student[key]); // 仅输出name: 张三
}
}
五、面试高频题
原型与原型链的面试题往往围绕“属性查找”“原型链结构”“继承实现”展开,以下是高频真题解析。
真题1:原型链属性查找与修改
题目:以下代码的输出结果是什么?
function Foo() {
this.a = 1;
}
Foo.prototype.a = 2;
Foo.prototype.b = 3;
const f1 = new Foo();
const f2 = new Foo();
console.log(f1.a); // ?
console.log(f1.b); // ?
f1.a = 4;
console.log(f1.a); // ?
console.log(f2.a); // ?
delete f1.a;
console.log(f1.a); // ?
delete Foo.prototype.a;
console.log(f1.a); // ?
答案:1 → 3 → 4 → 1 → 2 → undefined
解析:
f1.a:f1自身有a属性(值为1),优先取自身,输出1;f1.b:f1自身无b属性,原型链查找Foo.prototype的b(值为3),输出3;f1.a = 4:给f1自身赋值a=4,覆盖原型链属性,输出4;f2.a:f2自身a=1,与f1独立,输出1;delete f1.a:删除f1自身的a属性,再次查找时取原型链的a=2,输出2;delete Foo.prototype.a:删除原型的a属性,原型链上无a属性,输出undefined。
真题2:判断实例与构造函数的关系
题目:instanceof运算符的作用是什么?以下代码输出结果是什么?
function Person() {}
function Student() {}
Student.prototype = new Person();
const s = new Student();
console.log(s instanceof Student); // ?
console.log(s instanceof Person); // ?
console.log(s instanceof Object); // ?
console.log(Student instanceof Function); // ?
答案:true → true → true → true
解析:
instanceof的作用:判断一个实例是否属于某个构造函数,本质是判断“实例的原型链上是否存在该构造函数的prototype属性指向的原型对象”;s instanceof Student:s的原型链包含Student.prototype,true;s instanceof Person:s的原型链包含Person.prototype,true;s instanceof Object:s的原型链包含Object.prototype,true;Student instanceof Function:Student是函数,函数的原型链指向Function.prototype,true。
真题3:手写原型链继承(经典继承)
题目:实现一个Teacher构造函数,继承自Person构造函数,要求:
- Person有name、age属性和eat方法;
- Teacher有subject属性和teach方法;
- Teacher实例能访问自身、Teacher原型、Person原型、Object原型的属性和方法。
// 父构造函数
function Person(name, age) {
this.name = name;
this.age = age;
}
// 父原型方法
Person.prototype.eat = function() {
console.log(`${this.name}正在吃饭`);
};
// 子构造函数
function Teacher(name, age, subject) {
// 继承父构造函数的私有属性
Person.call(this, name, age);
// 子构造函数私有属性
this.subject = subject;
}
// 原型链继承:子原型指向父实例
Teacher.prototype = new Person();
// 修复constructor指向
Teacher.prototype.constructor = Teacher;
// 子原型方法
Teacher.prototype.teach = function() {
console.log(`${this.name}正在教${this.subject}`);
};
// 测试
const teacher = new Teacher('李老师', 30, '数学');
console.log(teacher.name); // 李老师(继承自Person)
console.log(teacher.subject); // 数学(自身属性)
teacher.eat(); // 李老师正在吃饭(继承自Person.prototype)
teacher.teach(); // 李老师正在教数学(自身原型方法)
console.log(teacher.toString()); // [object Object](继承自Object.prototype)
console.log(teacher instanceof Teacher); // true
console.log(teacher instanceof Person); // true
六、总结
原型与原型链是JavaScript的“基石”,面试考察的核心是对“属性复用”“链状查找”“继承实现”的理解。最后梳理核心考点:
- 三个核心关联:构造函数的
prototype指向原型对象,实例的__proto__指向原型对象,原型对象的constructor指向构造函数; - 原型链的结构:实例 → 构造函数.prototype → Object.prototype → null,属性查找遵循“自身→原型→上层原型→null”的规则;
- 继承的核心:通过“子构造函数.prototype = new 父构造函数()”改变原型指向,结合
Person.call(this)继承私有属性,同时修复constructor; - 关键API:
Object.getPrototypeOf()、Object.create()、hasOwnProperty()、instanceof的用法与区别; - 常见误区:实例修改属性不会影响原型,删除实例属性后会回归原型链查找,原型链继承时需修复构造器指向。