"讲讲原型链" —— 面试官最爱问的 JavaScript 基础

JavaScript 原型与原型链:从困惑到完全理解

以前在看 JavaScript 代码的时候,经常会遇到一个问题:

javascript 复制代码
const arr = [1, 2, 3];
arr.push(4);      // 4
arr.join(',');    // "1,2,3,4"
arr.toString();   // "1,2,3,4"

我明明只创建了一个数组,为什么它能调用 pushjointoString 这些方法?这些方法是从哪来的?

再看这段代码:

javascript 复制代码
function Person(name) {
  this.name = name;
}

Person.prototype.sayHello = function() {
  console.log(`Hello, I'm ${this.name}`);
};

const person = new Person('张三');
person.sayHello(); // "Hello, I'm 张三"

person 对象本身没有 sayHello 方法,但却能调用它。这背后的机制就是原型链。


先搞清楚几个概念

在深入之前,先把几个容易混淆的概念理清楚:

[[Prototype]]__proto__prototype 的区别

概念 是什么 属于谁 作用
[[Prototype]] 内部属性 所有对象 指向对象的原型,隐藏属性
__proto__ 访问器属性 所有对象 暴露 [[Prototype]],非标准但广泛支持
prototype 普通属性 函数 存放给实例共享的属性和方法

简单说:

  • prototype函数才有的属性,用来存放共享方法
  • __proto__所有对象都有的属性,指向它的原型对象
  • [[Prototype]]__proto__ 的内部实现
javascript 复制代码
function Foo() {}
const foo = new Foo();

// prototype 只有函数才有
console.log(Foo.prototype);      // {constructor: ƒ}
console.log(foo.prototype);      // undefined

// __proto__ 所有对象都有
console.log(foo.__proto__ === Foo.prototype);  // true

现代写法:Object.getPrototypeOf()

__proto__ 虽然好用,但它不是 ECMAScript 标准的一部分,只是各浏览器都实现了。推荐用标准方法:

javascript 复制代码
// 获取原型
Object.getPrototypeOf(foo) === Foo.prototype  // true

// 设置原型
Object.setPrototypeOf(obj, prototype)

// 创建时指定原型
Object.create(prototype)

原型是什么

JavaScript 里每个函数都有一个 prototype 属性,指向一个对象。这个对象叫做原型对象,它的作用是让该函数创建的所有实例共享属性和方法。

javascript 复制代码
function Car(brand) {
  this.brand = brand;
}

// 方法定义在原型上,所有实例共享
Car.prototype.start = function() {
  console.log(`${this.brand} 启动了`);
};

const car1 = new Car('丰田');
const car2 = new Car('本田');

car1.start(); // 丰田 启动了
car2.start(); // 本田 启动了

// 两个实例用的是同一个方法
console.log(car1.start === car2.start); // true

这就是原型的核心价值:方法只需要定义一次,所有实例都能用

如果把方法定义在构造函数里,每创建一个实例就会新建一个函数,浪费内存:

javascript 复制代码
// 不推荐的写法
function BadCar(brand) {
  this.brand = brand;
  this.start = function() {  // 每个实例都有一份
    console.log(`${this.brand} 启动了`);
  };
}

const bad1 = new BadCar('丰田');
const bad2 = new BadCar('本田');
console.log(bad1.start === bad2.start); // false,两个不同的函数

new 关键字到底做了什么

理解原型链之前,得先搞清楚 new 的工作原理。当你写 new Foo() 时,JavaScript 引擎会执行以下四个步骤:

flowchart LR A['1. 创建空对象']:::step --> B['2. 设置原型链']:::step B --> C['3. 执行构造函数']:::step C --> D['4. 返回对象']:::success classDef step fill:#cce5ff,stroke:#0d6efd,color:#004085 classDef success fill:#d4edda,stroke:#28a745,color:#155724

详细步骤

javascript 复制代码
function Person(name) {
  this.name = name;
}
Person.prototype.greet = function() {
  console.log(`Hi, I'm ${this.name}`);
};

const john = new Person('John');

Step 1:创建一个空对象

javascript 复制代码
// 内部创建:{}

Step 2:将空对象的 [[Prototype]] 指向构造函数的 prototype

javascript 复制代码
// 内部操作:newObj.__proto__ = Person.prototype

Step 3:用这个空对象作为 this 执行构造函数

javascript 复制代码
// 内部操作:Person.call(newObj, 'John')
// 执行后 newObj 变成 { name: 'John' }

Step 4:返回对象

  • 如果构造函数返回一个对象,就用那个对象
  • 否则返回 Step 1 创建的对象

手写一个 new

理解了原理,可以自己实现一个:

