原型和原型链
原型(Prototype)和原型链(Prototype Chain)是JavaScript中的核心概念,它们是实现对象继承的基础。理解原型和原型链对于掌握JavaScript的面向对象编程至关重要。本文将详细介绍JavaScript中的原型和原型链,以及它们的应用。
1. 原型的概念
在JavaScript中,每个对象都有一个原型对象,这个原型对象是该对象的父对象。对象可以从其原型继承属性和方法。
1.1 构造函数、原型对象和实例对象
在JavaScript中,有三个重要的概念:构造函数、原型对象和实例对象。
- 构造函数:用于创建对象的函数,使用
new关键字调用。 - 原型对象:构造函数的
prototype属性指向的对象,包含了所有实例对象共享的属性和方法。 - 实例对象:通过构造函数创建的对象,有一个内部属性
[[Prototype]]指向原型对象。
使用方式:
// 构造函数
function Person(name, age) {
this.name = name;
this.age = age;
this.sayHello = function() {
console.log(`Hello, my name is ${this.name}!`);
};
}
// 原型对象
Person.prototype.sayAge = function() {
console.log(`I am ${this.age} years old!`);
};
// 实例对象
const person1 = new Person('John', 30);
const person2 = new Person('Jane', 25);
// 访问实例属性和方法
console.log(person1.name); // John
person1.sayHello(); // Hello, my name is John!
// 访问原型方法
person1.sayAge(); // I am 30 years old!
person2.sayAge(); // I am 25 years old!
// 检查原型关系
console.log(person1.__proto__ === Person.prototype); // true
console.log(person2.__proto__ === Person.prototype); // true
console.log(Person.prototype.constructor === Person); // true1.2 原型的作用
原型的主要作用是实现对象的继承和属性共享。通过将方法定义在原型对象上,可以让所有实例对象共享这些方法,减少内存使用。
使用方式:
// 不使用原型
function Person(name) {
this.name = name;
this.sayHello = function() {
console.log(`Hello, my name is ${this.name}!`);
};
}
const person1 = new Person('John');
const person2 = new Person('Jane');
console.log(person1.sayHello === person2.sayHello); // false(每个实例都有自己的sayHello方法)
// 使用原型
function PersonWithPrototype(name) {
this.name = name;
}
PersonWithPrototype.prototype.sayHello = function() {
console.log(`Hello, my name is ${this.name}!`);
};
const person3 = new PersonWithPrototype('John');
const person4 = new PersonWithPrototype('Jane');
console.log(person3.sayHello === person4.sayHello); // true(所有实例共享sayHello方法)2. 原型链
原型链是指当访问对象的属性或方法时,JavaScript会先在对象自身查找,如果找不到,就会沿着原型链向上查找,直到找到该属性或方法,或者到达原型链的末端(null)。
2.1 原型链的结构
在JavaScript中,每个对象都有一个原型对象,而原型对象也有自己的原型对象,这样就形成了一条原型链。原型链的末端是null。
使用方式:
// 构造函数
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function() {
console.log(`Hello, my name is ${this.name}!`);
};
// 实例对象
const person = new Person('John');
// 原型链
console.log(person.__proto__); // Person.prototype
console.log(person.__proto__.__proto__); // Object.prototype
console.log(person.__proto__.__proto__.__proto__); // null
// 访问原型链上的方法
person.sayHello(); // Hello, my name is John!(来自Person.prototype)
console.log(person.toString()); // [object Object](来自Object.prototype)2.2 原型链的查找机制
当访问对象的属性或方法时,JavaScript会按照以下步骤查找:
- 在对象自身查找该属性或方法。
- 如果找不到,就到对象的原型对象中查找。
- 如果还是找不到,就到原型对象的原型对象中查找,以此类推。
- 直到找到该属性或方法,或者到达原型链的末端(
null),此时返回undefined。
使用方式:
function Person(name) {
this.name = name;
}
Person.prototype.age = 30;
Person.prototype.sayHello = function() {
console.log(`Hello, my name is ${this.name}!`);
};
const person = new Person('John');
person.age = 25; // 实例属性,覆盖原型属性
// 查找属性
console.log(person.name); // John(自身属性)
console.log(person.age); // 25(自身属性,覆盖了原型属性)
console.log(person.gender); // undefined(原型链上找不到)
// 查找方法
person.sayHello(); // Hello, my name is ${this.name}!(来自原型)
console.log(person.toString()); // [object Object](来自Object.prototype)2.3 原型链的注意事项
- 属性遮蔽:当实例对象有一个与原型对象同名的属性时,实例对象的属性会遮蔽原型对象的属性。
- 原型链长度:原型链过长会影响性能,因为查找属性时需要遍历更长的链。
- 修改原型:修改原型对象会影响所有实例对象,即使是修改前创建的实例。
3. 继承
JavaScript中的继承主要通过原型链实现,以下是几种常见的继承方式:
3.1 原型链继承
原型链继承是最基本的继承方式,通过将子类的原型设置为父类的实例来实现继承。
使用方式:
// 父类
function Animal(name) {
this.name = name;
this.eat = function() {
console.log(`${this.name} is eating!`);
};
}
Animal.prototype.sleep = function() {
console.log(`${this.name} is sleeping!`);
};
// 子类
function Dog(name, breed) {
this.breed = breed;
}
// 原型链继承
Dog.prototype = new Animal();
Dog.prototype.constructor = Dog; // 修复constructor指向
// 添加子类方法
Dog.prototype.bark = function() {
console.log(`${this.name} is barking!`);
};
// 创建实例
const dog = new Dog('Buddy', 'Golden Retriever');
dog.name = 'Buddy'; // 需要手动设置父类属性
// 访问父类方法
dog.eat(); // Buddy is eating!
dog.sleep(); // Buddy is sleeping!
// 访问子类方法
dog.bark(); // Buddy is barking!缺点:
- 父类的构造函数会被调用多次。
- 子类实例无法向父类构造函数传递参数。
- 所有子类实例共享父类的引用类型属性。
3.2 构造函数继承
构造函数继承通过在子类构造函数中调用父类构造函数来实现继承。
使用方式:
// 父类
function Animal(name) {
this.name = name;
this.eat = function() {
console.log(`${this.name} is eating!`);
};
this.friends = ['Cat', 'Bird'];
}
// 子类
function Dog(name, breed) {
// 调用父类构造函数
Animal.call(this, name);
this.breed = breed;
}
// 添加子类方法
Dog.prototype.bark = function() {
console.log(`${this.name} is barking!`);
};
// 创建实例
const dog1 = new Dog('Buddy', 'Golden Retriever');
const dog2 = new Dog('Max', 'German Shepherd');
// 访问父类属性和方法
console.log(dog1.name); // Buddy
dog1.eat(); // Buddy is eating!
// 访问子类方法
dog1.bark(); // Buddy is barking!
// 引用类型属性不共享
dog1.friends.push('Mouse');
console.log(dog1.friends); // ['Cat', 'Bird', 'Mouse']
console.log(dog2.friends); // ['Cat', 'Bird']缺点:
- 父类的原型方法不会被继承。
- 每个实例都有父类方法的副本,浪费内存。
3.3 组合继承
组合继承结合了原型链继承和构造函数继承的优点,是最常用的继承方式。
使用方式:
// 父类
function Animal(name) {
this.name = name;
this.friends = ['Cat', 'Bird'];
}
Animal.prototype.eat = function() {
console.log(`${this.name} is eating!`);
};
Animal.prototype.sleep = function() {
console.log(`${this.name} is sleeping!`);
};
// 子类
function Dog(name, breed) {
// 调用父类构造函数,继承实例属性
Animal.call(this, name);
this.breed = breed;
}
// 原型链继承,继承原型方法
Dog.prototype = new Animal();
Dog.prototype.constructor = Dog; // 修复constructor指向
// 添加子类方法
Dog.prototype.bark = function() {
console.log(`${this.name} is barking!`);
};
// 创建实例
const dog1 = new Dog('Buddy', 'Golden Retriever');
const dog2 = new Dog('Max', 'German Shepherd');
// 访问父类方法
dog1.eat(); // Buddy is eating!
dog1.sleep(); // Buddy is sleeping!
// 访问子类方法
dog1.bark(); // Buddy is barking!
// 引用类型属性不共享
dog1.friends.push('Mouse');
console.log(dog1.friends); // ['Cat', 'Bird', 'Mouse']
console.log(dog2.friends); // ['Cat', 'Bird']优点:
- 既继承了父类的实例属性,又继承了父类的原型方法。
- 子类实例可以向父类构造函数传递参数。
- 引用类型属性不会被所有实例共享。
缺点:
- 父类构造函数会被调用两次(一次在创建子类原型时,一次在子类构造函数中)。
3.4 原型式继承
原型式继承通过Object.create()方法创建一个新对象,该对象的原型指向指定的对象。
使用方式:
// 原型对象
const animal = {
name: 'Animal',
eat: function() {
console.log(`${this.name} is eating!`);
}
};
// 创建新对象
const dog = Object.create(animal);
dog.name = 'Dog';
dog.bark = function() {
console.log(`${this.name} is barking!`);
};
// 访问属性和方法
console.log(dog.name); // Dog
dog.eat(); // Dog is eating!
dog.bark(); // Dog is barking!
// 检查原型关系
console.log(dog.__proto__ === animal); // true优点:
- 简单易用,适合创建基于现有对象的新对象。
缺点:
- 所有实例共享原型对象的引用类型属性。
3.5 寄生式继承
寄生式继承是在原型式继承的基础上,增强新对象的功能。
使用方式:
function createDog(original) {
const dog = Object.create(original);
dog.bark = function() {
console.log(`${this.name} is barking!`);
};
return dog;
}
// 原型对象
const animal = {
name: 'Animal',
eat: function() {
console.log(`${this.name} is eating!`);
}
};
// 创建实例
const dog = createDog(animal);
dog.name = 'Buddy';
// 访问方法
dog.eat(); // Buddy is eating!
dog.bark(); // Buddy is barking!优点:
- 可以增强新对象的功能。
缺点:
- 每个实例都有增强方法的副本,浪费内存。
3.6 寄生组合式继承
寄生组合式继承是组合继承的改进版,通过Object.create()方法创建子类原型,避免了父类构造函数被调用两次的问题。
使用方式:
// 父类
function Animal(name) {
this.name = name;
this.friends = ['Cat', 'Bird'];
}
Animal.prototype.eat = function() {
console.log(`${this.name} is eating!`);
};
// 子类
function Dog(name, breed) {
Animal.call(this, name);
this.breed = breed;
}
// 寄生组合式继承
function inheritPrototype(subType, superType) {
const prototype = Object.create(superType.prototype);
prototype.constructor = subType;
subType.prototype = prototype;
}
inheritPrototype(Dog, Animal);
// 添加子类方法
Dog.prototype.bark = function() {
console.log(`${this.name} is barking!`);
};
// 创建实例
const dog = new Dog('Buddy', 'Golden Retriever');
// 访问方法
dog.eat(); // Buddy is eating!
dog.bark(); // Buddy is barking!优点:
- 既继承了父类的实例属性,又继承了父类的原型方法。
- 避免了父类构造函数被调用两次的问题。
- 是最理想的继承方式。
3.7 ES6类继承
ES6引入了class关键字,使JavaScript的继承更加简洁和清晰。
使用方式:
// 父类
class Animal {
constructor(name) {
this.name = name;
this.friends = ['Cat', 'Bird'];
}
eat() {
console.log(`${this.name} is eating!`);
}
sleep() {
console.log(`${this.name} is sleeping!`);
}
}
// 子类
class Dog extends Animal {
constructor(name, breed) {
super(name); // 调用父类构造函数
this.breed = breed;
}
bark() {
console.log(`${this.name} is barking!`);
}
}
// 创建实例
const dog = new Dog('Buddy', 'Golden Retriever');
// 访问父类方法
dog.eat(); // Buddy is eating!
dog.sleep(); // Buddy is sleeping!
// 访问子类方法
dog.bark(); // Buddy is barking!
// 检查原型关系
console.log(dog instanceof Dog); // true
console.log(dog instanceof Animal); // true优点:
- 语法简洁清晰,类似于传统面向对象语言。
- 自动处理原型链,避免了手动设置原型的复杂性。
- 支持
super关键字,方便调用父类方法。
4. 原型的相关方法
4.1 Object.getPrototypeOf()
Object.getPrototypeOf()方法返回指定对象的原型对象。
使用方式:
function Person(name) {
this.name = name;
}
const person = new Person('John');
const prototype = Object.getPrototypeOf(person);
console.log(prototype === Person.prototype); // true4.2 Object.setPrototypeOf()
Object.setPrototypeOf()方法设置指定对象的原型对象。
使用方式:
const animal = {
eat: function() {
console.log(`${this.name} is eating!`);
}
};
const dog = {
name: 'Buddy'
};
// 设置原型
Object.setPrototypeOf(dog, animal);
// 访问原型方法
dog.eat(); // Buddy is eating!4.3 Object.create()
Object.create()方法创建一个新对象,该对象的原型指向指定的对象。
使用方式:
const animal = {
eat: function() {
console.log(`${this.name} is eating!`);
}
};
// 创建新对象
const dog = Object.create(animal);
dog.name = 'Buddy';
dog.eat(); // Buddy is eating!4.4 hasOwnProperty()
hasOwnProperty()方法检查对象自身是否具有指定的属性(不包括原型链上的属性)。
使用方式:
function Person(name) {
this.name = name;
}
Person.prototype.age = 30;
const person = new Person('John');
console.log(person.hasOwnProperty('name')); // true(自身属性)
console.log(person.hasOwnProperty('age')); // false(原型属性)
console.log(person.hasOwnProperty('eat')); // false(不存在的属性)4.5 in操作符
in操作符检查对象是否具有指定的属性(包括原型链上的属性)。
使用方式:
function Person(name) {
this.name = name;
}
Person.prototype.age = 30;
const person = new Person('John');
console.log('name' in person); // true(自身属性)
console.log('age' in person); // true(原型属性)
console.log('eat' in person); // false(不存在的属性)4.6 Object.keys()
Object.keys()方法返回对象自身的可枚举属性的数组(不包括原型链上的属性)。
使用方式:
function Person(name) {
this.name = name;
}
Person.prototype.age = 30;
Person.prototype.eat = function() {};
const person = new Person('John');
person.gender = 'male';
console.log(Object.keys(person)); // ['name', 'gender']4.7 Object.getOwnPropertyNames()
Object.getOwnPropertyNames()方法返回对象自身的所有属性的数组(包括不可枚举属性,不包括原型链上的属性)。
使用方式:
function Person(name) {
this.name = name;
Object.defineProperty(this, 'age', {
value: 30,
enumerable: false
});
}
const person = new Person('John');
console.log(Object.getOwnPropertyNames(person)); // ['name', 'age']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('John');
const person2 = new Person('Jane');
console.log(person1.sayHello === person2.sayHello); // true(共享同一个方法)5.2 实现继承
通过原型链,可以实现对象之间的继承关系。
使用方式:
// 父类
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;
// 添加子类方法
Dog.prototype.bark = function() {
console.log(`${this.name} is barking!`);
};
const dog = new Dog('Buddy', 'Golden Retriever');
dog.eat(); // 继承自Animal
dog.bark(); // 子类自己的方法5.3 扩展内置对象
通过修改内置对象的原型,可以扩展内置对象的功能。
使用方式:
// 扩展Array原型
Array.prototype.sum = function() {
return this.reduce((total, current) => total + current, 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注意:扩展内置对象的原型可能会与其他代码产生冲突,应谨慎使用。
6. 面试常见问题
1. 什么是原型和原型链?
原型是JavaScript中每个对象都具有的一个内部属性,它指向另一个对象,包含了所有实例对象共享的属性和方法。
原型链是指当访问对象的属性或方法时,JavaScript会先在对象自身查找,如果找不到,就会沿着原型链向上查找,直到找到该属性或方法,或者到达原型链的末端(null)。
2. 构造函数、原型对象和实例对象之间的关系是什么?
- 构造函数:用于创建对象的函数,有一个
prototype属性指向原型对象。 - 原型对象:包含了所有实例对象共享的属性和方法,有一个
constructor属性指向构造函数。 - 实例对象:通过构造函数创建的对象,有一个内部属性
[[Prototype]]指向原型对象。
3. 如何检查一个对象是否是另一个对象的实例?
可以使用instanceof操作符或Object.prototype.isPrototypeOf()方法。
使用方式:
function Person(name) {
this.name = name;
}
const person = new Person('John');
// 使用instanceof
console.log(person instanceof Person); // true
console.log(person instanceof Object); // true
// 使用isPrototypeOf
console.log(Person.prototype.isPrototypeOf(person)); // true
console.log(Object.prototype.isPrototypeOf(person)); // true4. 什么是原型链继承?它有什么优缺点?
原型链继承是通过将子类的原型设置为父类的实例来实现继承。
优点:
- 简单易用,代码量少。
- 子类可以继承父类的所有属性和方法。
缺点:
- 父类的构造函数会被调用多次。
- 子类实例无法向父类构造函数传递参数。
- 所有子类实例共享父类的引用类型属性。
5. 什么是组合继承?它有什么优缺点?
组合继承是结合了原型链继承和构造函数继承的优点,通过在子类构造函数中调用父类构造函数来继承实例属性,通过将子类的原型设置为父类的实例来继承原型方法。
优点:
- 既继承了父类的实例属性,又继承了父类的原型方法。
- 子类实例可以向父类构造函数传递参数。
- 引用类型属性不会被所有实例共享。
缺点:
- 父类构造函数会被调用两次(一次在创建子类原型时,一次在子类构造函数中)。
6. 什么是寄生组合式继承?它为什么是最理想的继承方式?
寄生组合式继承是组合继承的改进版,通过Object.create()方法创建子类原型,避免了父类构造函数被调用两次的问题。
为什么是最理想的继承方式:
- 既继承了父类的实例属性,又继承了父类的原型方法。
- 避免了父类构造函数被调用两次的问题。
- 代码简洁,性能良好。
7. ES6的class继承与ES5的原型继承有什么区别?
ES6的class继承:
- 使用
class和extends关键字,语法简洁清晰。 - 自动处理原型链,避免了手动设置原型的复杂性。
- 支持
super关键字,方便调用父类方法。 - 本质上是ES5原型继承的语法糖。
ES5的原型继承:
- 使用构造函数和原型对象,语法较为复杂。
- 需要手动设置原型链和修复
constructor指向。 - 没有
super关键字,调用父类方法较为麻烦。
8. 如何判断一个属性是对象自身的还是来自原型链?
可以使用hasOwnProperty()方法判断一个属性是否是对象自身的。
使用方式:
function Person(name) {
this.name = name;
}
Person.prototype.age = 30;
const person = new Person('John');
console.log(person.hasOwnProperty('name')); // true(自身属性)
console.log(person.hasOwnProperty('age')); // false(原型属性)9. 什么是属性遮蔽?
属性遮蔽是指当实例对象有一个与原型对象同名的属性时,实例对象的属性会遮蔽原型对象的属性,即访问该属性时会返回实例对象的属性值,而不是原型对象的属性值。
使用方式:
function Person(name) {
this.name = name;
}
Person.prototype.age = 30;
const person = new Person('John');
person.age = 25; // 实例属性,覆盖原型属性
console.log(person.age); // 25(实例属性,遮蔽了原型属性)10. 如何扩展一个对象的功能而不修改其原型?
可以使用以下方法:
方法1:使用对象组合
const animal = {
eat: function() {
console.log(`${this.name} is eating!`);
}
};
const dog = {
...animal,
name: 'Dog',
bark: function() {
console.log(`${this.name} is barking!`);
}
};方法2:使用工厂函数
function createDog(name) {
const dog = {
name: name,
eat: function() {
console.log(`${this.name} is eating!`);
},
bark: function() {
console.log(`${this.name} is barking!`);
}
};
return dog;
}
const dog = createDog('Buddy');方法3:使用类继承
class Animal {
constructor(name) {
this.name = name;
}
eat() {
console.log(`${this.name} is eating!`);
}
}
class Dog extends Animal {
constructor(name) {
super(name);
}
bark() {
console.log(`${this.name} is barking!`);
}
}
const dog = new Dog('Buddy');11. 原型链的末端是什么?
原型链的末端是null,它表示没有原型。
使用方式:
const obj = {};
console.log(obj.__proto__); // Object.prototype
console.log(obj.__proto__.__proto__); // null12. 如何避免原型链过长导致的性能问题?
可以通过以下方法避免原型链过长:
- 合理设计继承层次:尽量减少继承层次,避免创建过深的原型链。
- 使用组合而非继承:对于复杂的功能,优先使用对象组合而不是继承。
- 缓存频繁访问的属性:对于需要频繁访问的原型链上的属性,可以将其缓存到实例对象上。
- 使用ES6的class:ES6的class语法更加简洁清晰,有助于避免原型链相关的错误。
7. 总结
原型和原型链是JavaScript中的核心概念,它们是实现对象继承的基础。
- 原型:每个对象都有一个原型对象,包含了所有实例对象共享的属性和方法。
- 原型链:当访问对象的属性或方法时,JavaScript会沿着原型链向上查找,直到找到该属性或方法,或者到达原型链的末端(
null)。 - 继承方式:
- 原型链继承
- 构造函数继承
- 组合继承
- 原型式继承
- 寄生式继承
- 寄生组合式继承
- ES6类继承
- 原型的相关方法:
Object.getPrototypeOf()Object.setPrototypeOf()Object.create()hasOwnProperty()in操作符Object.keys()Object.getOwnPropertyNames()
通过理解和掌握原型和原型链的概念,你可以更好地理解JavaScript的面向对象编程,创建更加高效、可维护的代码。