Appearance
JavaScript prototype
prototype 的概念
在 JavaScript 中,每个函数都有一个 prototype 属性,它是一个对象,用于存储可以被该函数的所有实例共享的属性和方法。当创建一个新对象时,该对象会继承其构造函数的 prototype 对象的属性和方法。
prototype 的作用
- 属性和方法共享:所有实例共享同一个
prototype对象的属性和方法,节省内存 - 继承:通过修改
prototype对象,可以实现继承 - 扩展内置对象:可以通过修改内置对象的
prototype来扩展其功能
构造函数和 prototype
构造函数
构造函数用于创建对象:
javascript
function Person(name, age) {
this.name = name;
this.age = age;
// 方法定义在构造函数内,每个实例都会有一个新的方法副本
this.greet = function () {
return `Hello, my name is ${this.name}!`;
};
}
const person1 = new Person("John", 30);
const person2 = new Person("Jane", 25);
console.log(person1.greet()); // 输出: Hello, my name is John!
console.log(person2.greet()); // 输出: Hello, my name is Jane!
console.log(person1.greet === person2.greet); // 输出: false(不同的方法副本)使用 prototype
将方法定义在 prototype 上,实现方法共享:
javascript
function Person(name, age) {
this.name = name;
this.age = age;
}
// 方法定义在 prototype 上,所有实例共享同一个方法
Person.prototype.greet = function () {
return `Hello, my name is ${this.name}!`;
};
const person1 = new Person("John", 30);
const person2 = new Person("Jane", 25);
console.log(person1.greet()); // 输出: Hello, my name is John!
console.log(person2.greet()); // 输出: Hello, my name is Jane!
console.log(person1.greet === person2.greet); // 输出: true(相同的方法)原型链
原型链的概念
当访问对象的属性或方法时,JavaScript 会先在对象自身查找,如果找不到,就会沿着原型链向上查找,直到找到该属性或方法,或者到达原型链的末尾(null)。
原型链的结构
- 每个对象都有一个
__proto__属性(ES6 中标准化为Object.getPrototypeOf()),指向其原型对象 - 原型对象也是一个对象,它也有自己的原型对象,形成一个链式结构,称为原型链
- 原型链的末尾是
Object.prototype,它的原型是null
javascript
function Person(name) {
this.name = name;
}
Person.prototype.greet = function () {
return `Hello, my name is ${this.name}!`;
};
const person = new Person("John");
// 原型链:person -> Person.prototype -> Object.prototype -> null
console.log(person.name); // 输出: John(自身属性)
console.log(person.greet()); // 输出: Hello, my name is John!(来自 Person.prototype)
console.log(person.toString()); // 输出: [object Object](来自 Object.prototype)
console.log(person.nonexistent); // 输出: undefined(原型链中找不到)
// 检查原型
console.log(Object.getPrototypeOf(person) === Person.prototype); // 输出: true
console.log(Object.getPrototypeOf(Person.prototype) === Object.prototype); // 输出: true
console.log(Object.getPrototypeOf(Object.prototype) === null); // 输出: true继承与 prototype
原型继承
通过修改 prototype 实现继承:
javascript
// 父构造函数
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function () {
return `${this.name} makes a noise.`;
};
// 子构造函数
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 () {
return `${this.name} barks!`;
};
// 重写父构造函数的方法
Dog.prototype.speak = function () {
return `${this.name} barks!`;
};
const dog = new Dog("Rex", "German Shepherd");
console.log(dog.name); // 输出: Rex
console.log(dog.breed); // 输出: German Shepherd
console.log(dog.bark()); // 输出: Rex barks!
console.log(dog.speak()); // 输出: Rex barks!(重写后的方法)
// 检查原型链
console.log(dog instanceof Dog); // 输出: true
console.log(dog instanceof Animal); // 输出: true
console.log(dog instanceof Object); // 输出: trueES6 class 继承
ES6 引入了 class 关键字,使继承更简洁:
javascript
// 父类
class Animal {
constructor(name) {
this.name = name;
}
speak() {
return `${this.name} makes a noise.`;
}
}
// 子类
class Dog extends Animal {
constructor(name, breed) {
super(name); // 调用父类构造函数
this.breed = breed;
}
bark() {
return `${this.name} barks!`;
}
// 重写父类方法
speak() {
return `${this.name} barks!`;
}
}
const dog = new Dog("Rex", "German Shepherd");
console.log(dog.name); // 输出: Rex
console.log(dog.breed); // 输出: German Shepherd
console.log(dog.bark()); // 输出: Rex barks!
console.log(dog.speak()); // 输出: Rex barks!(重写后的方法)
// 检查原型链
console.log(dog instanceof Dog); // 输出: true
console.log(dog instanceof Animal); // 输出: true
console.log(dog instanceof Object); // 输出: trueprototype 的方法
Object.getPrototypeOf()
Object.getPrototypeOf() 方法返回对象的原型:
javascript
function Person(name) {
this.name = name;
}
const person = new Person("John");
console.log(Object.getPrototypeOf(person) === Person.prototype); // 输出: trueObject.setPrototypeOf()
Object.setPrototypeOf() 方法设置对象的原型:
javascript
const person = { name: "John" };
const animal = {
speak: function () {
return "Woof!";
},
};
// 设置 person 的原型为 animal
Object.setPrototypeOf(person, animal);
console.log(person.name); // 输出: John
console.log(person.speak()); // 输出: Woof!Object.create()
Object.create() 方法创建一个新对象,使用指定的原型:
javascript
const animal = {
speak: function () {
return "Woof!";
},
};
// 创建以 animal 为原型的新对象
const dog = Object.create(animal);
dog.name = "Rex";
console.log(dog.name); // 输出: Rex
console.log(dog.speak()); // 输出: Woof!
console.log(Object.getPrototypeOf(dog) === animal); // 输出: truehasOwnProperty()
hasOwnProperty() 方法检查对象是否有自己的属性(不包括继承的属性):
javascript
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("age" in person); // 输出: true(包括继承的属性)扩展内置对象
扩展 Array.prototype
javascript
// 扩展 Array.prototype
Array.prototype.sum = function () {
return this.reduce((total, num) => total + num, 0);
};
const numbers = [1, 2, 3, 4, 5];
console.log(numbers.sum()); // 输出: 15
// 扩展 Array.prototype
Array.prototype.average = function () {
if (this.length === 0) return 0;
return this.sum() / this.length;
};
console.log(numbers.average()); // 输出: 3扩展 String.prototype
javascript
// 扩展 String.prototype
String.prototype.capitalize = function () {
return this.charAt(0).toUpperCase() + this.slice(1);
};
const str = "hello";
console.log(str.capitalize()); // 输出: Hello
// 扩展 String.prototype
String.prototype.truncate = function (length) {
if (this.length <= length) {
return this;
}
return this.slice(0, length) + "...";
};
const longStr = "Hello, world!";
console.log(longStr.truncate(5)); // 输出: Hello...注意:扩展内置对象的
prototype可能会与未来的 JavaScript 版本冲突,或者与其他库冲突,应该谨慎使用。
prototype 的最佳实践
1. 将方法定义在 prototype 上
将方法定义在 prototype 上,实现方法共享,节省内存:
javascript
// 好的做法
function Person(name) {
this.name = name;
}
Person.prototype.greet = function () {
return `Hello, my name is ${this.name}!`;
};
// 不好的做法
function Person(name) {
this.name = name;
this.greet = function () {
return `Hello, my name is ${this.name}!`;
};
}2. 使用 Object.create() 实现继承
使用 Object.create() 实现原型继承,避免直接修改 prototype:
javascript
// 好的做法
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function () {
return `${this.name} makes a noise.`;
};
function Dog(name, breed) {
Animal.call(this, name);
this.breed = breed;
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
// 不好的做法
function Dog(name, breed) {
Animal.call(this, name);
this.breed = breed;
}
Dog.prototype = Animal.prototype; // 直接赋值,会影响父构造函数的 prototype
Dog.prototype.constructor = Dog;3. 避免使用 proto
__proto__ 是一个非标准属性(虽然大多数浏览器支持),应该使用 Object.getPrototypeOf() 和 Object.setPrototypeOf() 代替:
javascript
// 好的做法
const person = { name: "John" };
const animal = {
speak: function () {
return "Woof!";
},
};
Object.setPrototypeOf(person, animal);
console.log(Object.getPrototypeOf(person));
// 不好的做法
const person = { name: "John" };
const animal = {
speak: function () {
return "Woof!";
},
};
person.__proto__ = animal; // 非标准
console.log(person.__proto__); // 非标准4. 使用 ES6 class
对于现代 JavaScript,优先使用 ES6 class 语法,它更简洁、易读:
javascript
// 好的做法
class Person {
constructor(name) {
this.name = name;
}
greet() {
return `Hello, my name is ${this.name}!`;
}
}
// 不好的做法
function Person(name) {
this.name = name;
}
Person.prototype.greet = function () {
return `Hello, my name is ${this.name}!`;
};5. 谨慎扩展内置对象
扩展内置对象的 prototype 可能会与未来的 JavaScript 版本冲突,或者与其他库冲突,应该谨慎使用:
javascript
// 谨慎使用
if (!Array.prototype.sum) {
Array.prototype.sum = function () {
return this.reduce((total, num) => total + num, 0);
};
}prototype 的常见错误
1. 忘记修复构造函数指向
在实现继承时,忘记修复 constructor 指向:
javascript
function Animal(name) {
this.name = name;
}
function Dog(name, breed) {
Animal.call(this, name);
this.breed = breed;
}
// 错误:忘记修复 constructor 指向
Dog.prototype = Object.create(Animal.prototype);
const dog = new Dog("Rex", "German Shepherd");
console.log(dog.constructor); // 输出: Animal(错误的构造函数指向)
// 正确:修复 constructor 指向
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
const dog2 = new Dog("Rex", "German Shepherd");
console.log(dog2.constructor); // 输出: Dog(正确的构造函数指向)2. 直接修改 prototype
直接修改 prototype 会丢失原有的 constructor 指向:
javascript
function Person(name) {
this.name = name;
}
// 错误:直接修改 prototype,丢失 constructor 指向
Person.prototype = {
greet: function () {
return `Hello, my name is ${this.name}!`;
},
};
const person = new Person("John");
console.log(person.constructor); // 输出: Object(错误的构造函数指向)
// 正确:保留 constructor 指向
Person.prototype = {
constructor: Person,
greet: function () {
return `Hello, my name is ${this.name}!`;
},
};
const person2 = new Person("John");
console.log(person2.constructor); // 输出: Person(正确的构造函数指向)3. 混淆 proto 和 prototype
__proto__ 是对象的属性,指向其原型;prototype 是构造函数的属性,用于存储实例共享的方法和属性:
javascript
function Person(name) {
this.name = name;
}
const person = new Person("John");
console.log(person.__proto__ === Person.prototype); // 输出: true
console.log(Person.prototype.constructor === Person); // 输出: true
console.log(person.constructor === Person); // 输出: true(通过原型链查找)4. 原型链污染
修改原型对象会影响所有实例:
javascript
function Person(name) {
this.name = name;
}
Person.prototype.greet = function () {
return `Hello, my name is ${this.name}!`;
};
const person1 = new Person("John");
const person2 = new Person("Jane");
// 修改原型对象
Person.prototype.greet = function () {
return `Hi, I'm ${this.name}!`;
};
// 所有实例都会受到影响
console.log(person1.greet()); // 输出: Hi, I'm John!
console.log(person2.greet()); // 输出: Hi, I'm Jane!小结
prototype 是 JavaScript 中实现继承和方法共享的核心机制。每个函数都有一个 prototype 属性,它是一个对象,用于存储可以被该函数的所有实例共享的属性和方法。当创建一个新对象时,该对象会继承其构造函数的 prototype 对象的属性和方法。通过修改 prototype 对象,可以实现继承和扩展对象的功能。在现代 JavaScript 中,ES6 引入的 class 语法提供了更简洁、易读的方式来实现继承,但它仍然基于原型继承机制。在实际开发中,应该遵循最佳实践,合理使用 prototype,提高代码的可读性和可维护性。