从面试题说起:深入浅出聊聊 JavaScript 的原型链
前段时间去面试,前端岗位,面试官问了个经典问题:“能不能讲讲 JavaScript 的原型链是怎么回事?” 我当时稍微整理了一下思路,尽量用通俗的语言答了一番。后来想想,这个问题其实特别适合拿出来好好聊聊,既能帮助新手入门,也能让有经验的开发者温故知新。今天就以这个面试场景为引子,带大家深入浅出地搞懂原型链。
先从一个问题开始:为什么能访问没定义的属性?
假设有段代码:
let animal = {
eats: true,
walk() {
console.log("动物会走路");
}
};
let rabbit = {
jumps: true,
__proto__: animal
};
console.log(rabbit.eats); // true
rabbit.walk(); // 动物会走路
你有没有好奇过,rabbit 上明明没定义 eats 和 walk,为啥还能访问到?这就得说到 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.prototype,walk 方法也定义在 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 自己加了个属性,并不会改 animal 的 eats。这叫属性覆盖——子对象可以“遮住”原型上的同名属性,但原型本身不受影响。
想确认属性是自己的还是继承来的?可以用 hasOwnProperty:
console.log(rabbit.hasOwnProperty('eats')); // true
console.log(rabbit.hasOwnProperty('jumps')); // true
原型链的妙用与小心机
妙用
- 方法共享:原型上的方法是所有实例共享的,省内存。比如
Array.prototype.push,所有数组都能用。 - 扩展对象:想给数组加个新方法?直接改
Array.prototype(不过小心别乱改内置对象)。 - 层次继承:比如
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 的运行机制。”
这个回答涵盖了原型链的定义、工作原理、实现方式、应用场景和注意事项,还澄清了与作用域链的区别,既结构化又有深度。面试时可以根据时间灵活调整,能简能详,足以应对各种追问。
参考资源
想深入研究?推荐几篇干货:
- MDN: Inheritance and the prototype chain
- GeeksforGeeks: Understanding the Prototype Chain
- JavaScript.info: Prototype inheritance
希望这篇文章能帮你在面试中多一份底气,也祝你在 JavaScript 的世界里越走越远!