JavaScript 原型的深入指南

Posted Jay_帅小伙

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JavaScript 原型的深入指南相关的知识,希望对你有一定的参考价值。

不学会怎么处理对象,你在 javascript 道路就就走不了多远。它们几乎是 JavaScript 编程语言每个方面的基础。事实上,学习如何创建对象可能是你刚开始学习的第一件事。

对象是键/值对。创建对象的最常用方法是使用花括号{},并使用点表示法向对象添加属性和方法。

let animal = {}
animal.name = 'Leo'
animal.energy = 10

animal.eat = function (amount) {
  console.log(`${this.name} is eating.`)
  this.energy += amount
}

animal.sleep = function (length) {
  console.log(`${this.name} is sleeping.`)
  this.energy += length
}

animal.play = function (length) {
  console.log(`${this.name} is playing.`)
  this.energy -= length
}

现在,在我们的应用程序中,我们需要创建多个 animal。当然,下一步是将逻辑封装,当我们需要创建新 animal 时,只需调用函数即可,我们将这种模式称为函数的实例化(unctional Instantiation),我们将函数本身称为“构造函数”,因为它负责“构造”一个新对象。

函数的实例化

function Animal (name, energy) {
  let animal = {}
  animal.name = name
  animal.energy = energy

  animal.eat = function (amount) {
    console.log(`${this.name} is eating.`)
    this.energy += amount
  }

  animal.sleep = function (length) {
    console.log(`${this.name} is sleeping.`)
    this.energy += length
  }

  animal.play = function (length) {
    console.log(`${this.name} is playing.`)
    this.energy -= length
  }
  return animal
}
const leo = Animal('Leo', 7)
const snoop = Animal('Snoop', 10)

现在,无论何时我们想要创建一个新 animal(或者更广泛地说,创建一个新的“实例”),我们所要做的就是调用我们的 Animal 函数,并传入参数:name 和 energy 。这很有用,而且非常简单。但是,你能说这种模式的哪些缺点吗?

最大的和我们试图解决的问题与函数里面的三个方法有关 - eat,sleep 和 play。这些方法中的每一种都不仅是动态的,而且它们也是完全通用的。这意味着,我们没有理由像现在一样,在创造新animal的时候重新创建这些方法。我们只是在浪费内存,让每一个新建的对象都比实际需要的还大。

你能想到一个解决方案吗?如果不是在每次创建新动物时重新创建这些方法,我们将它们移动到自己的对象然后我们可以让每个动物引用该对象,该怎么办?我们可以将此模式称为函数实例化与共享方法。

函数实例化与共享方法

const animalMethods = {
  eat(amount) {
    console.log(`${this.name} is eating.`)
    this.energy += amount
  },
  sleep(length) {
    console.log(`${this.name} is sleeping.`)
    this.energy += length
  },
  play(length) {
    console.log(`${this.name} is playing.`)
    this.energy -= length
  }
}

function Animal (name, energy) {
  let animal = {}
  animal.name = name
  animal.energy = energy
  animal.eat = animalMethods.eat
  animal.sleep = animalMethods.sleep
  animal.play = animalMethods.play

  return animal
}

const leo = Animal('Leo', 7)
const snoop = Animal('Snoop', 10)

通过将共享方法移动到它们自己的对象并在 Animal 函数中引用该对象,我们现在已经解决了内存浪费和新对象体积过大的问题。

Object.create

const parent = {
  name: 'Stacey',
  age: 35,
  heritage: 'Irish'
}

const child = Object.create(parent)
child.name = 'Ryan'
child.age = 7

console.log(child.name) // Ryan
console.log(child.age) // 7
console.log(child.heritage) // Irish

因此,在上面的示例中,由于 child 是用 object.create(parent) 创建的,所以每当child 对象上的属性查找失败时,JavaScript 就会将该查找委托给 parent 对象。这意味着即使 child 没有属性 heritage ,当你打印 child.heritage 时,它会从 parent 对象中找到对应 heritage 并打印出来。
现在如何使用 Object.create 来简化之前的 Animal代码?好吧,我们可以使用Object.create 来委托给animalMethods对象,而不是像我们现在一样逐一向 animal 添加所有共享方法。为了B 格一点,就叫做 使用共享方法 和 Object.create 的函数实例化。
**使用共享方法 和 Object.create 的函数实例化
**

const animalMethods = {
  eat(amount) {
    console.log(`${this.name} is eating.`)
    this.energy += amount
  },
  sleep(length) {
    console.log(`${this.name} is sleeping.`)
    this.energy += length
  },
  play(length) {
    console.log(`${this.name} is playing.`)
    this.energy -= length
  }
}

function Animal (name, energy) {
  let animal = Object.create(animalMethods)
  animal.name = name
  animal.energy = energy

  return animal
}

const leo = Animal('Leo', 7)
const snoop = Animal('Snoop', 10)

leo.eat(10)
snoop.play(5)

