从面试题说起:深入浅出聊聊 JavaScript 的原型链

494 阅读7分钟

从面试题说起:深入浅出聊聊 JavaScript 的原型链

前段时间去面试,前端岗位,面试官问了个经典问题:“能不能讲讲 JavaScript 的原型链是怎么回事?” 我当时稍微整理了一下思路,尽量用通俗的语言答了一番。后来想想,这个问题其实特别适合拿出来好好聊聊,既能帮助新手入门,也能让有经验的开发者温故知新。今天就以这个面试场景为引子,带大家深入浅出地搞懂原型链。


先从一个问题开始:为什么能访问没定义的属性?

假设有段代码:

let animal = {
  eats: true,
  walk() {
    console.log("动物会走路");
  }
};

let rabbit = {
  jumps: true,
  __proto__: animal
};

console.log(rabbit.eats); // true
rabbit.walk(); // 动物会走路

你有没有好奇过,rabbit 上明明没定义 eatswalk,为啥还能访问到?这就得说到 JavaScript 的原型链了。简单来说,原型链是 JavaScript 实现对象继承的“秘密武器”,它让对象可以“借用”其他对象的属性和方法。

那么,原型链到底是什么?它怎么工作的?下面我们一步步拆开来看。


原型链是个啥?

原型链的核心思想很简单:每个对象都有一个原型(prototype),这个原型本身也是对象,可能又有自己的原型,这样就形成了一条链。 当你访问一个对象的属性或方法时,JavaScript 会先从对象本身找,找不到就顺着这条链往上查,直到找到或者到达链的尽头。

举个生活中的例子:假设你去朋友家借书,你先翻自己的包,没找到;然后问你朋友,他翻了翻说也没有;最后他打电话问他哥,他哥说有。这条“找书链”就是原型链的缩影——从你开始,沿着朋友、朋友的哥一路找下去。

在 JavaScript 里,链的尽头是 null,通常顶端是 Object.prototype(它的原型就是 null)。所以,刚才那段代码里,rabbit 能访问 eats,是因为它通过原型链“借”了 animal 的属性。


原型链是怎么搭起来的?

明白了原型链的概念,你可能会问:这条链是怎么建起来的?JavaScript 提供了几种搭链子的方法,我们来挨个看看。

1. 用 Object.create():最现代的方式

let animal = {
  eats: true
};

let rabbit = Object.create(animal);
rabbit.jumps = true;

console.log(rabbit.eats); // true

Object.create(animal) 会创建一个新对象 rabbit,它的原型直接指向 animal。这种方式干净又直观,是现代 JavaScript 推荐的做法。

2. 用构造函数和 prototype:传统但实用

如果你写过构造函数,应该对这个不陌生:

function Animal() {}
Animal.prototype.eats = true;

let rabbit = new Animal();
rabbit.jumps = true;

console.log(rabbit.eats); // true

这里,rabbit 的原型是 Animal.prototype,而 eats 定义在 Animal.prototype 上,所以 rabbit 能访问到。这个方法在 ES6 之前特别常见。

3. 动态调整:Object.setPrototypeOf

原型链还有个神奇的地方——它可以动态改!比如:

let rabbit = { jumps: true };
let animal = { eats: true };

Object.setPrototypeOf(rabbit, animal);
console.log(rabbit.eats); // true

不过要注意,这种动态调整可能会影响性能,生产环境里尽量少用。还有个老家伙 __proto__,也能干这活儿,但它已经被标记为“历史遗留”,不建议用。


类跟原型链啥关系?

面试官有时候会追问:“ES6 的类是怎么跟原型链挂钩的?” 这其实是个好问题。

ES6 引入的 class 看着像 Java 或 C++ 的类,但本质上还是原型链的“化妆版”。看看这段代码:

class Animal {
  eats = true;
  walk() {
    console.log("动物会走路");
  }
}

let rabbit = new Animal();
console.log(rabbit.eats); // true
rabbit.walk(); // 动物会走路

表面上很“面向对象”,但背后,rabbit 的原型还是指向 Animal.prototypewalk 方法也定义在 Animal.prototype 上。也就是说,类只是原型链的语法糖,核心还是那条链


一个容易忽略的细节:属性覆盖

讲到这里,有个细节特别有意思,面试时也容易被问到。看这段代码:

