一文吃透 JavaScript 原型和原型链(含与 Python 的对比)

0 阅读8分钟

一文吃透 JavaScript 原型和原型链(含与 Python 的对比)

为什么所有实例能共享同一个方法?
prototype__proto__ 到底什么区别?
为什么说 JavaScript 是“基于原型”的语言,而 Python 是“基于类”的?
本文从零开始,用图解 + 代码 + 语言对比,带你一次性通关原型和原型链。


目录

  1. 前言:为什么需要原型?
  2. 构造函数:对象的“起手式”
  3. 原型对象:共享的“宝箱”
  4. prototype__proto__constructor 三兄弟
  5. 原型链:属性查找的“一镜到底”
  6. 函数也是对象:双重身份的奥义
  7. 一张终极关系图
  8. 与 Python 的对比:基于类 vs 基于原型
  9. 实际开发中的应用
  10. 总结与面试题

1. 前言:为什么需要原型?

假设我们需要创建多个学生对象,每个学生有 namegrade,并且都能 study

重复定义方法

function createStudent(name, grade) {
  return {
    name,
    grade,
    study() { console.log(`${this.name} 正在学习`); }
  };
}
const s1 = createStudent('张三', 1);
const s2 = createStudent('李四', 2);

问题:每个学生都独立保存了一个 study 函数,1000 个学生就有 1000 个相同的 study 函数,内存浪费严重。

原型共享方法

const studentProto = {
  study() { console.log(`${this.name} 正在学习`); }
};
function createStudent(name, grade) {
  const obj = Object.create(studentProto);
  obj.name = name;
  obj.grade = grade;
  return obj;
}

这就是原型的思想:公共方法存在一个对象上,实例通过原型链去访问。JavaScript 内部通过 prototype__proto__ 自动化了这个过程。


2. 构造函数:对象的“起手式”

构造函数和普通函数本质上没什么区别,只不过使用了 new 关键字创建对象的函数,被叫做了构造函数。构造函数的首字母一般大写(约定),用来区分普通函数。

function Person(name, age) {
  this.name = name;
  this.age = age;
  this.species = '人类';
  this.say = function() {
    console.log("Hello");
  };
}
let per1 = new Person('xiaoming', 20);

这里 speciessay 对于所有实例都一样,每创建一个实例就会重复创建一次,浪费内存。更好的做法是把它们放到原型对象上。


3. 原型对象:共享的“宝箱”

在 JavaScript 中,每一个函数类型的数据,都有一个叫做 prototype 的属性,这个属性指向的是一个对象,就是所谓的原型对象。原型对象用来存放实例对象的公有属性和公有方法。

function Person(name, age) {
  this.name = name;
  this.age = age;
}
Person.prototype.species = '人类';
Person.prototype.say = function() {
  console.log("Hello");
};

let per1 = new Person('xiaoming', 20);
let per2 = new Person('xiaohong', 19);

console.log(per1.species); // 人类
console.log(per2.species); // 人类
per1.say(); // Hello
per2.say(); // Hello

实例对象自己并没有 speciessay,为什么可以直接用点运算符访问?
因为当对象在自己身上找不到属性时,JavaScript 会自动去它的构造函数的 prototype 对象上找。

此外,原型对象还有一个 constructor 属性,指向它的构造函数:

console.log(per1.constructor === Person); // true

4. prototype__proto__constructor 三兄弟

  • prototype:函数才有的属性,指向一个对象(原型对象),用于存放实例的共享方法。
  • __proto__:每个对象都有的属性(包括实例、普通对象、函数),指向其构造函数的 prototype
  • constructor:原型对象上的属性,指向构造函数本身。
console.log(per1.__proto__ === Person.prototype); // true
console.log(Person.prototype.constructor === Person); // true

关系图

Person (构造函数) --prototype--> Person.prototype (原型对象)
                                    ↑
                                    | __proto__
                                 per1 (实例)

5. 原型链:属性查找的“一镜到底”

既然 __proto__ 是对象属性,而原型对象本身也是对象,那么原型对象也有自己的 __proto__。它指向哪?
原型对象是 Object 的实例,所以 Person.prototype.__proto__ === Object.prototype。而 Object.prototype 比较特殊,它的 __proto__ 指向 null,是原型链的终点。

当访问一个属性时,查找顺序是:
实例自身 → 实例.__proto__(即构造函数的 prototype)→ 构造函数的 prototype.__proto__(即 Object.prototype)→ … → null

这条由 __proto__ 串联起来的链条,就是原型链(也叫隐式原型链)。

console.log(per1.toString()); // 自身没有,Person.prototype 没有,Object.prototype 上有 toString

正因为原型链的存在,对象、数组、函数等才自带一些属性和方法(如 toStringhasOwnProperty)。


6. 函数也是对象:双重身份的奥义

在 JavaScript 中,函数也是一种特殊的对象。那么,函数有没有 __proto__?当然有。

所有函数都是 Function() 的实例。PersonObjectFunction 本身都是函数,所以它们的构造函数都是 Function()

console.log(Person.constructor === Function);   // true
console.log(Object.constructor === Function);   // true
console.log(Function.constructor === Function); // true

因此,函数的 __proto__ 指向 Function.prototype

console.log(Person.__proto__ === Function.prototype);   // true
console.log(Object.__proto__ === Function.prototype);   // true
console.log(Function.__proto__ === Function.prototype); // true

现在,我们可以画出一张完整的终极关系图。


7. 一张终极关系图

Function.prototype (对象)
       ↑
       | __proto__
    Person (函数) --prototype--> Person.prototype (对象)
                                   ↑
                                   | __proto__
                                per1 (实例)

同时:
Person.prototype.__proto__ === Object.prototype
Object.prototype.__proto__ === null

这张图可以总结为:

  • 每个函数都有一个 prototype 属性(指向原型对象)。
  • 每个对象都有一个 __proto__ 属性(指向构造函数的 prototype)。
  • 原型对象也是对象,所以也有 __proto__,最终指向 Object.prototype
  • Object.prototype.__proto__ === null,终点。

8. 与 Python 的对比:基于类 vs 基于原型

很多从 Python 转前端的开发者会觉得原型链很难理解。这是因为两种语言的设计哲学完全不同:Python 是基于类的(class-based),而 JavaScript 是基于原型的(prototype-based)

8.1 Python:类的“模具”思维

在 Python 中,定义一个类,然后通过 class 实例化对象。方法属于类,实例通过继承获得方法。

class Person:
    species = '人类'
    def __init__(self, name, age):
        self.name = name
        self.age = age
    def say(self):
        print("Hello")

p1 = Person("xiaoming", 20)
p2 = Person("xiaohong", 19)
  • 类是蓝图:实例是类制作出来的,类修改后已创建的实例不会自动更新(除非使用特殊机制)。
  • 继承结构class Student(Person): 定义了静态的继承关系。
  • 语言哲学:追求稳定、清晰、结构。

8.2 JavaScript:原型的“对象链接”思维

JavaScript 没有类(ES6 的 class 只是语法糖),只有对象。每个对象可以通过 __proto__ 链接到另一个对象,实现属性和方法的共享。

function Person(name, age) {
  this.name = name;
  this.age = age;
}
Person.prototype.species = '人类';
Person.prototype.say = function() { console.log("Hello"); };
  • 原型是运行时链接:实例通过 __proto__ 指向原型对象,原型对象可以在运行时被修改,影响所有指向它的实例。
  • 继承实现Student.prototype = Object.create(Person.prototype); 动态建立链。
  • 语言哲学:追求灵活、动态、极致复用。

8.3 核心差异对比表

维度Python (基于类)JavaScript (基于原型)
核心概念class 作为蓝图对象直接链接对象 (__proto__)
继承方式静态的 class A(B):动态的 Object.create(B.prototype)
修改原型/类的影响通常不影响已创建实例修改原型立即影响所有指向它的实例
属性查找先实例 → 类 → 父类 → ...先自身 → __proto____proto__.__proto__ → ...
语言哲学稳定、清晰、适合大型工程灵活、动态、适合快速迭代和原型开发

8.4 一个生动的比喻

  • Python:像一个蛋糕模具。你用它做出一个个蛋糕(实例)。模具本身不会改变,修改模具后新蛋糕用新模具,旧蛋糕保持不变。
  • JavaScript:像一个乐高积木。每个积木(对象)可以拼接在另一个积木上(通过 __proto__),你随时可以把一块积木换成另一块,所有拼接在一起的结构都会受影响。

9. 实际开发中的应用

9.1 内存优化

把公共方法放在 prototype 上,避免每个实例都创建一份函数。

9.2 动态扩展

Person.prototype.walk = function() { console.log('walking'); };
per1.walk(); // 即使 per1 已创建,也能立即使用新方法

9.3 实现继承(ES5)

function Student(name, age, grade) {
  Person.call(this, name, age);
  this.grade = grade;
}
Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student;
Student.prototype.study = function() { console.log('studying'); };

9.4 理解 instanceof

console.log(per1 instanceof Person); // true

instanceof 检查的是 per1.__proto__ 链上是否存在 Person.prototype


10. 总结与面试题

10.1 核心知识点回顾

  • 构造函数:使用 new 调用,首字母大写(约定)。
  • prototype:函数特有的属性,指向一个对象,存放实例共享的属性和方法。
  • __proto__:所有对象都有的属性,指向其构造函数的 prototype
  • 原型链:通过 __proto__ 串联起来的链条,直到 null,用于属性查找。
  • 函数也是对象:函数既有 __proto__(作为对象),也有 prototype(作为构造函数)。
  • 与 Python 的区别:Python 基于类(静态蓝图),JavaScript 基于原型(动态链接)。

10.2 经典面试题

function Foo() {}
Foo.prototype.x = 10;
const obj = new Foo();
console.log(obj.x);      // 10
obj.x = 20;
console.log(obj.x);      // 20
delete obj.x;
console.log(obj.x);      // 10
delete Foo.prototype.x;
console.log(obj.x);      // undefined

解释:属性访问优先自身,自身没有则去原型链找。删除自身属性后恢复原型值,删除原型值后最终找不到。

10.3 手写 instanceof

function myInstanceof(left, right) {
  let proto = Object.getPrototypeOf(left);
  while (proto) {
    if (proto === right.prototype) return true;
    proto = Object.getPrototypeOf(proto);
  }
  return false;
}

10.4 手写 Object.create

function myCreate(proto) {
  function F() {}
  F.prototype = proto;
  return new F();
}

写在最后

原型和原型链是 JavaScript 这座大厦的地基。理解了它,你就能:

  • 看懂 Vue/React 源码中的继承和扩展机制。
  • 写出更优雅的插件和库。
  • 轻松理解 ES6 class 的本质(语法糖)。
  • 在面试中自信地推导各种原型链题目。

希望这篇文章能帮你彻底通关原型。如果你还有疑问,欢迎在评论区留言,我们一起探讨。


本文参考了掘金作者 hobby爱吃猫的鱼 的精彩文章,并在其基础上扩展了语言对比和实际应用。