知识篇 -- JS原型与原型链
Ray Shine
2024/2/26 JavaScript基础知识继承 JavaScript是一种基于原型的面向对象语言,与传统的基于类的面向对象语言(如Java、C++)有着本质的区别。理解原型 (Prototype) 和原型链 (Prototype Chain) 是掌握JavaScript继承机制、对象创建和属性查找的关键。
# 一、什么是原型 (Prototype)? 核心
在JavaScript中,每个函数(除了箭头函数)在创建时都会自动拥有一个 prototype 属性,这个属性通常指向一个对象,我们称之为原型对象。
- 作用:原型对象是用来实现继承和共享属性与方法的。当使用
new关键字调用一个函数(作为构造函数)来创建实例时,新创建的实例会隐式地链接到构造函数的原型对象。 __proto__属性:每个JavaScript对象(除了null)都有一个内部属性[[Prototype]],在ES5及之前可以通过__proto__属性访问(现在推荐使用Object.getPrototypeOf()和Object.setPrototypeOf())。这个__proto__指向创建该对象的构造函数的原型对象。
示例:
function Person(name) {
this.name = name;
}
// Person.prototype 就是原型对象
Person.prototype.greet = function() {
console.log(`Hello, my name is ${this.name}`);
};
const alice = new Person("Alice");
const bob = new Person("Bob");
alice.greet(); // Hello, my name is Alice
bob.greet(); // Hello, my name is Bob
console.log(alice.__proto__ === Person.prototype); // true
console.log(bob.__proto__ === Person.prototype); // true
解释:greet 方法定义在 Person.prototype 上,而不是每个 Person 实例上。这样,所有 Person 实例都可以共享同一个 greet 方法,节省了内存,也实现了方法的复用。
# 二、什么是原型链 (Prototype Chain)? 继承机制
当试图访问一个对象的属性时,如果该对象本身没有这个属性,JavaScript会沿着其内部的 [[Prototype]] 链接向上查找,直到找到该属性或到达原型链的顶端(null)。这个查找过程形成的链条就是原型链。
- 链的构成:
实例对象的__proto__指向构造函数的prototype对象。构造函数的prototype对象 的__proto__指向Object.prototype。Object.prototype的__proto__指向null,标志着原型链的终点。
图示 (简化版):
alice (实例)
↓ __proto__
Person.prototype (原型对象)
↓ __proto__
Object.prototype (所有对象的最终原型)
↓ __proto__
null (原型链终点)
示例:
function Animal(name) {
this.name = name;
}
Animal.prototype.eat = function() {
console.log(`${this.name} is eating.`);
};
function Dog(name, breed) {
Animal.call(this, name); // 继承Animal的属性
this.breed = breed;
}
// 关键步骤:将Dog的原型设置为Animal的实例,实现原型链继承
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");
myDog.eat(); // Buddy is eating. (通过原型链找到Animal.prototype上的eat方法)
myDog.bark(); // Buddy is barking. (Dog.prototype上的bark方法)
console.log(myDog.name); // Buddy (Dog实例自身的属性)
解释:当 myDog.eat() 被调用时,JavaScript首先在 myDog 实例上查找 eat 方法,没有找到。然后沿着 myDog.__proto__ 向上查找,找到 Dog.prototype,也没有找到。再沿着 Dog.prototype.__proto__ 向上查找,找到 Animal.prototype,找到了 eat 方法并执行。
# 三、constructor 属性 重要
每个原型对象都有一个 constructor 属性,它指向关联的构造函数。
- 作用:通过实例的
constructor属性可以追溯到创建它的构造函数。 - 注意:当通过
Object.create()改变原型时,需要手动修复constructor的指向,否则它会指向旧的构造函数。function MyFunc() {} const obj = new MyFunc(); console.log(obj.constructor === MyFunc); // true function AnotherFunc() {} AnotherFunc.prototype = Object.create(MyFunc.prototype); const anotherObj = new AnotherFunc(); console.log(anotherObj.constructor === AnotherFunc); // false (指向MyFunc) // 修复:AnotherFunc.prototype.constructor = AnotherFunc; // console.log(anotherObj.constructor === AnotherFunc); // true
# 四、原型链的实际应用 应用
- 继承:通过原型链,子对象可以继承父对象的属性和方法。
- 代码复用:将共享的方法放在原型对象上,所有实例都可以复用,减少内存占用。
- 属性查找:理解属性查找机制,有助于优化代码和避免不必要的性能开销。
# 五、现代JavaScript中的原型与继承 ES6+
虽然原型链是JavaScript继承的底层机制,但在ES6之后,我们通常使用 class 关键字来定义类和实现继承,这只是原型链的语法糖,底层依然是基于原型实现的。
class Animal {
constructor(name) {
this.name = name;
}
eat() {
console.log(`${this.name} is eating.`);
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name); // 调用父类的构造函数
this.breed = breed;
}
bark() {
console.log(`${this.name} is barking.`);
}
}
const myDog = new Dog("Buddy", "Golden Retriever");
myDog.eat(); // Buddy is eating.
myDog.bark(); // Buddy is barking.
解释:class 语法使得JavaScript的继承看起来更像传统的面向对象语言,但其内部仍然是基于原型链的。extends 关键字会自动处理原型链的设置和 constructor 的修复。
理解原型和原型链是深入掌握JavaScript的必经之路。它揭示了JavaScript对象之间如何共享行为和数据,是理解许多高级概念(如闭包、this 指向)的基础。