javascript 复制代码
function myNew(Constructor, ...args) {
  // 1. 创建空对象,原型指向构造函数的 prototype
  const obj = Object.create(Constructor.prototype);

  // 2. 执行构造函数,this 绑定到新对象
  const result = Constructor.apply(obj, args);

  // 3. 如果构造函数返回对象,就用它;否则用新创建的对象
  return result instanceof Object ? result : obj;
}

// 测试
const p = myNew(Person, 'Alice');
p.greet(); // Hi, I'm Alice
console.log(p instanceof Person); // true

原型链的查找机制

当访问对象的属性或方法时,JavaScript 会按照这个顺序查找:

  1. 先在对象自身找
  2. 找不到,去对象的原型 (__proto__) 上找
  3. 还找不到,继续往上一级原型找
  4. 直到 Object.prototype,再往上就是 null

这条查找链路就是原型链

flowchart TB A["dog 实例
{ name: 'Buddy' }"]:::instance -->|__proto__| B["Dog.prototype
{ bark: ƒ }"]:::proto B -->|__proto__| C["Animal.prototype
{ speak: ƒ }"]:::proto C -->|__proto__| D["Object.prototype
{ toString: ƒ, ... }"]:::rootProto D -->|__proto__| E["null"]:::endNode classDef instance fill:#cce5ff,stroke:#0d6efd,color:#004085 classDef proto fill:#d4edda,stroke:#28a745,color:#155724 classDef rootProto fill:#fff3cd,stroke:#ffc107,color:#856404 classDef endNode fill:#f8d7da,stroke:#dc3545,color:#721c24

代码示例

javascript 复制代码
function Animal(name) {
  this.name = name;
}

Animal.prototype.speak = function() {
  console.log(`${this.name} makes a sound`);
};

function Dog(name) {
  Animal.call(this, name);
}

// 建立原型链:Dog.prototype -> Animal.prototype
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

Dog.prototype.bark = function() {
  console.log('Woof!');
};

const dog = new Dog('Buddy');

// 查找过程:
dog.name;    // 在 dog 自身找到
dog.bark();  // 在 Dog.prototype 找到
dog.speak(); // 在 Animal.prototype 找到
dog.toString(); // 在 Object.prototype 找到

用代码验证这条链:

javascript 复制代码
console.log(dog.__proto__ === Dog.prototype);                 // true
console.log(Dog.prototype.__proto__ === Animal.prototype);    // true
console.log(Animal.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__ === null);             // true

这就解释了开头的问题。数组能调用 pushjoin,是因为这些方法定义在 Array.prototype 上。能调用 toString,是因为顺着原型链能找到 Object.prototype.toString(虽然 Array 重写了这个方法)。


完整的原型链图谱

JavaScript 的原型链比想象中更复杂,函数本身也是对象,也有自己的原型链:

flowchart TB subgraph IL[实例层] foo["foo 实例"]:::instance end subgraph PL[原型层] FooP["Foo.prototype"]:::proto ObjP["Object.prototype"]:::rootProto end subgraph FL[函数层] Foo["Foo 函数"]:::func Obj["Object 函数"]:::func Func["Function 函数"]:::func end subgraph FPL[函数原型层] FuncP["Function.prototype"]:::funcProto end foo -->|__proto__| FooP FooP -->|__proto__| ObjP ObjP -->|__proto__| NULL["null"]:::endNode Foo -->|prototype| FooP Foo -->|__proto__| FuncP Obj -->|prototype| ObjP Obj -->|__proto__| FuncP Func -->|prototype| FuncP Func -->|__proto__| FuncP FuncP -->|__proto__| ObjP classDef instance fill:#cce5ff,stroke:#0d6efd,color:#004085 classDef proto fill:#d4edda,stroke:#28a745,color:#155724 classDef rootProto fill:#fff3cd,stroke:#ffc107,color:#856404 classDef func fill:#e2d9f3,stroke:#6f42c1,color:#432874 classDef funcProto fill:#fce4ec,stroke:#e91e63,color:#880e4f classDef endNode fill:#f8d7da,stroke:#dc3545,color:#721c24 style IL fill:#e8f4fc,stroke:#0d6efd style PL fill:#e8f5e9,stroke:#28a745 style FL fill:#f3e5f5,stroke:#6f42c1 style FPL fill:#fce4ec,stroke:#e91e63

几个关键点

1. 所有函数都是 Function 的实例

javascript 复制代码
console.log(Foo.__proto__ === Function.prototype);    // true
console.log(Object.__proto__ === Function.prototype); // true
console.log(Function.__proto__ === Function.prototype); // true(自己创建自己)

2. Function.prototype 也是对象,它的原型是 Object.prototype

javascript 复制代码
console.log(Function.prototype.__proto__ === Object.prototype); // true

3. Object.prototype 是原型链的终点

javascript 复制代码
console.log(Object.prototype.__proto__ === null); // true

4. 一个有趣的循环

javascript 复制代码
// Object 是函数,所以它的 __proto__ 是 Function.prototype
console.log(Object.__proto__ === Function.prototype); // true

// Function.prototype 是对象,所以它的 __proto__ 是 Object.prototype
console.log(Function.prototype.__proto__ === Object.prototype); // true

// 这形成了一个有趣的"鸡生蛋蛋生鸡"的关系

属性遮蔽(Property Shadowing)

如果对象自身和原型上有同名属性,会发生什么?

javascript 复制代码
function Person(name) {
  this.name = name;
}

Person.prototype.name = 'Default';
Person.prototype.greet = function() {
  console.log(`Hello, ${this.name}`);
};

const john = new Person('John');

// 自身属性遮蔽原型属性
console.log(john.name); // 'John',不是 'Default'

// 删除自身属性后,原型属性就露出来了
delete john.name;
console.log(john.name); // 'Default'

这就是属性遮蔽:自身属性会"遮住"原型链上的同名属性。

检查属性来源

javascript 复制代码
const john = new Person('John');

// hasOwnProperty 只检查自身属性
console.log(john.hasOwnProperty('name'));  // true
console.log(john.hasOwnProperty('greet')); // false

// in 操作符检查整个原型链
console.log('name' in john);  // true
console.log('greet' in john); // true

实现继承

理解了原型链,继承就好办了。核心就两步:

  1. 调用父构造函数,继承实例属性
  2. 设置原型链,继承原型方法
javascript 复制代码
function Vehicle(type) {
  this.type = type;
  this.speed = 0;
}

Vehicle.prototype.accelerate = function(amount) {
  this.speed += amount;
  console.log(`${this.type} 加速到 ${this.speed} km/h`);
};

function Car(brand) {
  Vehicle.call(this, '汽车');  // 继承实例属性
  this.brand = brand;
}

Car.prototype = Object.create(Vehicle.prototype);  // 继承原型方法
Car.prototype.constructor = Car;

// 添加子类特有的方法
Car.prototype.honk = function() {
  console.log(`${this.brand} 鸣笛`);
};

// 重写父类方法
Car.prototype.accelerate = function(amount) {
  Vehicle.prototype.accelerate.call(this, amount);
  if (this.speed > 120) {
    console.log('超速警告');
  }
};

const myCar = new Car('丰田');
myCar.accelerate(50);   // 汽车 加速到 50 km/h
myCar.accelerate(80);   // 汽车 加速到 130 km/h
                        // 超速警告
myCar.honk();           // 丰田 鸣笛

为什么用 Object.create() 而不是直接赋值

javascript 复制代码
// 错误写法
Car.prototype = Vehicle.prototype;
// 问题:修改 Car.prototype 会影响 Vehicle.prototype

// 错误写法
Car.prototype = new Vehicle();
// 问题:会执行 Vehicle 构造函数,可能有副作用

// 正确写法
Car.prototype = Object.create(Vehicle.prototype);
// 创建一个新对象,原型指向 Vehicle.prototype

ES6 的 class 语法

ES6 引入了 class 关键字,写起来更清爽:

javascript 复制代码
class Animal {
  constructor(name) {
    this.name = name;
  }

  speak() {
    console.log(`${this.name} makes a sound`);
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name);
    this.breed = breed;
  }

  bark() {
    console.log('Woof!');
  }
}

