原型和原型链是 JavaScript 中实现对象继承和属性查找的核心机制。为了更深入地理解它们,我们需要从底层原理、实现机制以及实际应用等多个角度进行分析。
1. 原型(Prototype)
1.1 什么是原型?
- 每个 JavaScript 对象(除
null
外)都有一个内部属性[[Prototype]]
,指向它的原型对象。 - 原型对象也是一个普通对象,它包含可以被其他对象共享的属性和方法。
- 通过
__proto__
(非标准,但广泛支持)或Object.getPrototypeOf()
可以访问对象的原型。
1.2 构造函数与原型
- 每个函数(构造函数)都有一个
prototype
属性,指向一个对象,这个对象是该构造函数实例的原型。 - 当使用
new
关键字创建实例时,实例的[[Prototype]]
会指向构造函数的prototype
对象。
function Person(name) {this.name = name;
}// 在构造函数的原型上添加方法
Person.prototype.sayHello = function() {console.log(`Hello, my name is ${this.name}`);
};const person1 = new Person('Alice');
person1.sayHello(); // 输出: Hello, my name is Alice
- 在上面的例子中:
Person.prototype
是person1
的原型对象。person1.__proto__
指向Person.prototype
。
2. 原型链(Prototype Chain)
2.1 什么是原型链?
- 原型链是由对象的
[[Prototype]]
连接起来的链式结构。 - 当访问对象的属性或方法时,如果对象本身没有该属性,JavaScript 会沿着原型链向上查找,直到找到该属性或到达原型链的顶端(
null
)。
2.2 原型链的顶端
- 所有对象的原型链最终都会指向
Object.prototype
,而Object.prototype
的[[Prototype]]
是null
。 - 如果在整个原型链中都找不到属性,则返回
undefined
。
console.log(person1.toString()); // 输出: [object Object]
- 在上面的例子中:
person1
本身没有toString
方法。- JavaScript 引擎会沿着原型链查找:
person1
-> 没有toString
。person1.__proto__
(即Person.prototype
) -> 没有toString
。Person.prototype.__proto__
(即Object.prototype
) -> 找到toString
,调用它。
3. 原型链的深度分析
3.1 原型链的构建
- 原型链是通过
[[Prototype]]
连接起来的。 - 当我们创建一个对象时,它的
[[Prototype]]
会指向某个原型对象。
const obj = {};
console.log(obj.__proto__ === Object.prototype); // true
- 如果我们手动修改
[[Prototype]]
,可以构建自定义的原型链。
const parent = { name: 'Parent' };
const child = Object.create(parent); // child.__proto__ 指向 parent
console.log(child.name); // 输出: Parent
3.2 原型链的查找机制
- 当访问对象的属性时,JavaScript 引擎会按照以下步骤查找:
- 检查对象自身是否有该属性。
- 如果没有,沿着
[[Prototype]]
向上查找。 - 重复这个过程,直到找到属性或到达
null
。
const grandparent = { familyName: 'Smith' };
const parent = Object.create(grandparent);
const child = Object.create(parent);console.log(child.familyName); // 输出: Smith
- 在上面的例子中:
child
自身没有familyName
。child.__proto__
(即parent
)也没有familyName
。parent.__proto__
(即grandparent
)有familyName
,返回Smith
。
4. 原型链与继承
4.1 基于原型的继承
- JavaScript 通过原型链实现继承。
- 子类的原型对象指向父类的实例,从而继承父类的属性和方法。
function Person(name) {this.name = name;
}Person.prototype.sayHello = function() {console.log(`Hello, my name is ${this.name}`);
};function Student(name, grade) {Person.call(this, name); // 调用父类构造函数this.grade = grade;
}// 继承父类原型
Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student; // 修复构造函数指向const student1 = new Student('Bob', 10);
student1.sayHello(); // 输出: Hello, my name is Bob
- 在上面的例子中:
Student.prototype
的原型指向Person.prototype
。student1
可以访问Person.prototype
上的方法。
4.2 原型链的局限性
- 原型链继承是单向的,子类可以访问父类的属性和方法,但父类不能访问子类的属性和方法。
- 如果原型链过长,属性查找的性能会受到影响。
5. 原型链的实际应用
5.1 共享方法
- 将方法定义在原型上,可以避免每个实例都创建一份方法的副本,节省内存。
function Person(name) {this.name = name;
}Person.prototype.sayHello = function() {console.log(`Hello, my name is ${this.name}`);
};const person1 = new Person('Alice');
const person2 = new Person('Bob');// person1 和 person2 共享同一个 sayHello 方法
console.log(person1.sayHello === person2.sayHello); // true
5.2 扩展内置对象
- 可以通过修改内置对象的原型来扩展其功能。
Array.prototype.last = function() {return this[this.length - 1];
};const arr = [1, 2, 3];
console.log(arr.last()); // 输出: 3
6. 总结
- 原型:每个对象都有一个
[[Prototype]]
,指向它的原型对象。 - 原型链:通过
[[Prototype]]
连接起来的链式结构,用于属性查找。 - 继承:通过原型链实现对象之间的继承。
- 性能:原型链过长会影响查找性能,需谨慎设计。
理解原型和原型链是掌握 JavaScript 面向对象编程的关键,也是深入理解 JavaScript 运行机制的基础。