你真的理解了 javascript 中的原型及原型链?
JavaScript原型与原型链:javascript 继承的基础
引言
故事开始于
面试官:“说说你对原型以及原型链的理解”
我:原型是这样的..., 原型链是这样的..., 说的很抽象,听得也很抽象
面试官接着问:“说说javascript 怎么实现继承的”
作为前端开发者,我们经常会听到「原型」和「原型链」这两个概念,但你真的理解它们吗?它们是JavaScript面向对象编程的核心机制,掌握它们对于理解JavaScript的运行原理至关重要。
本文将从基础概念出发,逐步深入解析JavaScript原型与原型链的工作原理,结合大量代码示例和可视化图解,让你轻松掌握这一核心知识点。
一、原型的基本概念
1. 什么是原型?
在JavaScript中,每个对象都有一个原型对象(__proto__),对象可以从原型中继承属性和方法。原型对象也可以有自己的原型,这样就形成了一个链式结构,称为「原型链」。
通过console控制台,可以看到foo对象_proto_对象上面有个age等于18的属性,而age又是通过构造函数Foo的prototype添加上去的。
![]()
所以有了 foo.__proto__ === Foo.prototype
2. 原型的作用
原型主要有两个作用:
- 属性继承:对象可以继承原型的属性
- 方法共享:多个对象可以共享原型上的方法,节省内存空间
3. 代码示例:原型的基本使用
// 创建一个普通对象
const person = {
name: 'John',
age: 30
};
// 获取person的原型
const proto = Object.getPrototypeOf(person);
console.log(proto); // 输出:[Object: null prototype] {}
console.log(proto === Object.prototype); // 输出:true
二、__proto__与prototype的区别
这是初学者最容易混淆的两个概念,让我们来彻底搞清楚它们:
1. proto(隐式原型)
-
定义:每个对象都有一个
__proto__属性,指向它的原型对象 - 作用:用于实现原型链查找
-
注意:这是一个非标准属性,推荐使用
Object.getPrototypeOf()和Object.setPrototypeOf()代替
2. prototype(显式原型)
-
定义:只有函数才有
prototype属性 -
作用:当函数作为构造函数使用时,新创建的对象会将这个
prototype作为自己的__proto__ -
组成:
prototype对象包含constructor属性,指向构造函数本身
3. 可视化对比
| 特性 | proto | prototype |
|---|---|---|
| 所属对象 | 所有对象 | 只有函数 |
| 指向 | 对象的原型 | 构造函数创建的实例的原型 |
| 作用 | 实现原型继承 | 定义构造函数的实例共享属性和方法 |
| 标准性 | 非标准(建议使用Object.getPrototypeOf) | 标准属性 |
4. 代码示例:__proto__与prototype
// 构造函数
function Person(name) {
this.name = name;
}
// 构造函数的prototype属性
console.log(Person.prototype); // 输出:Person {}(包含constructor属性)
// 创建实例
const alice = new Person('Alice');
// 实例的__proto__指向构造函数的prototype
console.log(alice.__proto__ === Person.prototype); // 输出:true
// 构造函数的prototype的constructor指向构造函数
console.log(Person.prototype.constructor === Person); // 输出:true
三、构造函数与原型的关系
1. 构造函数创建实例的过程
当使用new关键字调用构造函数创建实例时,发生了以下几件事:
- 创建一个新的空对象
- 将这个新对象的
__proto__指向构造函数的prototype - 将构造函数的
this指向这个新对象 - 执行构造函数体内的代码
- 如果构造函数没有返回对象,则返回这个新对象
2. 代码示例:构造函数创建实例
// 构造函数
function Car(brand, model) {
this.brand = brand;
this.model = model;
}
// 在原型上添加方法
Car.prototype.drive = function() {
console.log(`驾驶 ${this.brand} ${this.model}`);
};
// 创建两个实例
const car1 = new Car('Toyota', 'Camry');
const car2 = new Car('Honda', 'Accord');
// 调用原型上的方法
car1.drive(); // 输出:驾驶 Toyota Camry
car2.drive(); // 输出:驾驶 Honda Accord
// 两个实例共享同一个原型方法
console.log(car1.drive === car2.drive); // 输出:true
四、原型链的形成与查找机制
1. 什么是原型链?
当访问一个对象的属性或方法时,如果该对象本身没有这个属性或方法,JavaScript会沿着__proto__属性向上查找,直到找到该属性或方法,或者到达原型链的末端(null)。这个链式查找结构就是「原型链」。
2. 原型链的末端
原型链的末端是Object.prototype,它的__proto__指向null,表示原型链的结束。
// Object.prototype是原型链的顶端之一
console.log(Object.prototype.__proto__); // 输出:null
3. 代码示例:原型链查找
// 创建对象
const obj = {};
// obj自身没有toString方法
console.log(obj.hasOwnProperty('toString')); // 输出:false
// 但可以调用toString方法,因为它继承自Object.prototype
console.log(obj.toString()); // 输出:[object Object]
// 原型链:obj -> Object.prototype -> null
console.log(obj.__proto__ === Object.prototype); // 输出:true
console.log(Object.prototype.__proto__ === null); // 输出:true
4. 完整原型链示例
// 构造函数
function Animal(type) {
this.type = type;
}
// 原型方法
Animal.prototype.eat = function() {
console.log('进食中...');
};
// 子类构造函数
function Dog(name, breed) {
Animal.call(this, 'dog'); // 调用父类构造函数
this.name = name;
this.breed = breed;
}
// 设置Dog的原型为Animal的实例
Dog.prototype = Object.create(Animal.prototype);
// 修复constructor指向
Dog.prototype.constructor = Dog;
// Dog的原型方法
Dog.prototype.bark = function() {
console.log('汪汪汪!');
};
// 创建实例
const myDog = new Dog('Buddy', 'Golden Retriever');
// 访问自身属性
console.log(myDog.name); // 输出:Buddy
// 访问继承自Dog.prototype的方法
myDog.bark(); // 输出:汪汪汪!
// 访问继承自Animal.prototype的方法
myDog.eat(); // 输出:进食中...
// 访问继承自Object.prototype的方法
console.log(myDog.toString()); // 输出:[object Object]
// 原型链:myDog -> Dog.prototype -> Animal.prototype -> Object.prototype -> null
五、原型链的实际应用
1. 实现继承
原型链是JavaScript实现继承的主要方式。通过将子类的原型设置为父类的实例,可以实现属性和方法的继承。
// 父类
function Parent(name) {
this.name = name;
this.family = 'Smith';
}
Parent.prototype.sayFamily = function() {
console.log(`My family name is ${this.family}`);
};
// 子类
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(`I'm ${this.age} years old`);
};
// 使用
const child = new Child('John', 10);
child.sayFamily(); // 输出:My family name is Smith
child.sayAge(); // 输出:I'm 10 years old
2. 扩展内置对象
我们可以通过修改内置对象的原型来扩展其功能:
// 扩展Array原型,添加求和方法
Array.prototype.sum = function() {
return this.reduce((total, item) => total + item, 0);
};
// 使用扩展后的方法
const numbers = [1, 2, 3, 4, 5];
console.log(numbers.sum()); // 输出:15
// 扩展String原型,添加反转方法
String.prototype.reverse = function() {
return this.split('').reverse().join('');
};
// 使用扩展后的方法
const str = 'hello';
console.log(str.reverse()); // 输出:olleh
注意:虽然可以扩展内置对象,但不推荐在生产环境中使用,因为可能会与其他库冲突。
3. 原型链实现对象类型检查
// 判断对象类型的函数
function getType(obj) {
if (obj === null) return 'null';
if (typeof obj !== 'object') return typeof obj;
// 使用原型链判断具体类型
const proto = Object.getPrototypeOf(obj);
const constructor = proto.constructor;
return constructor.name;
}
// 测试
console.log(getType(123)); // 输出:number
console.log(getType('hello')); // 输出:string
console.log(getType(true)); // 输出:boolean
console.log(getType(null)); // 输出:null
console.log(getType([])); // 输出:Array
console.log(getType({})); // 输出:Object
六、常见误区与注意事项
1. 误区一:所有对象都是Object的实例
正确理解:除了Object.prototype本身,所有对象都是Object的实例吗?不完全是。比如:
// 创建一个没有原型的对象
const obj = Object.create(null);
console.log(obj.__proto__); // 输出:undefined
console.log(obj instanceof Object); // 输出:false
2. 误区二:原型上的属性修改会立即反映到所有实例
正确理解:是的,但如果是直接给实例添加同名属性,会覆盖原型属性,而不是修改原型:
function Person() {}
Person.prototype.name = 'Anonymous';
const p1 = new Person();
const p2 = new Person();
console.log(p1.name); // 输出:Anonymous
console.log(p2.name); // 输出:Anonymous
// 修改原型属性
Person.prototype.name = 'Default';
console.log(p1.name); // 输出:Default
console.log(p2.name); // 输出:Default
// 给实例添加同名属性(覆盖)
p1.name = 'John';
console.log(p1.name); // 输出:John
console.log(p2.name); // 输出:Default(不受影响)
3. 注意事项:原型链查找的性能
原型链查找是有性能开销的,层级越深,查找速度越慢。因此:
- 避免在原型链的深层定义常用属性和方法
- 对于频繁访问的属性,可以考虑直接定义在对象本身
4. 注意事项:不要使用__proto__赋值
直接修改__proto__会影响对象的原型链,可能导致性能问题和意外行为。推荐使用:
-
Object.create()创建指定原型的对象 -
Object.setPrototypeOf()修改对象的原型
七、可视化理解原型链
为了更好地理解原型链,我们可以通过可视化的方式来呈现它的结构:
1. 简单对象的原型链
obj (实例对象)
└── __proto__ → Object.prototype
└── __proto__ → null
2. 构造函数创建的对象原型链
instance (实例对象)
└── __proto__ → Constructor.prototype
└── __proto__ → Object.prototype
└── __proto__ → null
3. 继承关系的原型链
childInstance (子类实例)
└── __proto__ → Child.prototype
└── __proto__ → Parent.prototype
└── __proto__ → Object.prototype
└── __proto__ → null
4. 代码示例:可视化原型链
// 定义构造函数
function Grandparent() {
this.grandparentProp = 'grandparent';
}
function Parent() {
this.parentProp = 'parent';
}
function Child() {
this.childProp = 'child';
}
// 设置继承关系
Parent.prototype = Object.create(Grandparent.prototype);
Child.prototype = Object.create(Parent.prototype);
// 创建实例
const child = new Child();
// 可视化原型链
console.log('child:', child);
console.log('child.__proto__ (Child.prototype):', child.__proto__);
console.log('child.__proto__.__proto__ (Parent.prototype):', child.__proto__.__proto__);
console.log('child.__proto__.__proto__.__proto__ (Grandparent.prototype):', child.__proto__.__proto__.__proto__);
console.log('child.__proto__.__proto__.__proto__.__proto__ (Object.prototype):', child.__proto__.__proto__.__proto__.__proto__);
console.log('child.__proto__.__proto__.__proto__.__proto__.__proto__ (null):', child.__proto__.__proto__.__proto__.__proto__.__proto__);
八、原型与现代JavaScript
1. ES6 Class与原型的关系
ES6引入了class语法,但它只是原型继承的语法糖,底层仍然是基于原型链实现的:
// ES6 Class
class Animal {
constructor(type) {
this.type = type;
}
eat() {
console.log('进食中...');
}
}
class Dog extends Animal {
constructor(name, breed) {
super('dog');
this.name = name;
this.breed = breed;
}
bark() {
console.log('汪汪汪!');
}
}
// 等价于原型继承
console.log(typeof Animal); // 输出:function
console.log(Dog.prototype.__proto__ === Animal.prototype); // 输出:true
2. 原型与组合继承
现代JavaScript中,我们通常使用组合继承模式,结合原型链和构造函数:
// 组合继承模式
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);
};
九、总结
通过本文的学习,我们已经全面了解了JavaScript原型与原型链的核心概念:
- 原型:每个对象都有一个原型,可以继承原型的属性和方法
- proto:对象的隐式原型,指向它的原型对象
- prototype:函数的显式原型,用于构造函数创建实例时的原型指向
- 原型链:对象通过__proto__形成的链式结构,用于属性和方法的查找
- 继承:通过原型链实现对象间的继承关系
原型与原型链是JavaScript的核心机制,掌握它们对于理解JavaScript的运行原理、实现面向对象编程至关重要。希望本文的详细解析和丰富示例能帮助你彻底理解这一知识点。
思考与练习
- 为什么说原型链是JavaScript实现继承的基础?
- 如何优化原型链查找的性能?
- ES6 Class和传统原型继承有什么区别?
- 尝试实现一个完整的原型链继承案例
- 解释
instanceof运算符的工作原理(提示:基于原型链)
欢迎在评论区分享你的理解和思考,让我们一起进步!
参考资料
如果你觉得本文对你有帮助,欢迎点赞、收藏、分享,也欢迎关注我,获取更多前端技术干货!