JavaScript原型链 - 继承的基石与核心机制
前言:从一道面试题说起
function Person() {}
const person = new Person();
console.log(person.__proto__ === Person.prototype); // true
console.log(Person.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__); // null
console.log(person.constructor === Person); // true
console.log(Person.constructor === Function); // true
console.log(person.constructor === Person.prototype.constructor); // true (!)
console.log(Function.constructor === Function); // true (!)
如果你能完全理解上面的代码,那么你已经掌握了原型链的核心。如果不能,本篇文章将带我们一步步揭开原型链的神秘面纱。
构造函数、实例、原型的三者关系
在JavaScript中,每个对象都有一个特殊的内部属性[[Prototype]](可以通过__proto__访问),它指向该对象的原型对象。
function Person(name) {
this.name = name;
}
// 原型对象:所有实例共享的方法和属性
Person.prototype.sayHello = function() {
console.log(`Hello, I'm ${this.name}`);
};
// 实例:通过new创建的对象
const zhangsan = new Person('zhangsna');
// 三者关系验证
console.log(zhangsan.__proto__ === Person.prototype); // true
console.log(Person.prototype.constructor === Person); // true
prototype和__proto__的区别与联系
-
prototype是函数特有的属性。 -
__proto__是每个对象都有的属性。
function Foo() {}
const obj = new Foo();
console.log(typeof Foo.prototype); // "object"
console.log(typeof obj.__proto__); // "object"
console.log(Foo.prototype === obj.__proto__); // true
console.log(Foo.__proto__ === Function.prototype); // true
// 一定要注意:函数也是对象,所以函数也有__proto__
console.log(Foo.__proto__ === Function.prototype); // true
console.log(Function.prototype.__proto__ === Object.prototype); // true
- 每个函数都有一个
prototype属性,指向该函数的原型对象。 - 每个对象都有一个
__proto__属性,指向创建该对象的构造函数的原型对象。 - 原型对象的
constructor属性指向创建该实例对象的构造函数。 -
Function是一个特殊的函数,它的constructor指向它自己。
完整的原型链结构
function Animal(name) {
this.name = name;
}
Animal.prototype.eat = function() {
console.log(`${this.name} is eating`);
};
function Dog(name, breed) {
Animal.call(this, name); // 调用父类构造函数
this.breed = breed;
}
// 设置原型链
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog; // 修复constructor指向
Dog.prototype.bark = function() {
console.log(`${this.name} is barking`);
};
const myDog = new Dog('Buddy', 'Golden Retriever');
// 原型链查找路径:
console.log(myDog.__proto__ === Dog.prototype); // true
console.log(Dog.prototype.__proto__ === Animal.prototype); // true
console.log(Animal.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__); // null
上述代码中,原型链的查找过程:
-
myDog本身有name和breed属性 -
myDog.__proto__(Dog.prototype) 有bark方法 -
Dog.prototype.__proto__(Animal.prototype) 有eat方法 -
Animal.prototype.__proto__(Object.prototype) 有toString等方法 -
Object.prototype.__proto__为null,查找结束
Object.prototype是所有原型链的终点。
属性屏蔽规则
hasOwnProperty vs in操作符
-
hasOwnProperty: 检查属性是否在对象自身(不在原型链上) - in操作符: 检查属性是否在对象自身或原型链上
属性屏蔽的三种情况
function Parent() {
this.value = 'parent value';
}
Parent.prototype.shared = 'parent shared';
function Child() {
this.value = 'child value';
}
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
Child.prototype.shared = 'child shared';
const child = new Child();
情况1:对象自身有该属性(完全屏蔽)
以上述代码为例,Child 本身有自己的 value 属性,当调用 value 属性时,会完全屏蔽原型链上的同名 value 属性:
console.log(child.value); // 'child value'(不是'parent value')
console.log(child.hasOwnProperty('value')); // true
情况2:对象自身本来没有,但添加对象自身属性
console.log(child.shared); // 'child shared'(来自Child.prototype)
console.log(child.hasOwnProperty('shared')); // false
// 添加对象自身属性
child.shared = 'instance shared';
console.log(child.shared); // 'instance shared'(现在自身有了)
console.log(child.hasOwnProperty('shared')); // true
情况3:属性是只读的
Parent.prototype.readOnly = 'cannot change';
// 试图修改只读属性
child.readOnly = 'try to change';
console.log(child.readOnly); // 'cannot change'(修改失败)
console.log(child.hasOwnProperty('readOnly')); // false(没有添加成功)
原型链的基础结构
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function() {
console.log(`Hello, ${this.name}`);
};
const zhangsan = new Person('zhangsan');
其原型结构图如下:
zhangsan (实例)
├── __proto__: Person.prototype
│ ├── constructor: Person
│ ├── sayHello: function()
│ └── __proto__: Object.prototype
│ ├── constructor: Object
│ ├── toString: function()
│ ├── hasOwnProperty: function()
│ └── __proto__: null
├── name: "zhangsan"
└── (其他实例属性...)
Person (构造函数)
├── prototype: Person.prototype
└── __proto__: Function.prototype
├── constructor: Function
├── apply: function()
├── call: function()
└── __proto__: Object.prototype
原型链的实际应用
实现继承的多种方式
原型链继承(经典方式)
function Parent(name) {
this.name = name;
}
function Child(age) {
this.age = age;
}
// 关键:让子类原型指向父类实例
Child.prototype = new Parent();
// 修复constructor指向
Child.prototype.constructor = Child;
- 问题:引用类型的属性会被所有实例共享
组合继承(最常用)
function Parent(name) {
this.name = name ;
}
function Child(name, age) {
// 继承属性
Parent.call(this, name); // 第二次调用Parent
this.age = age;
}
// 继承方法
Child.prototype = new Parent(); // 第一次调用Parent
Child.prototype.constructor = Child;
- 优点:结合了原型链和构造函数的优点
- 缺点:父类构造函数被调用了两次
寄生组合式继承(最佳实践)
function inheritPrototype(child, parent) {
// 创建父类原型的副本
const prototype = Object.create(parent.prototype);
// 修复constructor指向
prototype.constructor = child;
// 设置子类原型
child.prototype = prototype;
}
function Parent(name) {
this.name = name;
}
function Child(name, age) {
Parent2.call(this, name);
this.age = age;
}
inheritPrototype(Child, Parent);
- 只调用一次父类构造函数
- 避免在子类原型上创建不必要的属性
- 原型链保持不变
ES6 class继承
class Parent3 {
constructor(name) {
this.name = name;
}
}
class Child3 extends Parent3 {
constructor(name, age) {
super(name);
this.age = age;
}
sayAge() {
console.log(this.age);
}
}
- 语法简洁,现代解决方案,但需要ES6+支持
实现混入(Mixin)
const canEat = {
eat: function(food) {
console.log(`${this.name} is eating ${food}`);
this.energy += 10;
}
};
const canSleep = {
sleep: function(hours) {
console.log(`${this.name} is sleeping for ${hours} hours`);
this.energy += hours * 5;
}
};
const canWalk = {
walk: function(distance) {
console.log(`${this.name} is walking ${distance} km`);
this.energy -= distance * 2;
}
};
// 混入函数
function mixin(target, ...sources) {
Object.assign(target.prototype, ...sources);
}
// 创建动物类
function Animal(name) {
this.name = name;
this.energy = 100;
}
mixin(Animal, canEat, canSleep, canWalk);
// 创建鸟类,额外添加飞行能力
const canFly = {
fly: function(distance) {
console.log(`${this.name} is flying ${distance} km`);
this.energy -= distance * 5;
}
};
function Bird(name) {
Animal.call(this, name);
this.wings = 2;
}
// 设置原型链
Bird.prototype = Object.create(Animal.prototype);
Bird.prototype.constructor = Bird;
// 添加飞行能力
Object.assign(Bird.prototype, canFly);
原型链常见陷阱
陷阱1:修改原型会影响所有实例
function Person(name) {
this.name = name;
}
const zhangsan = new Person('zhangsan');
const lisi = new Person('lisi');
// 修改原型
Person.prototype.sayHello = function() {
console.log(`Hello, ${this.name}`);
};
zhangsan.sayHello(); // Hello, zhangsan (正常)
lisi.sayHello(); // Hello, lisi (正常)
陷阱2:原型上的引用类型属性被所有实例共享
function Problem() {
this.values = []; // 正确:每个实例有自己的数组
}
Problem.prototype.sharedValues = []; // 错误:所有实例共享同一个数组
const p1 = new Problem();
const p2 = new Problem();
p1.sharedValues.push('from p1');
p2.sharedValues.push('from p2');
console.log(p1.sharedValues); // ['from p1', 'from p2']
console.log(p2.sharedValues); // ['from p1', 'from p2']
陷阱3:for...in会遍历原型链上的可枚举属性
function Parent() {
this.parentProp = 'parent';
}
Parent.prototype.inheritedProp = 'inherited';
function Child() {
this.childProp = 'child';
}
Child.prototype = new Parent();
const child = new Child();
for (let key in child) {
console.log(key); // childProp, parentProp, inheritedProp
}
解决方案:使用hasOwnProperty过滤:
for (let key in child) {
if (child.hasOwnProperty(key)) {
console.log(key); // childProp, parentProp
}
}
原型链的最佳实践
实践1:使用Object.create设置原型链
function Parent(name) {
this.name = name;
}
Parent.prototype.sayName = function() {
console.log(this.name);
};
function Child(name, age) {
Parent.call(this, name);
this.age = age;
}
// 最佳方式
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
// 添加子类方法
Child.prototype.sayAge = function() {
console.log(this.age);
};
实践2:使用class语法(ES6+)
class GoodParent {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
}
class GoodChild extends GoodParent {
constructor(name, age) {
super(name);
this.age = age;
}
sayAge() {
console.log(this.age);
}
}
实践3:安全地检查属性
const obj = { ownProp: 'value' };
// 不好的做法
if (obj.property) {
// 如果property值为falsy(0, '', false, null, undefined),会被误判
}
// 好的做法
if (obj.hasOwnProperty('property')) {
// 明确检查自身属性
}
// 更好的做法(防止hasOwnProperty被覆盖)
if (Object.prototype.hasOwnProperty.call(obj, 'property')) {
// 最安全的方式
}
实践4:避免修改内置对象的原型
// 非必要情况,不得进行以下操作
if (!Array.prototype.customMethod) {
Array.prototype.customMethod = function() {
// 实现
};
}
思考题:以下代码的输出是什么?为什么?
function Foo() {}
function Bar() {}
Bar.prototype = Object.create(Foo.prototype);
const bar = new Bar();
console.log(bar instanceof Bar);
console.log(bar instanceof Foo);
console.log(bar instanceof Object);
console.log(Bar.prototype.isPrototypeOf(bar));
console.log(Foo.prototype.isPrototypeOf(bar));
console.log(Object.prototype.isPrototypeOf(bar));
结语
原型链是 JavaScript 面向对象编程的基石,在 JavaScript 中没有真正的类,只有对象和它们之间的链接(原型链),对于文章中错误的地方或者有任何问题,欢迎在评论区留言讨论!