const dog = new Dog('Buddy', 'Labrador');
dog.speak(); // Buddy makes a sound
dog.bark();  // Woof!

但要清楚,class 只是语法糖,底层还是原型链那套:

javascript 复制代码
console.log(typeof Dog); // "function"
console.log(dog.__proto__ === Dog.prototype); // true
console.log(Dog.prototype.__proto__ === Animal.prototype); // true

class 的一些特性

javascript 复制代码
class Example {
  // 实例属性(ES2022+)
  instanceProp = 'instance';

  // 私有属性(ES2022+)
  #privateProp = 'private';

  // 静态属性
  static staticProp = 'static';

  // 静态方法
  static staticMethod() {
    return 'static method';
  }

  // getter/setter
  get value() {
    return this.#privateProp;
  }
}

几个容易踩的坑

1. 引用类型放原型上会共享

javascript 复制代码
function Student(name) {
  this.name = name;
}

Student.prototype.hobbies = [];  // 所有实例共享这个数组

const s1 = new Student('张三');
const s2 = new Student('李四');

s1.hobbies.push('reading');
console.log(s2.hobbies); // ['reading']  // s2 也有了,出问题了

引用类型(数组、对象)应该放在构造函数里:

javascript 复制代码
function Student(name) {
  this.name = name;
  this.hobbies = [];  // 每个实例独立
}

