01. 原型链——JavaScript 的继承机制
JavaScript 的继承跟 Java/C++ 不一样——它不是基于类的,而是基于原型的。每个对象都有一个隐藏的 [[Prototype]] 属性(通过 __proto__ 或 Object.getPrototypeOf 访问),指向它的原型对象。
当你在对象上访问一个属性,JS 引擎先找对象本身有没有这个属性。没有就顺着 __proto__ 链往上层找,一直找到 null 为止。这条链就是原型链。
Object.prototype 是所有普通对象的最终原型。Function.prototype 是所有函数的原型。Array.prototype 是所有数组的原型,继承自 Object.prototype。
Array.prototype → Object.prototype → null。你定义的数组能用 push、map、filter 是因为它们定义在 Array.prototype 上,通过原型链传递到你数组里。
javascript
const arr = [1, 2, 3];
arr.push(4); // push 来自 Array.prototype
arr.toString(); // toString 来自 Object.prototype
// 验证原型链
arr.__proto__ === Array.prototype; // true
Array.prototype.__proto__ === Object.prototype; // true
Object.prototype.__proto__ === null; // true(链的终点)02. 构造函数与 new 关键字
在 ES6 class 出来之前,JavaScript 的类是用构造函数模拟的。任何普通函数前面加 new 调用,就变成了构造函数。
new 做了四件事:
1. 创建一个空对象{}
2. 把空对象的原型指向构造函数的 prototype(obj.__proto__ = Fn.prototype)
3. 以这个空对象为 this 调用构造函数(Fn.call(obj))
4. 如果构造函数没返回对象,就返回这个空对象
所以构造函数的 prototype 属性就是用它创建出来的实例的原型。注意只有函数的 prototype 属性跟对象的 __proto__ 是两个东西——prototype 是函数才有的属性,__proto__ 是所有对象都有的。
javascript
// 构造函数
function Person(name) {
this.name = name;
}
Person.prototype.sayHi = function() {
console.log('Hi, I am ' + this.name);
};
const alice = new Person('Alice');
alice.sayHi(); // Hi, I am Alice
// new 的模拟实现
function myNew(Fn, ...args) {
const obj = Object.create(Fn.prototype);
const result = Fn.apply(obj, args);
return result instanceof Object ? result : obj;
}03. ES6 class——语法糖
ES6 引入了 class 关键字,但它本质上是原型继承的语法糖——底层还是基于原型的。class 让代码更直观,也加了几个只有 class 才有的特性。
constructor 是构造函数。extends 设置原型链(子类的 prototype.__proto__ 指向父类的 prototype)。super 调用父类构造函数。
class 跟构造函数的不同:class 必须用 new 调用(否则报错);class 的方法不可枚举(默认不可用 for...in 遍历出来);class 内部默认是严格模式。
静态方法(static)定义在类本身而不是原型上,相当于 Person.staticMethod。实例访问不到。
javascript
// class 语法
class Person {
constructor(name) {
this.name = name;
}
sayHi() {
console.log('Hi, I am ' + this.name);
}
static create(name) {
return new Person(name);
}
}
const bob = new Person('Bob');
bob.sayHi();
// extends 继承
class Student extends Person {
constructor(name, grade) {
super(name); // 调用父类构造函数
this.grade = grade;
}
study() {
console.log(this.name + ' is studying');
}
}虽然有了 class 语法,但理解背后的原型机制仍然重要。debug 时看对象结构、理解为什么方法在所有实例间共享,都跟原型有关。
04. prototype vs __proto__ vs Object.create
这三个东西容易搞混,一张图搞清楚:
prototype——只有函数对象有。它是一个普通对象,当你用 new 调用这个函数时,新对象的 __proto__ 指向函数的 prototype。
__proto__——每个对象都有(除了 null),指向它的原型。这是原型链的物理连接。
Object.create(proto)——创建一个新对象,手动指定它的原型。这是最纯粹的原型继承方式,不需要构造函数。
它们的关系:实例.__proto__ === 构造函数.prototype。函数的 __proto__ 指向 Function.prototype。
javascript
// 三种继承方式的对比
// 1. 构造函数 + prototype
function Animal(name) { this.name = name; }
Animal.prototype.eat = function() { console.log('eat'); };
const dog = new Animal('dog');
// 2. Object.create
const animalProto = { eat() { console.log('eat'); } };
const cat = Object.create(animalProto);
cat.name = 'cat';
// 3. class
class AnimalClass {
constructor(name) { this.name = name; }
eat() { console.log('eat'); }
}
const bird = new AnimalClass('bird');05. 原型链的面试陷阱
原型链相关的高频面试题:
1. instanceof——检查构造函数.prototype 是否在某个对象的原型链上。obj instanceof Fn 就是看 Fn.prototype 在不在 obj.__proto__ 链上。
2. hasOwnProperty——检查属性是在对象自身上还是在原型链上。for...in 会遍历原型链上的可枚举属性,配合 hasOwnProperty 过滤。
3. 修改内置原型——给 Array.prototype 加自定义方法(如 Array.prototype.lastItem)。虽然可以做到但不推荐——可能跟未来的 JS 标准冲突或者覆盖别人的代码。
4. 各种方法在哪个 prototype 上——toString 在 Object.prototype 上,所有对象都能用。call/apply/bind 在 Function.prototype 上,只有函数能用。
javascript
// instanceof
function Foo() {}
const f = new Foo();
f instanceof Foo; // true
f instanceof Object; // true(Foo.prototype 的最顶端是 Object.prototype)
// hasOwnProperty
const obj = Object.create({ inherited: true });
obj.own = false;
obj.hasOwnProperty('own'); // true
obj.hasOwnProperty('inherited'); // false
// 检查属性在不在自身
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
console.log('own:', key);
}
}不要扩展内置原型——Array.prototype.myMethod。你的代码和第三方库、未来的 JS 标准都看上了同一个 Array.prototype。
知识测验
第 1/5 题正确 0
JavaScript 的继承是基于什么的?
下一节
下一节 事件循环深入