原型是什么?就像一个共享模板

在 JavaScript 中,每个对象都有一个“隐藏的靠山”,这个靠山就是它的原型(prototype)。你可以把原型想象成一个模板,上面存放了一些属性和方法,供其他对象共享使用。

举个例子:

const person = {
  name: '张三',
  sayHello() {
    console.log(`你好,我是 ${this.name}`)
  }
}

console.log(person.sayHello()) // 你好,我是 张三
console.log(person.__proto__ === Object.prototype) // true

这里的 person 是一个对象,它有一个隐藏的 __proto__ 属性,指向它的原型 Object.prototype。这个原型就像一个工具箱,里面有一些默认的工具(比如 toString 方法),person 可以直接用。

关键点:每个对象都有一个 __proto__,它指向自己的原型对象。


原型链:一层一层往上找

原型链是 JavaScript 的核心机制之一。当你访问一个对象的属性或方法时,JavaScript 会按以下步骤查找:

  1. 先检查对象本身有没有这个属性。
  2. 如果没有,就去它的原型(__proto__ 指向的对象)上找。
  3. 如果原型上也没有,就继续找原型的原型。
  4. 一直找到链的尽头(Object.prototype__proto__null)。

来看个例子:

const animal = {
  eat() {
    return '我在吃东西'
  }
}

const dog = {
  bark() {
    return '汪汪汪'
  }
}

Object.setPrototypeOf(dog, animal)

console.log(dog.bark()) // 汪汪汪(来自 dog 自身)
console.log(dog.eat())  // 我在吃东西(来自原型 animal)

这里我们用 Object.setPrototypeOfdog 的原型设为 animal。当调用 dog.eat() 时,JavaScript 发现 dog 自身没有 eat 方法,就去它的原型 animal 上找,找到了就执行。

关键点:原型链就像一个家谱,属性和方法可以从“祖先”那里继承过来。


prototype__proto__:别搞混了!

这两个概念听起来很像,但作用完全不同:

  • prototype:是函数专有的属性,指向一个对象。这个对象会成为通过该函数(作为构造函数)创建的新对象的原型。
  • __proto__:是每个对象都有的属性,指向它的原型对象。

来看代码:

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

Person.prototype.sayHello = function() {
  console.log(`你好,我是 ${this.name}`)
}

const xiaoming = new Person('小明')

console.log(xiaoming.__proto__ === Person.prototype) // true
console.log(Person.prototype.__proto__ === Object.prototype) // true
  • Person.prototypePerson 函数的一个属性,定义了所有 Person 实例的共享方法(比如 sayHello)。
  • xiaoming.__proto__ 指向 Person.prototype,这就是 xiaoming 能调用 sayHello 的原因。

关键点prototype 是给构造函数用的,__proto__ 是给实例对象用的。


用构造函数创建对象:new 到底干了啥?

当你用 new 关键字调用一个函数时,JavaScript 在幕后做了几件事:

  1. 创建一个空对象 {}
  2. 把这个新对象的 __proto__ 设为构造函数的 prototype
  3. 把构造函数的 this 绑定到新对象上,执行构造函数代码。
  4. 如果构造函数没有返回对象,就返回这个新对象。

来看个例子:

function Dog(name) {
  this.name = name
}

Dog.prototype.bark = function() {
  console.log(`${this.name}:汪汪汪!`)
}

const husky = new Dog('哈士奇')
husky.bark() // 哈士奇:汪汪汪!

这里,husky__proto__ 指向 Dog.prototype,所以它能调用 bark 方法。

关键点new 是一个魔法操作,它把构造函数和原型链连了起来。


实现继承:让对象“传家宝”

JavaScript 的继承主要靠原型链实现。子类可以通过原型链访问父类的属性和方法。来看一个完整的继承例子:

function Animal(name) {
  this.name = name
}

Animal.prototype.eat = function() {
  console.log(`${this.name} 在吃东西`)
}

function Dog(name, breed) {
  Animal.call(this, name) // 继承 Animal 的属性
  this.breed = breed
}

// 设置原型链继承
Dog.prototype = Object.create(Animal.prototype)
Dog.prototype.constructor = Dog // 修复 constructor

Dog.prototype.bark = function() {
  console.log(`${this.breed} ${this.name}:汪汪汪!`)
}

const myDog = new Dog('旺财', '金毛')
myDog.bark() // 金毛 旺财:汪汪汪!
myDog.eat()  // 旺财 在吃东西

解释一下:

  1. Animal.call(this, name):调用 Animal 的构造函数,确保 Dog 实例有 name 属性。
  2. Object.create(Animal.prototype):创建了一个新对象,继承了 Animal.prototype 的属性和方法。
  3. Dog.prototype.constructor = Dog:修复 constructor,因为 Object.create 会让 constructor 指向错误的对象。

关键点:通过 call 继承属性,通过原型链继承方法。


实用技巧:让你的代码更顺手

1. 检查原型关系

想知道一个对象是不是另一个对象的原型?用 isPrototypeOf

console.log(Object.prototype.isPrototypeOf(myDog)) // true

想知道属性是来自对象自身还是原型链?用 hasOwnProperty

console.log(myDog.hasOwnProperty('name')) // true
console.log(myDog.hasOwnProperty('eat'))  // false

2. 谨慎扩展内置原型

你可能会想给内置对象(比如 Array)加点“新功能”:

Array.prototype.first = function() {
  return this[0]
}

const arr = [1, 2, 3]
console.log(arr.first()) // 1

虽然这很方便,但不推荐!因为:

  • 可能和第三方库或未来 JavaScript 标准的方法冲突。
  • 会影响所有数组实例,可能导致意外行为。

建议:如果需要扩展功能,考虑写独立的工具函数。


总结:从原理到实践

JavaScript 的原型和继承机制的核心在于原型链。通过 __proto__prototype,对象可以共享属性和方法;通过构造函数和原型链,可以实现灵活的继承。理解这些机制后,你就能写出更高效、更清晰的代码。

几点建议:

  • 多用 Object.createclass(ES6 提供的更现代的方式)来实现继承。
  • 避免直接操作 __proto__,因为它在现代代码中不推荐使用。
  • 遇到问题时,画出原型链,理清关系会更直观。