2. 别直接替换 prototype 对象

javascript 复制代码
function Foo() {}

// 直接替换 prototype 会丢失 constructor
Foo.prototype = {
  method: function() {}
};

const foo = new Foo();
console.log(foo.constructor === Foo); // false,变成 Object 了

要么记得补上 constructor,要么用属性添加的方式:

javascript 复制代码
// 方式一:补上 constructor
Foo.prototype = {
  constructor: Foo,
  method: function() {}
};

// 方式二:直接添加属性(推荐)
Foo.prototype.method = function() {};

3. 箭头函数不能用作构造函数

javascript 复制代码
const Foo = () => {};
const foo = new Foo(); // TypeError: Foo is not a constructor

箭头函数没有 prototype 属性,也没有自己的 this,所以不能用 new

4. instanceof 的局限性

javascript 复制代码
// instanceof 检查的是原型链
console.log([] instanceof Array);  // true
console.log([] instanceof Object); // true

// 跨 iframe/realm 时会失效
// iframe 里的 Array 和主页面的 Array 不是同一个

更可靠的类型检查:

javascript 复制代码
Object.prototype.toString.call([]);  // "[object Array]"
Array.isArray([]);  // true

性能考虑

原型链查找有开销

属性查找会沿着原型链向上,链越长开销越大。虽然现代引擎有优化,但还是要注意:

javascript 复制代码
// 如果频繁访问原型链上的属性,可以缓存
const method = obj.someMethod;
for (let i = 0; i < 1000000; i++) {
  method.call(obj);  // 比 obj.someMethod() 快
}

Object.create(null) 创建纯净对象

javascript 复制代码
// 普通对象会继承 Object.prototype
const obj = {};
console.log(obj.toString); // ƒ toString() { [native code] }

// 纯净对象没有原型链
const pureObj = Object.create(null);
console.log(pureObj.toString); // undefined

// 适合用作字典/哈希表,不用担心键名冲突
const dict = Object.create(null);
dict['hasOwnProperty'] = 'safe';  // 不会覆盖原型方法

小结

原型链说穿了就是一条查找链:找属性时从对象自身开始,顺着 __proto__ 一路往上找,直到 null

几个要点:

  • prototype 是函数的属性,用于存放共享的方法
  • __proto__(或 [[Prototype]])是对象的属性,指向它的原型
  • 推荐用 Object.getPrototypeOf() 代替 __proto__
  • new 关键字做了四件事:创建对象、设置原型、执行构造函数、返回对象
  • 方法定义在原型上,省内存
  • class 是语法糖,底层还是原型链
  • Object.prototype 是原型链的终点,它的 __proto__null

理解了这个机制,再看 JavaScript 的面向对象就清晰多了。框架源码里大量使用原型链,比如 Vue 2 的响应式系统、各种插件的 mixin 实现,都是基于这套机制。


如果你觉得这篇文章有帮助,欢迎关注我的 GitHub,下面是我的一些开源项目:

Claude Code Skills (按需加载,意图自动识别,不浪费 token,介绍文章):

全栈项目(适合学习现代技术栈):

  • prompt-vault - Prompt 管理器,用的都是最新的技术栈,适合用来学习了解最新的前端全栈开发范式:Next.js 15 + React 19 + tRPC 11 + Supabase 全栈示例,clone 下来配个免费 Supabase 就能跑
  • chat_edit - 双模式 AI 应用(聊天+富文本编辑),Vue 3.5 + TypeScript + Vite 5 + Quill 2.0 + IndexedDB

参考资料

相关推荐
用户47949283569151 小时前
2025 年 TC39 都在忙什么?Import Bytes、Iterator Chunking 来了
前端·javascript·面试
大怪v2 小时前
【Virtual World 04】我们的目标,无限宇宙!!
前端·javascript·代码规范
努力学算法的蒟蒻2 小时前
day27(12.7)——leetcode面试经典150
算法·leetcode·面试
狂炫冰美式3 小时前
不谈技术,搞点文化 🧀 —— 从复活一句明代残诗破局产品迭代
前端·人工智能·后端
xw53 小时前
npm几个实用命令
前端·npm
!win !3 小时前
npm几个实用命令
前端·npm
代码狂想家4 小时前
使用openEuler从零构建用户管理系统Web应用平台
前端
dorisrv5 小时前
优雅的React表单状态管理
前端
蓝瑟5 小时前
告别重复造轮子!业务组件多场景复用实战指南
前端·javascript·设计模式