let animal = { eats: true };
let rabbit = Object.create(animal);

rabbit.eats = false;

console.log(rabbit.eats); // false
console.log(animal.eats); // true

你会发现,rabbit.eats = false 只是给 rabbit 自己加了个属性,并不会改 animaleats。这叫属性覆盖——子对象可以“遮住”原型上的同名属性,但原型本身不受影响。

想确认属性是自己的还是继承来的?可以用 hasOwnProperty

console.log(rabbit.hasOwnProperty('eats')); // true
console.log(rabbit.hasOwnProperty('jumps')); // true

原型链的妙用与小心机

妙用

  1. 方法共享:原型上的方法是所有实例共享的,省内存。比如 Array.prototype.push,所有数组都能用。
  2. 扩展对象:想给数组加个新方法?直接改 Array.prototype(不过小心别乱改内置对象)。
  3. 层次继承:比如 Vehicle -> Car -> SportsCar,一级一级继承属性和方法。

小心机

  • 性能:原型链太长,查找属性会变慢,尽量保持链条短。
  • 区分属性:遍历对象时,记得用 hasOwnProperty 过滤继承属性,不然可能会踩坑。
  • 动态性:原型链可以随时改,但乱改可能让代码变成“调试噩梦”。

面试官再问:跟作用域链有啥区别?

如果面试官刁钻一点,可能会问:“原型链跟作用域链有啥不一样?” 这俩确实容易混。

简单说:

  • 原型链管对象属性和方法的查找,比如 rabbit.eats 从哪来。
  • 作用域链管变量的查找,比如函数里找 let x 是从哪层作用域来的。

举个例子:

let x = 10;
function foo() {
  console.log(x); // 作用域链找变量
}

let obj = { x: 20, __proto__: { x: 30 } };
console.log(obj.x); // 原型链找属性

用途不同,别搞混就行。


总结:原型链的核心与面试回答

回到面试开头的问题,原型链其实就是 JavaScript 的“家传秘籍”——通过一条链,把属性和方法从祖先传给后代。理解了它,你就抓住了 JavaScript 面向对象编程的灵魂。它不仅是个基础概念,也是面试中的高频考点。如果面试官让我回答“讲讲 JavaScript 的原型链”,我会这样组织,既清晰又有深度:

“JavaScript 的原型链是其对象继承的核心机制。简单来说,每个对象都有一个原型(通过内部的 [[Prototype]] 引用指向),这个原型可以是另一个对象,形成一条链式结构,直到顶端是 null,通常是 Object.prototype。当访问对象的属性或方法时,引擎会先检查对象自身,如果没有,就沿着原型链逐级查找,直到找到或返回 undefined

比如,假设有个对象 animal 定义了 eats: true,另一个对象 rabbit 通过 Object.create(animal) 继承它,那么 rabbit.eats 会返回 true,因为查找会顺着原型链到 animal

原型链的实现方式有几种:可以用 Object.create() 创建新对象并指定原型;用构造函数时,实例的原型指向构造函数的 prototype 属性;甚至可以用 Object.setPrototypeOf 动态调整原型链。ES6 的 class 也是基于原型链的语法糖,比如 class Animal 的实例原型仍是 Animal.prototype

实际应用中,原型链能实现方法共享,比如 Array.prototype.push 被所有数组实例共用,节省内存。但也有需要注意的地方:一是属性覆盖,子对象可以定义同名属性遮盖原型属性,但不影响原型本身;二是性能,长原型链可能拖慢查找速度;三是动态性,虽然灵活,但滥用可能导致代码难以维护。

举个例子:

let animal = { eats: true };
let rabbit = Object.create(animal);
rabbit.eats = false;
console.log(rabbit.eats); // false,自身属性
console.log(animal.eats); // true,原型未变

最后,原型链和作用域链是不同的概念:原型链管对象属性继承,作用域链管变量查找。理解这两者的区别,能更好地掌握 JavaScript 的运行机制。”

这个回答涵盖了原型链的定义、工作原理、实现方式、应用场景和注意事项,还澄清了与作用域链的区别,既结构化又有深度。面试时可以根据时间灵活调整,能简能详,足以应对各种追问。


参考资源

想深入研究?推荐几篇干货:

希望这篇文章能帮你在面试中多一份底气,也祝你在 JavaScript 的世界里越走越远!