Skip to content

JavaScript prototype

prototype 的概念

在 JavaScript 中,每个函数都有一个 prototype 属性,它是一个对象,用于存储可以被该函数的所有实例共享的属性和方法。当创建一个新对象时,该对象会继承其构造函数的 prototype 对象的属性和方法。

prototype 的作用

  1. 属性和方法共享:所有实例共享同一个 prototype 对象的属性和方法,节省内存
  2. 继承:通过修改 prototype 对象,可以实现继承
  3. 扩展内置对象:可以通过修改内置对象的 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); // 输出: true

ES6 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); // 输出: true

prototype 的方法

Object.getPrototypeOf()

Object.getPrototypeOf() 方法返回对象的原型:

javascript
function Person(name) {
  this.name = name;
}

const person = new Person("John");
console.log(Object.getPrototypeOf(person) === Person.prototype); // 输出: true

Object.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); // 输出: true

hasOwnProperty()

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,提高代码的可读性和可维护性。