所以现在当我们调用 leo.eat 时,JavaScript 将在 leo 对象上查找 eat 方法,因为leo 中没有 eat 方法,所以查找将失败,由于 Object.create,它将委托给animalMethods对象,所以会从 animalMethods 对象上找到 eat 方法。
到现在为止还挺好。尽管如此,我们仍然可以做出一些改进。为了跨实例共享方法,必须管理一个单独的对象(animalMethods)似乎有点“傻哈”。我们希望这在语言本身中实现的一个常见特,所以就需要引出下一个属性 - prototype。

那么究竟 JavaScript 中的 prototype 是什么?好吧,简单地说,JavaScript 中的每个函数都有一个引用对象的prototype属性。

function doThing () {}
console.log(doThing.prototype) // {}

**
原型(prototype)实例化
**

function Animal (name, energy) {
  let animal = Object.create(Animal.prototype)
  animal.name = name
  animal.energy = energy

  return animal
}

Animal.prototype.eat = function (amount) {
  console.log(`${this.name} is eating.`)
  this.energy += amount
}

Animal.prototype.sleep = function (length) {
  console.log(`${this.name} is sleeping.`)
  this.energy += length
}

Animal.prototype.play = function (length) {
  console.log(`${this.name} is playing.`)
  this.energy -= length
}

const leo = Animal('Leo', 7)
const snoop = Animal('Snoop', 10)

leo.eat(10)
snoop.play(5)

同样,prototype 只是 JavaScript 中的每个函数都具有的一个属性,正如我们前面看到的,它允许我们跨函数的所有实例共享方法。我们所有的功能仍然是相同的,但是现在我们不必为所有的方法管理一个单独的对象,我们只需要使用 Animal 函数本身内置的另一个对象Animal.prototype。
**更进一步
**
现在我们知道三个点:
1如何创建构造函数。

2如何向构造函数的原型添加方法。

3如何使用 Object.create 将失败的查找委托给函数的原型。

这三个点对于任何编程语言来说都是非常基础的。JavaScript 真的有那么糟糕,以至于没有更简单的方法来完成同样的事情吗?正如你可能已经猜到的那样,现在已经有了,它是通过使用new关键字来实现的。

回顾一下我们的 Animal 构造函数,最重要的两个部分是创建对象并返回它。如果不使用Object.create创建对象,我们将无法在失败的查找上委托函数的原型。如果没有return语句,我们将永远不会返回创建的对象。

function Animal (name, energy) {
  let animal = Object.create(Animal.prototype) // 1 
  animal.name = name
  animal.energy = energy

  return animal   // 2
}

关于 new,有一件很酷的事情——当你使用new关键字调用一个函数时,以下编号为1和2两行代码将隐式地(在底层)为你完成,所创建的对象被称为this。

使用注释来显示底层发生了什么,并假设用new关键字调用了Animal构造函数,可以这样重写它。

function Animal (name, energy) {
  // const this = Object.create(Animal.prototype)

  this.name = name
  this.energy = energy
  // return this
}
const leo = new Animal('Leo', 7)
const snoop = new Animal('Snoop', 10)

正常如下:

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

Animal.prototype.eat = function (amount) {
  console.log(`${this.name} is eating.`)
  this.energy += amount
}

Animal.prototype.sleep = function (length) {
  console.log(`${this.name} is sleeping.`)
  this.energy += length
}

Animal.prototype.play = function (length) {
  console.log(`${this.name} is playing.`)
  this.energy -= length
}

const leo = new Animal('Leo', 7)
const snoop = new Animal('Snoop', 10)

再次说明,之所以这样做,并且这个对象是为我们创建的,是因为我们用new关键字调用了构造函数。如果在调用函数时省略new,则永远不会创建该对象,也不会隐式地返回该对象。我们可以在下面的例子中看到这个问题。

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

const leo = Animal('Leo', 7)
console.log(leo) // undefined

这种模式称为 伪类实例化。

对于那些不熟悉的人,类允许你为对象创建蓝图。然后,每当你创建该类的实例时,你可以访问这个对象中定义的属性和方法。

听起来有点熟?这基本上就是我们对上面的 Animal 构造函数所做的。但是,我们只使用常规的旧 JavaScript 函数来重新创建相同的功能,而不是使用class关键字。当然,它需要一些额外的工作以及了解一些 JavaScript “底层” 发生的事情,但结果是一样的。

这是个好消息。JavaScript 不是一种死语言。TC-39委员会不断改进和补充。这意味着即使JavaScript的初始版本不支持类,也没有理由将它们添加到官方规范中。事实上,这正是TC-39委员会所做的。2015 年,发布了EcmaScript(官方JavaScript规范)6,支持类和class关键字。让我们看看上面的Animal构造函数如何使用新的类语法。

class Animal {
  constructor(name, energy) {
    this.name = name
    this.energy = energy
  }
  eat(amount) {
    console.log(`${this.name} is eating.`)
    this.energy += amount
  }
  sleep(length) {
    console.log(`${this.name} is sleeping.`)
    this.energy += length
  }
  play(length) {
    console.log(`${this.name} is playing.`)
    this.energy -= length
  }
}

