一、原型链的工作原理
每个 JavaScript 对象都有一个内部私有属性 [[Prototype]](通常通过 __proto__ 或 Object.getPrototypeOf() 访问),它指向另一个对象,即该对象的原型。当我们访问一个对象的属性或方法时,引擎会执行以下步骤:
- 首先检查对象自身是否有该属性(自身属性,可通过
hasOwnProperty判断)。 - 如果没有,则沿着
[[Prototype]]链向上查找其原型对象,检查原型对象上是否有该属性。 - 如果仍然没有,继续向上查找原型的原型,以此类推。
- 直到找到该属性,或者到达原型链的末端(
null),返回undefined。
这种通过 [[Prototype]] 层层连接的链条,就是原型链。
一个形象比喻
假设你有一个家族,每个人都有自己的储物箱(自身属性)。当你需要找一件物品(访问属性)时:
- 你先翻自己的储物箱——如果有,直接拿出来用。
- 如果自己没有,你就去问你父亲(
__proto__指向的原型对象),看看他那里有没有。 - 如果父亲也没有,父亲会继续问他的父亲(祖父),以此类推。
- 直到问到家族最年长的祖先(原型链末端的
null)。如果祖先也没有,就告诉你“没找到”(返回undefined)。
在这个比喻中:
- 你 → 当前对象
- 父亲、祖父… → 原型链上的各级原型对象
- 祖先 →
Object.prototype(最终指向null) - 问的过程 → JavaScript 引擎沿着
[[Prototype]]向上查找
这种一环扣一环的委托关系,就是原型链的本质——对象之间通过“原型”连接,形成一条查找属性的链条。
示例
javascript
const parent = { name: 'parent' };
const child = Object.create(parent); // child 的原型指向 parent
child.age = 10;
console.log(child.age); // 10(自身属性)
console.log(child.name); // 'parent'(来自原型链)
console.log(child.toString); // 来自 Object.prototype(再往上一级)
二、如何实现继承
在 ES6 之前,主要通过构造函数和原型来模拟继承。常见的方式有:
1. 原型链继承
将子类的原型设置为父类的实例。
javascript
function Parent() {
this.name = 'parent';
}
Parent.prototype.say = function() {
console.log(this.name);
};
function Child() {}
Child.prototype = new Parent(); // 继承
const c = new Child();
c.say(); // 'parent'
缺点:所有实例共享父类实例的引用属性(如数组),且无法向父类构造函数传参。
2. 构造函数继承
在子类构造函数中调用父类构造函数(使用 call/apply)。
javascript
function Parent(name) {
this.name = name;
}
function Child(name) {
Parent.call(this, name); // 继承属性
}
const c = new Child('child');
console.log(c.name); // 'child'
缺点:无法继承父类原型上的方法,方法必须在构造函数中定义。
3. 组合继承
结合原型链继承和构造函数继承,既继承属性又继承方法。
javascript
function Parent(name) {
this.name = name;
}
Parent.prototype.say = function() {
console.log(this.name);
};
function Child(name) {
Parent.call(this, name); // 第二次调用 Parent
}
Child.prototype = new Parent(); // 第一次调用 Parent
Child.prototype.constructor = Child;
const c = new Child('child');
c.say(); // 'child'
缺点:父类构造函数被调用了两次,导致子类原型上存在多余的父类实例属性。
4. 寄生组合式继承(最理想的继承方式)
使用 Object.create() 直接让子类的原型继承父类的原型,避免了多余的父类实例属性。
javascript
function Parent(name) {
this.name = name;
}
Parent.prototype.say = function() {
console.log(this.name);
};
function Child(name) {
Parent.call(this, name);
}
// 关键:将子类的原型设置为父类原型的副本
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
const c = new Child('child');
c.say(); // 'child'
目前这种方式被认为是 ES5 环境下最理想的继承模式。
5. 使用 Object.create() 直接创建对象并指定原型
javascript
const parent = { name: 'parent' };
const child = Object.create(parent, {
age: { value: 10, enumerable: true }
});
console.log(child.name); // 'parent'
三、ES6 class 的本质
ES6 引入了 class 关键字,但它并不是一种新的面向对象继承模型,而是基于原型的语法糖。class 让对象的创建和继承变得更加清晰、接近传统面向对象语言,但底层依然是构造函数和原型链。
本质特性
class定义的类本质上是一个函数,具有prototype属性,方法定义在prototype上。- 通过
new调用时,会自动执行构造函数constructor。 - 类内部定义的方法都是不可枚举的(这与 ES5 中将方法直接挂载在原型上不同)。
- 类必须使用
new调用,不能直接作为普通函数执行。 - 类声明不会被提升(与函数声明不同),存在暂时性死区。
- 类体默认采用严格模式。
示例与转译
javascript
class Parent {
constructor(name) {
this.name = name;
}
say() {
console.log(this.name);
}
}
class Child extends Parent {
constructor(name, age) {
super(name); // 调用父类构造函数
this.age = age;
}
say() {
super.say(); // 调用父类方法
console.log(`age: ${this.age}`);
}
}
上述代码经过 Babel 转译成 ES5 后,本质上就是寄生组合式继承的实现:
Child.prototype = Object.create(Parent.prototype)建立原型链。- 在
Child构造函数中调用Parent.call(this, name)继承实例属性。 super关键字对应父类构造函数的调用或父类原型方法的调用。
class 与原型的关系验证
javascript
class A {}
typeof A // "function"
A.prototype.constructor === A // true
const a = new A();
a.__proto__ === A.prototype // true
静态方法
ES6 class 支持静态方法(使用 static 关键字),它们直接定义在构造函数上,而不是原型上。
javascript
class A {
static hello() {
console.log('static');
}
}
A.hello(); // 'static'
typeof A.hello // "function"
总结
- 原型链:通过对象的
[[Prototype]]链实现属性查找的机制。 - 继承实现:在 ES5 中可通过构造函数、原型、
Object.create等多种方式组合实现;最完善的是寄生组合式继承。 - ES6
class本质:语法糖,底层仍是构造函数 + 原型链,提供了更简洁的语法和更严格的语义,但并未改变 JavaScript 基于原型的本质。
#前端、#前端面试、#干货
如果这篇这篇文章对您有帮助?关注、点赞、收藏,三连支持一下。
有疑问或想法?评论区见。