JavaScript原型
原型是JavaScript实现面向对象编程的核心机制,它允许对象继承其他对象的属性和方法,实现代码复用和共享。理解原型和原型链是掌握JavaScript面向对象编程的关键。
原型的基本概念
在JavaScript中,每个对象都有一个原型(prototype),原型也是一个对象。当访问对象的属性或方法时,如果对象本身没有该属性或方法,JavaScript会自动到其原型对象中查找,这个过程会持续到原型链的末端。
原型链
当访问对象的属性或方法时,JavaScript会按照以下顺序查找:
- 首先在对象本身查找
- 如果找不到,到对象的原型中查找
- 如果还找不到,到原型的原型中查找
- 以此类推,直到找到该属性或方法,或者到达原型链的末端(null)
这种链式查找机制称为原型链。
构造函数与原型
在JavaScript中,使用构造函数创建对象时,构造函数的prototype属性指向一个对象,该对象是通过该构造函数创建的所有实例的原型。
构造函数的prototype属性
// 定义构造函数
function Person(name, age) {
this.name = name;
this.age = age;
}
// 在构造函数的原型上添加方法
Person.prototype.greet = function() {
console.log(`Hello, my name is ${this.name}!`);
};
// 使用构造函数创建实例
const person1 = new Person('Alice', 30);
const person2 = new Person('Bob', 25);
// 实例可以访问原型上的方法
person1.greet(); // Hello, my name is Alice!
person2.greet(); // Hello, my name is Bob!
// 检查实例的原型
console.log(Object.getPrototypeOf(person1)); // Person.prototype
console.log(Object.getPrototypeOf(person1) === Person.prototype); // true
console.log(Object.getPrototypeOf(person2) === Person.prototype); // true实例的__proto__属性
每个对象实例都有一个__proto__属性(非标准属性,但大多数浏览器支持),指向其原型对象。
// 访问实例的__proto__属性
console.log(person1.__proto__); // Person.prototype
console.log(person1.__proto__ === Person.prototype); // true
// 注意:__proto__是实例的属性,prototype是构造函数的属性
console.log(Person.prototype); // 原型对象
console.log(Person.__proto__); // Function.prototype(构造函数本身的原型)constructor属性
原型对象上有一个constructor属性,指向创建该原型的构造函数。
// 访问原型的constructor属性
console.log(Person.prototype.constructor); // Person构造函数
console.log(Person.prototype.constructor === Person); // true
// 实例可以通过原型访问constructor属性
console.log(person1.constructor); // Person构造函数
console.log(person1.constructor === Person); // true原型继承
JavaScript通过原型链实现继承,子类的原型指向父类的实例,从而继承父类的属性和方法。
基于构造函数的继承
// 父类构造函数
function Animal(name) {
this.name = name;
this.type = 'Animal';
}
// 父类原型方法
Animal.prototype.makeSound = function() {
console.log(`${this.name} makes a sound.`);
};
// 子类构造函数
function Dog(name, breed) {
// 调用父类构造函数,继承实例属性
Animal.call(this, name);
this.breed = breed;
this.type = 'Dog';
}
// 设置子类原型为父类实例,继承原型方法
Dog.prototype = new Animal();
// 修复constructor属性
Dog.prototype.constructor = Dog;
// 子类原型方法
Dog.prototype.bark = function() {
console.log(`${this.name} barks: Woof!`);
};
// 创建子类实例
const dog = new Dog('Buddy', 'Golden Retriever');
// 访问继承的属性和方法
dog.makeSound(); // Buddy makes a sound.
dog.bark(); // Buddy barks: Woof!
console.log(dog.name); // Buddy
console.log(dog.type); // Dog(子类覆盖了父类的属性)
console.log(dog.breed); // Golden Retriever
// 检查原型链
console.log(dog.__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); // true基于Object.create()的继承
ES5引入了Object.create()方法,用于创建一个新对象,指定其原型对象。
// 父类对象
const animal = {
type: 'Animal',
makeSound: function() {
console.log(`${this.name} makes a sound.`);
}
};
// 使用Object.create()创建子类对象,继承animal
const dog = Object.create(animal);
dog.name = 'Buddy';
dog.breed = 'Golden Retriever';
dog.type = 'Dog';
dog.bark = function() {
console.log(`${this.name} barks: Woof!`);
};
dog.makeSound(); // Buddy makes a sound.
dog.bark(); // Buddy barks: Woof!
// 检查原型链
console.log(Object.getPrototypeOf(dog) === animal); // true组合继承
组合继承结合了构造函数继承和原型继承,是JavaScript中最常用的继承方式。
// 父类构造函数
function Animal(name) {
this.name = name;
this.colors = ['black', 'white'];
}
// 父类原型方法
Animal.prototype.makeSound = function() {
console.log(`${this.name} makes a sound.`);
};
// 子类构造函数
function Dog(name, breed) {
// 构造函数继承:继承实例属性
Animal.call(this, name);
this.breed = breed;
}
// 原型继承:继承原型方法
Dog.prototype = Object.create(Animal.prototype);
// 修复constructor属性
Dog.prototype.constructor = Dog;
// 子类原型方法
Dog.prototype.bark = function() {
console.log(`${this.name} barks: Woof!`);
};
// 创建实例
const dog1 = new Dog('Buddy', 'Golden Retriever');
const dog2 = new Dog('Max', 'German Shepherd');
// 修改dog1的colors数组,不会影响dog2
dog1.colors.push('brown');
console.log(dog1.colors); // ['black', 'white', 'brown']
console.log(dog2.colors); // ['black', 'white']
// 访问继承的方法
dog1.makeSound(); // Buddy makes a sound.
dog2.bark(); // Max barks: Woof!原型对象的方法
JavaScript提供了几个用于操作原型的方法:
1. Object.getPrototypeOf()
返回对象的原型。
const obj = {};
const proto = Object.getPrototypeOf(obj);
console.log(proto === Object.prototype); // true2. Object.setPrototypeOf()
设置对象的原型。
const obj = {};
const proto = { greet: () => console.log('Hello!') };
Object.setPrototypeOf(obj, proto);
obj.greet(); // Hello!3. Object.create()
创建一个新对象,指定其原型对象。
const proto = { greet: () => console.log('Hello!') };
const obj = Object.create(proto);
obj.greet(); // Hello!4. Object.hasOwnProperty()
判断对象是否有指定的自有属性(不包括原型链上的属性)。
const person = {
name: 'Alice',
age: 30
};
console.log(person.hasOwnProperty('name')); // true
console.log(person.hasOwnProperty('toString')); // false(toString是原型方法)5. in操作符
判断对象是否有指定的属性(包括原型链上的属性)。
const person = {
name: 'Alice',
age: 30
};
console.log('name' in person); // true
console.log('toString' in person); // true(toString是原型方法)
console.log('gender' in person); // false6. instanceof操作符
判断对象是否是构造函数的实例,基于原型链。
function Person() {}
const person = new Person();
console.log(person instanceof Person); // true
console.log(person instanceof Object); // true
console.log([] instanceof Array); // true
console.log([] instanceof Object); // true原型的最佳实践
- 使用原型共享方法:将公共方法放在原型上,避免每个实例都创建相同的方法,节省内存。
- 避免修改内置对象原型:修改
Object.prototype、Array.prototype等内置对象的原型可能会导致命名冲突和性能问题。 - 使用Object.create()创建对象:创建对象时,优先使用
Object.create()方法,明确指定原型。 - 修复constructor属性:使用原型继承时,记得修复子类的
constructor属性。 - 避免深度继承:原型链不宜过长,否则会影响属性查找性能。
- 使用组合继承:结合构造函数继承和原型继承,既继承实例属性又继承原型方法。
原型与ES6类的关系
ES6引入了class语法,使JavaScript的面向对象编程更加直观,但底层仍然基于原型机制实现。
ES6类与原型的对应关系
// ES6类
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
console.log(`Hello, my name is ${this.name}!`);
}
}
// 等价于ES5构造函数
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.greet = function() {
console.log(`Hello, my name is ${this.name}!`);
};
// ES6类继承
class Student extends Person {
constructor(name, age, grade) {
super(name, age);
this.grade = grade;
}
study() {
console.log(`${this.name} is studying.`);
}
}
// 等价于ES5组合继承
function Student(name, age, grade) {
Person.call(this, name, age);
this.grade = grade;
}
Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student;
Student.prototype.study = function() {
console.log(`${this.name} is studying.`);
};原型的常见误区
1. 混淆__proto__和prototype
__proto__是实例的属性,指向其原型对象prototype是构造函数的属性,指向实例的原型对象
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); // true2. 原型上的引用类型属性共享
如果原型上有引用类型属性,所有实例都会共享该属性,修改一个实例的该属性会影响其他实例。
function Person() {
// 实例属性,每个实例都有独立副本
this.hobbies = [];
}
// 不推荐:原型上的引用类型属性,所有实例共享
Person.prototype.skills = [];
const person1 = new Person();
const person2 = new Person();
// 修改实例属性,不影响其他实例
person1.hobbies.push('reading');
console.log(person1.hobbies); // ['reading']
console.log(person2.hobbies); // []
// 修改原型上的引用类型属性,影响所有实例
person1.skills.push('coding');
console.log(person1.skills); // ['coding']
console.log(person2.skills); // ['coding'](被影响)3. 原型链查找性能
原型链查找会影响性能,尤其是原型链较长时。访问对象自身的属性比访问原型链上的属性更快。
const obj = {
name: 'Alice'
// 自身属性,访问更快
};
// 原型属性,访问较慢
obj.__proto__.age = 30;
console.log(obj.name); // 自身属性,更快
console.log(obj.age); // 原型属性,较慢原型的应用场景
1. 实现继承
原型链是JavaScript实现继承的主要方式,允许子类继承父类的属性和方法。
2. 共享方法
将公共方法放在原型上,所有实例共享,节省内存。
function Circle(radius) {
this.radius = radius;
}
// 公共方法放在原型上,所有实例共享
Circle.prototype.getArea = function() {
return Math.PI * this.radius * this.radius;
};
const circle1 = new Circle(5);
const circle2 = new Circle(10);
console.log(circle1.getArea()); // 78.53981633974483
console.log(circle2.getArea()); // 314.1592653589793
console.log(circle1.getArea === circle2.getArea); // true(共享同一个方法)3. 扩展内置对象
虽然不推荐,但可以通过修改原型扩展内置对象的功能。
// 扩展Array原型,添加求和方法
Array.prototype.sum = function() {
return this.reduce((acc, current) => acc + current, 0);
};
const numbers = [1, 2, 3, 4, 5];
console.log(numbers.sum()); // 154. 创建对象池
使用原型创建对象池,复用对象,提高性能。
// 对象池
function ObjectPool() {
this.pool = [];
}
ObjectPool.prototype.get = function() {
return this.pool.length > 0 ? this.pool.pop() : {};
};
ObjectPool.prototype.release = function(obj) {
// 清空对象属性
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
delete obj[key];
}
}
this.pool.push(obj);
};
// 使用对象池
const pool = new ObjectPool();
const obj1 = pool.get();
obj1.name = 'Alice';
console.log(obj1.name); // Alice
pool.release(obj1);
const obj2 = pool.get();
console.log(obj2.name); // undefined(对象已被清空)
console.log(obj1 === obj2); // true(复用了同一个对象)总结
原型是JavaScript实现面向对象编程的核心机制,它通过原型链实现对象继承和属性方法共享。理解原型和原型链是掌握JavaScript面向对象编程的关键。
- 每个对象都有一个原型,原型也是一个对象
- 构造函数的
prototype属性指向实例的原型 - 实例的
__proto__属性指向其原型 - 原型链是属性查找的链式机制
- 可以通过原型实现继承和共享方法
- ES6的
class语法底层基于原型机制
掌握原型和原型链的概念,有助于编写更高效、更可维护的JavaScript代码,特别是在面向对象编程和组件开发中。