一、开篇直击:为什么原型链是 JS 的 “遗传密码”?
你是否有过这些困惑:
- 为什么 [] instanceof Array 是 true,{} instanceof Object 也是 true?
- 为什么给 Array.prototype 添加方法,所有数组实例都能直接调用?
- Vue 实例的 $mount、$emit 方法,到底存在哪里?
- 面试时被问 “手动实现继承”,却只能说出class extends,讲不清底层原理?
这些问题的答案,都指向 JS 的核心机制 ——原型链(Prototype Chain)。它不是语法糖,而是 JS 实现 “继承” 的底层逻辑,更是理解框架源码、写出优雅面向对象代码的关键。掌握原型链,才算真正打通 JS 的 “任督二脉”。
二、原型链的本质:3 个核心概念 + 1 条查找规则 + 内存模型
1. 先厘清 3 个易混淆概念(90% 的人在这里栽跟头)
概念 | 定义 | 关联关系 |
构造函数(Constructor) | 用于创建对象的函数(如 function Person() {}、Array、Object) | 构造函数有 prototype 属性 |
原型对象(Prototype) | 构造函数的 prototype 属性指向的对象,包含实例共享的方法和属性 | 原型对象有 constructor 属性,指向构造函数 |
实例(Instance) | 通过构造函数创建的对象(如 new Person()、[]、{}) | 实例有 __proto__ 属性,指向原型对象 |
核心公式(记死!):
实例.__proto__ === 构造函数.prototype
构造函数.prototype.constructor === 构造函数
实例.constructor === 构造函数(通过原型链继承而来)
2. 可视化案例:原型链的结构
// 1. 定义构造函数
function Person(name) {
this.name = name; // 实例私有属性
}
// 2. 给原型对象添加共享方法
Person.prototype.sayHi = function() {
console.log(`Hi, ${this.name}`);
};
// 3. 创建实例
const zhangsan = new Person("张三");
// 验证关联关系
console.log(zhangsan.__proto__ === Person.prototype); // true
console.log(Person.prototype.constructor === Person); // true
console.log(zhangsan.constructor === Person); // true
原型链结构图示(文字版):
zhangsan(实例)→ __proto__ → Person.prototype(原型对象)→ __proto__ → Object.prototype(顶层原型)→ __proto__ → null(原型链终点)
3. 原型链的核心作用:属性查找规则
当访问一个对象的属性 / 方法时,JS 会按以下顺序查找:
- 先在对象自身查找(如 zhangsan.name);
- 若找不到,沿 __proto__ 向上查找原型对象(如 zhangsan.sayHi() 来自 Person.prototype);
- 若仍找不到,继续沿原型链向上查找,直到 Object.prototype;
- 若 Object.prototype 中仍没有,返回 undefined。
示例验证:
console.log(zhangsan.toString()); // "[object Object]"
// 查找路径:zhangsan → 无toString → Person.prototype → 无toString → Object.prototype → 有toString
4. 底层补充:原型链的内存模型(95 分关键)
很多人只懂 “表面关联”,却不懂内存分配逻辑 —— 这是进阶高级工程师的核心差距:
- 实例的内存结构:每个实例仅存储 “自身私有属性”(如 zhangsan.name),原型对象的方法 / 属性(如 sayHi)不占用实例内存,仅通过 __proto__ 指针引用;
- 原型对象的内存特性:所有实例共享同一个原型对象的内存地址,因此修改原型对象的方法,所有实例都会 “实时感知”(如 Person.prototype.sayHi = () => {} 会影响所有 Person 实例);
- 内存释放条件:只有当 “实例被销毁” 且 “原型对象无其他引用” 时,原型对象才会被垃圾回收(GC)—— 这也是原型链可能导致内存泄漏的核心原因(如全局变量引用实例,实例引用原型对象)。
三、原型链的核心应用:JS 继承的 6 种实现方案(从基础到最优)
JS 本身没有 “类”(ES6 class 是语法糖,底层仍基于原型链),继承本质是 “原型链的复用”。以下是从基础到工业级的实现方案,附优缺点和实战选择:
1. 原型链继承(基础版)
// 父构造函数
function Parent() {
this.name = "父类";
}
Parent.prototype.getName = function() {
return this.name;
};
// 子构造函数
function Child() {}
Child.prototype = new Parent(); // 核心:子原型指向父实例
Child.prototype.constructor = Child; // 修复constructor指向
const child = new Child();
console.log(child.getName()); // "父类"(继承成功)
优点:简单直观,实现了原型方法复用;
缺点:父类私有属性会被所有子类实例共享(如 Parent 有数组属性,子类实例修改会相互影响);无法给父构造函数传参。
2. 构造函数继承(解决传参问题)
function Parent(name) {
this.name = name;
}
function Child(name) {
Parent.call(this, name); // 核心:调用父构造函数,绑定this
}
const child1 = new Child("张三");
const child2 = new Child("李四");
console.log(child1.name); // "张三"(不共享)
优点:父类私有属性不共享,支持给父构造函数传参;
缺点:原型方法无法继承(child1.getName() 会报错),方法只能定义在构造函数内,造成内存浪费。
3. 组合继承(原型链 + 构造函数,常用基础版)
function Parent(name) {
this.name = name;
}
Parent.prototype.getName = function() {
return this.name;
};
function Child(name, age) {
Parent.call(this, name); // 构造函数继承:私有属性
this.age = age;
}
Child.prototype = new Parent(); // 原型链继承:共享方法
Child.prototype.constructor = Child;
Child.prototype.getAge = function() {
return this.age;
};
const child = new Child("张三", 20);
console.log(child.getName()); // "张三"(继承原型方法)
console.log(child.age); // 20(私有属性)
优点:兼顾原型方法复用和私有属性独立,支持传参;
缺点:父构造函数会被调用两次(new Parent() 和 Parent.call()),造成不必要的性能开销。
4. 寄生组合继承(最优方案,框架源码常用)
解决组合继承的性能问题,核心是 “用父原型的副本替代父实例”:
function Parent(name) {
this.name = name;
}
Parent.prototype.getName = function() {
return this.name;
};
function Child(name, age) {
Parent.call(this, name); // 仅调用一次父构造函数
this.age = age;
}
// 核心:创建父原型的空对象副本(避免调用父构造函数)
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child; // 修复constructor
Child.prototype.getAge = function() {
return this.age;
};
优点:父构造函数仅调用一次,性能最优;兼顾所有优点,是工业级实现方案;
应用:Vue 源码中组件继承、React 早期的createClass继承,均基于此方案。
5. ES6 class 继承(语法糖,推荐实战使用)
class Parent {
constructor(name) {
this.name = name;
}
getName() { // 原型方法
return this.name;
}
static staticMethod() { // 静态方法(继承自类本身)
return "静态方法";
}
}
class Child extends Parent { // 核心:extends关键字
constructor(name, age) {
super(name); // 必须调用super,相当于Parent.call(this, name)
this.age = age;
}
getAge() {
return this.age;
}
}
const child = new Child("张三", 20);
console.log(child.getName()); // "张三"
console.log(Child.staticMethod()); // "静态方法"(静态继承)
本质:class + extends 是寄生组合继承的语法糖,底层仍基于原型链;
优点:语法简洁,支持静态方法继承,符合面向对象编程习惯;
实战选择:日常开发优先使用 ES6 class,需理解底层原理时参考寄生组合继承。
6. 混入继承(多继承场景)
JS 不支持多继承,但可通过 “混入(Mixin)” 实现多原型复用:
const Mixin1 = {
method1() { console.log("混入方法1"); }
};
const Mixin2 = {
method2() { console.log("混入方法2"); }
};
// 给Child原型添加混入方法
Object.assign(Child.prototype, Mixin1, Mixin2);
const child = new Child();
child.method1(); // "混入方法1"
child.method2(); // "混入方法2"
应用:Vue 的mixins选项、React 的HOC(高阶组件),本质是混入继承的延伸。
四、ES6 class 进阶:你不知道的底层细节(提分关键)
很多人用class却不懂其底层特性,这部分是面试高频加分项:
1. super 的双重角色
- 角色 1:作为函数:super(name) 相当于 Parent.call(this, name),必须在constructor内第一行调用(确保 this 绑定正确);
- 角色 2:作为对象:super.getName() 相当于 Parent.prototype.getName.call(this),可访问父类原型方法;
- 注意:在静态方法中,super 指向父类本身(如 super.staticMethod() 等价于 Parent.staticMethod())。
2. 私有字段与原型链的关系
ES6 新增的私有字段(# 前缀)不参与原型链继承,仅属于实例自身:
class Parent {
#privateField = "私有属性"; // 私有字段
getPrivate() {
return this.#privateField;
}
}
class Child extends Parent {}
const child = new Child();
console.log(child.getPrivate()); // "私有属性"(通过父类方法访问)
console.log(child.#privateField); // 报错:私有字段不可直接访问
console.log(Child.prototype.#privateField); // 报错:私有字段不在原型上
核心逻辑:私有字段存储在实例的 “私有槽位” 中,原型链无法访问,避免了原型链共享的问题。
3. 静态字段的继承原理
静态字段(static 关键字)存储在构造函数上,而非原型对象上,继承本质是 “子类构造函数引用父类静态字段”:
class Parent {
static staticField = "静态字段";
}
class Child extends Parent {}
console.log(Child.staticField); // "静态字段"(继承自Parent)
console.log(Child.prototype.staticField); // undefined(不在原型上)
底层逻辑:Child.staticField 是通过 Child.__proto__ = Parent 实现的 —— 子类构造函数的__proto__指向父类构造函数,因此能访问父类静态属性。
五、框架源码实战:原型链的工业级应用
1. Vue3 组件继承的底层实现
Vue3 的defineComponent本质是基于原型链的封装,组件的methods、computed等最终会挂载到组件实例的原型上:
// Vue3源码简化逻辑
function defineComponent(options) {
const Component = function() {};
// 原型链复用:将options.methods挂载到组件原型
Object.assign(Component.prototype, options.methods);
// 继承Vue内置方法(如$emit、$mount)
Component.prototype.__proto__ = Vue.prototype;
return Component;
}
// 组件使用
const MyComponent = defineComponent({
methods: {
handleClick() { console.log("点击"); }
}
});
const instance = new MyComponent();
instance.handleClick(); // 原型链查找:MyComponent.prototype → 存在
instance.$emit(); // 原型链查找:MyComponent.prototype → Vue.prototype → 存在
2. React 组件的原型链设计
React 的Component类是所有类组件的父类,底层基于 ES6 class继承,原型链结构如下:
MyComponent实例 → MyComponent.prototype → React.Component.prototype → Object.prototype → null
React 的生命周期方法(如componentDidMount)均定义在React.Component.prototype上,因此所有子类组件都能继承使用。
六、原型链的 “坑”:90% 开发者踩过的 5 个误区 + 进阶边界场景
1. 误区 1:__proto__ 与 prototype 混用
- 错误认知:认为实例有 prototype 属性,构造函数有 __proto__ 属性;
- 正确结论:只有构造函数(含Function)有 prototype;只有实例(含函数实例)有 __proto__;
- 例外:Function.prototype 是函数实例,但没有 prototype 属性(避免无限递归)。
2. 误区 2:修改原型对象后,已有实例失效
function Person() {}
const p1 = new Person();
// 错误写法:直接替换原型对象(已有实例的__proto__仍指向旧原型)
Person.prototype = { sayHi: () => {} };
console.log(p1.sayHi()); // 报错:sayHi is not a function
// 正确写法:修改原型对象的属性(不替换整个对象)
Person.prototype.sayHi = () => {};
console.log(p1.sayHi()); // 正常执行
3. 误区 3:instanceof 检测的是 “构造函数”,而非 “原型链”
- instanceof原理:检测构造函数的 prototype 是否在实例的原型链上;
- 示例:
console.log([] instanceof Array); // true(Array.prototype在[]的原型链上)
console.log([] instanceof Object); // true(Object.prototype在[]的原型链上)
console.log(Array instanceof Function); // true(Function.prototype在Array的原型链上)
4. 误区 4:原型链继承中,父类引用类型属性被共享
function Parent() {
this.hobbies = ["篮球"]; // 引用类型属性
}
function Child() {}
Child.prototype = new Parent();
const child1 = new Child();
const child2 = new Child();
child1.hobbies.push("足球");
console.log(child2.hobbies); // ["篮球", "足球"](意外共享)
解决方案:用构造函数继承或组合继承,将引用类型属性定义在构造函数内。
5. 误区 5:ES6 class 没有原型链
- 错误认知:class 是 “真正的类”,与原型链无关;
- 正确结论:class 是语法糖,Child extends Parent 本质是 Child.prototype.__proto__ = Parent.prototype,仍基于原型链实现继承。
6. 进阶边界场景:null 原型对象与原型链污染
- 场景 1:创建无原型对象:const obj = Object.create(null),此时 obj.__proto__ === undefined,原型链终点为null,不继承Object.prototype的任何方法(如toString、hasOwnProperty),适合作为纯净的字典对象;
- 场景 2:原型链污染(安全风险):恶意修改原型对象的属性,会影响所有实例:
// 原型链污染攻击
Object.prototype.__proto__.malicious = "恶意属性";
const obj = {};
console.log(obj.malicious); // "恶意属性"(所有对象都被污染)
防护方案:
- 避免直接修改 Object.prototype;
- 使用 hasOwnProperty 检测属性是否为对象自身属性(obj.hasOwnProperty('malicious'));
- 用 Object.create(null) 创建纯净对象,避免继承原型链属性。
七、面试高频考点:进阶真题解析(95 分必备)
真题 1:写出以下代码的输出结果(原型链 + 闭包结合)
function Parent() {
this.x = 100;
}
Parent.prototype.getX = function() {
return this.x;
};
function Child() {
Parent.call(this);
this.x = 200;
return {
x: 300,
getX: function() {
return this.x;
}
};
}
Child.prototype = new Parent();
Child.prototype.constructor = Child;
const child = new Child();
console.log(child.getX()); // 300(返回的对象自身有getX方法)
console.log(child.__proto__.getX.call(child)); // 100(Child.prototype的getX,this指向child返回的对象,x=300?不!这里易错:)
// 正确解析:
// 1. child是Child构造函数返回的对象({x:300, getX: ...}),其__proto__是Object.prototype(而非Child.prototype);
// 2. child.__proto__.getX 不存在,沿原型链查找Object.prototype也没有,会报错?不!再看:
// 3. Child.prototype是new Parent()创建的实例,有getX方法;但child的__proto__是Object.prototype,因此child.__proto__.getX 是undefined;
// 正确输出:child.getX() → 300;child.__proto__.getX → undefined(报错);
// 核心坑:构造函数返回对象时,实例的__proto__不再指向构造函数的prototype,而是Object.prototype。
真题 2:手动实现 ES6 class 的继承(含静态方法、super)
function myExtends(Child, Parent) {
// 1. 继承原型方法(寄生组合继承核心)
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
// 2. 继承静态方法(子类构造函数的__proto__指向父类构造函数)
Child.__proto__ = Parent;
// 3. 实现super(挂载到Child.prototype上)
Child.prototype.super = Parent;
}
// 使用示例
function Parent(name) {
this.name = name;
}
Parent.staticMethod = function() {
return "静态方法";
};
Parent.prototype.getName = function() {
return this.name;
};
function Child(name, age) {
this.super(name); // 相当于super(name)
this.age = age;
}
myExtends(Child, Parent);
Child.prototype.getAge = function() {
return this.age;
};
// 验证
const child = new Child("张三", 20);
console.log(child.getName()); // "张三"
console.log(Child.staticMethod()); // "静态方法"(继承静态方法)
真题 3:解释 Function.__proto__ === Function.prototype 的原因
答案核心:Function 是构造函数,同时也是函数实例 —— 所有函数实例的 __proto__ 都指向 Function.prototype,Function 作为函数实例,自然也遵循这一规则(避免原型链无限递归的特殊设计)。
延伸:Object.__proto__ === Function.prototype(因为 Object 是构造函数,属于函数实例),而 Function.prototype.__proto__ === Object.prototype(原型链的顶层关联)。
八、总结:原型链的 “道” 与 “术”
- 道:原型链是 JS 的底层机制,是 “对象复用” 的核心,ES6 class 只是其语法糖;
- 术:
- 日常开发用 class + extends 写继承(简洁高效);
- 看懂框架源码时,要能识别寄生组合继承、混入继承的本质;
- 避坑关键:分清 __proto__ 与 prototype,理解原型链查找规则,警惕原型链污染;
- 终极认知:JS 中 “一切皆对象”,而对象的 “遗传关系” 由原型链定义 —— 掌握原型链,才能真正理解 JS 的面向对象设计思想,看懂 Vue、React 等框架的底层实现。
原型链看似抽象,但只要抓住 “实例→原型对象→顶层原型” 的核心逻辑,结合内存模型、框架源码和进阶场景拆解,就能彻底掌握。它不仅是面试的 “加分项”,更是成为高级前端工程师的 “必修课”。