×

JavaScript原型链是怎么工作的?

作者:Terry2025.07.04来源:Web前端之家浏览:47评论:0

JS原型链

刚开始学JavaScript的时候,很多人都会被“原型链”搞晕——明明自己写的对象里没这个属性,调用的时候咋就有了?修改原型上的方法,为啥所有实例都跟着变?原型链到底是咋运作的?今天咱们就把这事儿拆明白,从根儿上理解JavaScript原型链的工作逻辑。

先搞懂“原型”的基本角色

要理解原型链,得先分清构造函数原型对象实例这几个关键角色,以及它们之间的关联。

举个例子:用构造函数创建对象时,像这样写代码:

function Person(name) {
  this.name = name; // 给实例自身加属性
}
const p1 = new Person('小明'); // 创建Person的实例p1

这里的 Person构造函数(本质是普通函数,用 new 调用时才当“构造器”用),每个构造函数都有个特殊属性 prototype,它指向的对象就是原型对象——可以理解成“所有实例的公共模板”。

那实例和原型对象咋关联?每个实例(p1)内部有个隐式原型 __proto__(现在更推荐用 Object.getPrototypeOf() 来获取,更规范),它会“指向”构造函数的 prototype,所以执行 Object.getPrototypeOf(p1) === Person.prototype 时,结果是 true

原型对象用来存啥?一般放公共属性或方法,比如所有 Person 实例都需要 sayHi 方法,就可以写到原型上:

Person.prototype.sayHi = function() {
  console.log(`我是${this.name}`);
};
p1.sayHi(); // 输出“我是小明”

这么做的好处是节省内存——所有 Person 实例共享这个方法,不用每个实例都单独存一份。

原型链的“链条”是怎么串起来的?

原型链的核心逻辑是:每个对象的隐式原型(__proto__)指向父级原型对象,父级原型对象的隐式原型又指向更上层的原型,直到终点 null,就像糖葫芦一样,一个串一个,形成一条“链”。

还是用上面的 Person 例子,咱们一步步扒开这条链:

  1. 实例 p1__proto__Person.prototype(构造函数的原型对象);

  2. Person.prototype 本身也是个对象,它的 __proto__Object.prototype(JavaScript里所有对象的“根原型”);

  3. Object.prototype__proto__null(原型链的终点,没有更上层了)。

用代码验证更直观:

console.log(Object.getPrototypeOf(p1) === Person.prototype); // true
console.log(Object.getPrototypeOf(Person.prototype) === Object.prototype); // true
console.log(Object.getPrototypeOf(Object.prototype)); // null

再延伸到内置构造函数(比如数组),逻辑也一样:

const arr = [1,2,3];
console.log(Object.getPrototypeOf(arr) === Array.prototype); // true
console.log(Object.getPrototypeOf(Array.prototype) === Object.prototype); // true
console.log(Object.getPrototypeOf(Object.prototype)); // null

不管是自定义构造函数,还是JS内置的 ArrayFunctionObject,它们的实例都会通过 __proto__ 一层一层往上找,形成原型链

原型链的“查找规则”是怎么运行的?

当我们访问一个对象的属性/方法时,JavaScript会触发「原型链查找」,规则是这样的:

  1. 先在对象自身找有没有这个属性/方法;

  2. 如果没找到,就去对象的隐式原型(__proto__指向的原型对象里找;

  3. 如果还没找到,就去原型对象的隐式原型里找(也就是父级原型);

  4. 以此类推,直到找到属性/方法,或者碰到原型链终点 null(这时返回 undefined)。

举个“捉迷藏”的例子:

Person 原型和实例玩属性/方法的“捉迷藏”:

function Person(name) {
  this.name = name;
}
// 给原型加公共属性和方法
Person.prototype.age = 18; 
Person.prototype.sayHi = function() {
  console.log(`我是${this.name},今年${this.age}岁`);
};
const p1 = new Person('小明');
const p2 = new Person('小红');

情况1:自身没有,原型有

console.log(p1.age); // 18(p1自身没age,去Person.prototype找)
p1.sayHi(); // 我是小明,今年18岁(方法在原型上)

情况2:自身有,原型也有 → 自身优先

p1.age = 20; // 给p1自身加age属性
console.log(p1.age); // 20(自身有,不再去原型找)
console.log(p2.age); // 18(p2自身没age,还是原型的18)

情况3:原型链上层也有

console.log(p1.toString()); // "[object Object]" 
// p1自身没有toString → 去Person.prototype找(没有)→ 再去Object.prototype找(有toString方法)

注意!赋值操作不走原型链

比如给 p1.age 赋值时,只是在p1自身添加属性,不会修改原型上的 age,只有读取操作console.log(p1.age))才会触发原型链查找。

原型链的终点为什么是null

前面例子里看到,Object.prototype.__proto__ 的值是 null,这是JavaScript的设计选择——null 表示“没有原型”,它是原型链的终点,用来终止查找过程。