const leo = new Animal('Leo', 7)
const snoop = new Animal('Snoop', 10)

这个相对前面的例子,是相对简单明了的。

因此,如果这是创建类的新方法,为什么我们花了这么多时间来复习旧的方式呢?原因是因为新方法(使用class关键字)主要只是我们称之为伪类实例化模式现有方式的“语法糖”。为了完全理解 ES6 类的便捷语法,首先必须理解伪类实例化模式。

数组方法
我们在上面深入讨论了如何在一个类的实例之间共享方法,你应该将这些方法放在类(或函数)原型上。如果我们查看Array类,我们可以看到相同的模式。

const friends = []

以为是代替使用 new Array() 的一个语法糖。

const friendsWithSugar = []
const friendsWithoutSugar = new Array()

你可能从未想过的一件事是,数组的每个实例如何具有所有内置方法 (splice, slice, pop 等)?

正如你现在所知,这是因为这些方法存在于 Array.prototype 上,当你创建新的Array实例时,你使用new关键字在失败的查找中将该委托设置为 Array.prototype。

我们可以打印 Array.prototype 来查看有哪些方法:

console.log(Array.prototype)

/*
  concat: ƒn concat()
  constructor: ƒn Array()
  copyWithin: ƒn copyWithin()
  entries: ƒn entries()
  every: ƒn every()
  fill: ƒn fill()
  filter: ƒn filter()
  find: ƒn find()
  findIndex: ƒn findIndex()
  forEach: ƒn forEach()
  includes: ƒn includes()
  indexOf: ƒn indexOf()
  join: ƒn join()
  keys: ƒn keys()
  lastIndexOf: ƒn lastIndexOf()
  length: 0n
  map: ƒn map()
  pop: ƒn pop()
  push: ƒn push()
  reduce: ƒn reduce()
  reduceRight: ƒn reduceRight()
  reverse: ƒn reverse()
  shift: ƒn shift()
  slice: ƒn slice()
  some: ƒn some()
  sort: ƒn sort()
  splice: ƒn splice()
  toLocaleString: ƒn toLocaleString()
  toString: ƒn toString()
  unshift: ƒn unshift()
  values: ƒn values()
*/

对象也存在完全相同的逻辑。所有的对象将在失败的查找后委托给 Object.prototype,这就是所有对象都有 toString 和 hasOwnProperty 等方法的原因
**静态方法
**
到目前为止,我们已经讨论了为什么以及如何在类的实例之间共享方法。但是,如果我们有一个对类很重要的方法,但是不需要在实例之间共享该方法怎么办?例如,如果我们有一个函数,它接收一系列 Animal 实例,并确定下一步需要喂养哪一个呢?我们这个方法叫做 nextToEat。

function nextToEat (animals) {
  const sortedByLeastEnergy = animals.sort((a,b) => {
    return a.energy - b.energy
  })

  return sortedByLeastEnergy[0].name
}

因为我们不希望在所有实例之间共享 nextToEat,所以在 Animal.prototype上使用nextToEat 是没有意义的。相反,我们可以将其视为辅助方法。
所以如果nextToEat不应该存在于Animal.prototype中,我们应该把它放在哪里?显而易见的答案是我们可以将nextToEat放在与我们的Animal类相同的范围内,然后像我们通常那样在需要时引用它。

class Animal {
  constructor(name, energy) {
    this.name = name
    this.energy = energy
  }
  eat(amount) {
    console.log(`${this.name} is eating.`)
    this.energy += amount
  }
  sleep(length) {
    console.log(`${this.name} is sleeping.`)
    this.energy += length
  }
  play(length) {
    console.log(`${this.name} is playing.`)
    this.energy -= length
  }
}

function nextToEat (animals) {
  const sortedByLeastEnergy = animals.sort((a,b) => {
    return a.energy - b.energy
  })

  return sortedByLeastEnergy[0].name
}

const leo = new Animal('Leo', 7)
const snoop = new Animal('Snoop', 10)

console.log(nextToEat([leo, snoop])) // Leo

这是可行的,但是还有一个更好的方法。
**只要有一个特定于类本身的方法,但不需要在该类的实例之间共享,就可以将其定义为类的静态属性。
**

class Animal {
  constructor(name, energy) {
    this.name = name
    this.energy = energy
  }
  eat(amount) {
    console.log(`${this.name} is eating.`)
    this.energy += amount
  }
  sleep(length) {
    console.log(`${this.name} is sleeping.`)
    this.energy += length
  }
  play(length) {
    console.log(`${this.name} is playing.`)
    this.energy -= length
  }
  static nextToEat(animals) {
    深入理解javascript原型和闭包——继承

深入浅出JavaScript之原型链&继承

深入理解javascript原型和闭包(12)——简介作用域

深入理解javascript原型和闭包(12)——闭包

深入理解JavaScript系列(46):代码复用模式(推荐篇)

深入理解javascript原型和闭包——instanceof