试想如果没有这个终点会怎样?比如让 Object.prototype__proto__ 指向自己,那查找属性时就会无限循环,程序直接卡死,所以用 null 当终点,既能避免循环引用,又能明确“没找到”的边界。

可以把原型链想象成“家族族谱”:Object.prototype 是所有对象的“老祖宗”,再往上没长辈了,所以老祖宗的“父级”是 null

原型链在实际开发中有哪些用?

原型链看着抽象,实际开发中到处都有它的影子,最典型的场景是实现继承共享公共逻辑

(1)原型式继承

比如要创建 Student 构造函数,让它继承 Person 的属性和方法,可以通过修改原型链实现:

function Student(name, grade) {
  Person.call(this, name); // 继承“自身属性”(把name传给Person构造函数)
  this.grade = grade; // 学生特有的属性
}
// 让Student的原型“指向”Person的原型,建立继承关系
Student.prototype = Object.create(Person.prototype); 
Student.prototype.constructor = Student; // 修正constructor的指向(否则会指向Person)
const s1 = new Student('小李', '三年级');
s1.sayHi(); // 能调用Person原型上的sayHi方法

这里 Object.create(Person.prototype) 会创建一个新对象,它的 __proto__ 指向 Person.prototype,然后把 Student.prototype 设为这个新对象——这样 Student 的实例就能通过原型链访问到 Person 原型上的内容,实现“继承”。

(2)封装公共方法/属性

前面提过,把方法放到原型上,所有实例共享,能节省内存,比如React里的类组件,方法都是定义在原型上的;再比如数组的 pushpop 等方法,其实都存在 Array.prototype 上,所有数组实例都能调用。

(3)扩展内置对象(谨慎使用)

有时候会看到给 Array.prototype 加自定义方法,

Array.prototype.myMap = function(callback) {
  const res = [];
  for(let i=0; i<this.length; i++){
    res.push(callback(this[i], i, this));
  }
  return res;
};
[1,2,3].myMap(x=>x*2); // [2,4,6]

这种做法有争议(可能污染全局、和未来内置方法重名),但能帮我们理解:内置对象的实例能调用原型方法,靠的就是原型链

容易踩的“原型链陷阱”有哪些?

原型链虽强,但稍不注意就会掉坑里,这几个场景要特别小心:

(1)直接替换原型对象,切断继承链

如果用字面量直接给构造函数的 prototype 赋值,会改变原型的指向,导致已有实例的原型链断裂:

function Person() {}
const p1 = new Person();
// 直接替换prototype(危险操作!)
Person.prototype = {
  sayHi() { console.log('Hi'); }
};
const p2 = new Person();
p1.sayHi(); // 报错!p1的__proto__还是旧的Person.prototype(没有sayHi)
p2.sayHi(); // 正常,p2的__proto__是新的原型对象

解决办法:如果要给原型加方法,逐个添加Person.prototype.sayHi = ...),而不是整体替换,如果必须替换,要手动修正原型链(但实际开发中很少这么做)。

(2)原型上的引用类型属性被共享

如果原型上放的是数组、对象这类引用类型,所有实例都会共享这个值,一个实例修改,其他实例也会受影响:

function Person() {}
Person.prototype.friends = []; // 原型上放引用类型(数组)
const p1 = new Person();
const p2 = new Person();
p1.friends.push('小张');
console.log(p2.friends); // ["小张"] → p2的friends也被改了

这是因为 p1.friends 读取时,找到原型上的 friends 数组(引用类型),push 操作修改的是这个数组本身。解决办法:把引用类型属性放到构造函数里(每个实例自己存一份),或者在实例初始化时深拷贝原型属性。

(3)误操作内置对象的原型

前面说过,给 ArrayObject 等内置对象的原型加方法,可能和未来新增的标准方法冲突,比如现在给 Array.prototype 加个 flatten 方法,过两年JS内置了 Array.prototype.flatten,就会出现命名冲突,导致逻辑混乱,所以非必要不扩展内置原型

原型链是JS的“继承骨架”

原型链的本质,是JavaScript实现继承属性查找的核心机制,记住这几个关键点:

  • 每个对象(除了 null)都有 __proto__,指向父原型;

  • 属性查找时,会沿着 __proto__ 一层一层往上找,直到 null

  • 构造函数的 prototype 是实例的“模板”,原型链通过 __proto__ 串联;

  • 开发中要注意原型共享、原型替换等陷阱。

理解了原型链,再看JS里的类(class 语法,本质还是原型链)、继承、内置对象的方法调用,就会觉得顺理成章了,下次碰到“这个方法从哪来的”“修改原型为啥所有实例都变”这类问题,不妨从原型链的角度拆解,答案自然就出来啦~

您的支持是我们创作的动力!
温馨提示:本文作者系Terry ,经Web前端之家编辑修改或补充,转载请注明出处和本文链接:
https://jiangweishan.com/article/jsyuanxinglian2235.html

网友评论文明上网理性发言 已有0人参与

发表评论: