跟我学习javascript的prototype,JavaScript原型深入浅出

时间: 2019-05-14阅读: 205标签: 原型

一、深入理解prototype, getPrototypeOf和_ proto _

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

prototype,getPropertyOf和 _ proto _ 是三个用来访问prototype的方法。它们的命名方式很类似因此很容易带来困惑。

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

C.prototype: 一般用来为一个类型建立它的原型继承对象。比如C.prototype = xxx,这样就会让使用new C()得到的对象的原型对象为xxx。当然使用obj.prototype也能够得到obj的原型对象。 getPropertyOf: Object.getPropertyOf是ES5中用来得到obj对象的原型对象的标准方法。 _ proto _: obj._ proto _是一个非标准的用来得到obj对象的原型对象的方法。

let animal = {}animal.name = 'Leo'animal.energy = 10animal.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 User { this.name = name; this.passwordHash = passwordHash; } User.prototype.toString = function() { return "[User " + this.name + "]"; }; User.prototype.checkPassword = function { return hash === this.passwordHash; }; var u = new User("sfalken", "0ef33ae791068ec64b502d6cb0191387"); 

函数的实例化

User函数拥有一个默认的prototype属性,该属性的值是一个空对象。在以上的例子中,向prototype对象添加了两个方法,分别是toString和checkPassword。当调用User构造函数得到一个新的对象u时,它的原型对象会被自动赋值到User.prototype对象。即u.prototype === User.prototype会返回true。

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)

User函数,User.prototype,对象u之间的关系可以表示如下:

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

上图中的箭头表示的是继承关系。当访问u对象的某些属性时,会首先尝试读取u对象上的属性,如果u对象上并没有这个属性,就会查找其原型对象。

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

比如当调用u.checkPassword()时,因为checkPassword定义在其原型对象上,所以在u对象上找不到该属性,就在u的原型上查找,查找顺序是u-> u.prototype。

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

前面提到过,getPrototypeOf方法是ES5中用来得到某个对象的原型对象的标准方法。因此:

函数实例化与共享方法

Object.getPrototypeOf === User.prototype; // true 
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)

在一些环境中,同时提供了一个非标准的_ proto _ 属性用来得到某个对象的原型对象。当环境不提供ES5的标准方法getPrototypeOf方法时,可以暂时使用该属性作为替代。可以使用下面的代码测试环境中是否支持_ proto _:

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

u.__ proto __ === User.prototype; // true 

Object.create

所以在JavaScript中,类的概念是由构造函数和用于实例间共享方法的原型对象共同完成的。构造函数中负责构造每个对象特有的属性,比如上述例子中的name和password属性。而其原型对象中负责存放所有对象共有的属性,比如上述例子中的checkPassword和toString方法。就像下面这张图表示的那样:

让我们再次使用Object.create改进我们的例子。 简单地说,Object.create 允许你创建一个对象,该对象将在失败的查找中委托给另一个对象。 换句话说,Object.create允许你创建一个对象,只要该对象上的属性查找失败,它就可以查询另一个对象以查看该另一个对象是否具有该属性。 我们来看一些代码:

二、获取对象优先使用Object.getPrototypeOf,而不是_ proto _

const parent = { name: 'Stacey', age: 35, heritage: 'Irish'}const child = Object.create(parent)child.name = 'Ryan'child.age = 7console.log(child.name) // Ryanconsole.log(child.age) // 7console.log(child.heritage) // Irish

在ES5中引入了Object.getPrototypeOf作为获取对象原型对象的标准API。但是在很多执行环境中,也提供了一个特殊的_ proto _属性来达到同样的目的。

因此,在上面的示例中,由于child是用object.create(parent)创建的,所以每当child对象上的属性查找失败时,JavaScript 就会将该查找委托给parent对象。这意味着即使child没有属性 heritage ,当你打印child.heritage时,它会从parent对象中找到对应heritage并打印出来。

因为并不是所有的环境都提供了这个_ proto _属性,且每个环境的实现方式各不相同,因此一些结果可能不一致,例如,对于拥有null原型的对象:

现在如何使用Object.create来简化之前的Animal代码? 好吧,我们可以使用Object.create来委托给animalMethods对象,而不是像我们现在一样逐一向animal添加所有共享方法。 为了B 格一点,就叫做使用共享方法 和 Object.create 的函数实例化

// 在某些环境中 var empty = Object.create; // object with no prototype "_proto _" in empty; // false  // 在某些环境中 var empty = Object.create; // object with no prototype "_proto_" in empty; // true  

使用共享方法 和 Object.create 的函数实例化

所以当环境中支持Object.getPrototypeOf方法时,优先使用它。即使不支持,也可以为了实现一个:

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)
if (typeof Object.getPrototypeOf === "undefined") { Object.getPrototypeOf = function { var t = typeof obj; if (!obj || (t !== "object" && t !== "function")) { throw new TypeError; } return obj._proto_; }; } 

所以现在当我们调用leo.eat时,JavaScript 将在leo对象上查找eat方法,因为leo 中没有eat方法,所以查找将失败,由于Object.create,它将委托给animalMethods对象,所以会从animalMethods对象上找到eat方法。

上述代码首先会对当前环境进行检查,如果已经支持了Object.getPrototypeOf,就不会再重复定义。

到现在为止还挺好。尽管如此,我们仍然可以做出一些改进。为了跨实例共享方法,必须管理一个单独的对象(animalMethods)似乎有点“傻哈”。我们希望这在语言本身中实现的一个常见特,所以就需要引出下一个属性 -prototype。

三、绝不要修改_ proto _

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

和Object.getPrototypeOf相比,_ proto _的特殊之处还体现在它能够修改一个对象的原型继承链。因为它是一个属性,除了执行获取它的操作外,还能够对它进行设置。

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

但是,绝不要修改_ proto _。原因如下:

如果不是创建一个单独的对象来管理我们的方法(如上例中animalMethods),我们只是将每个方法放在 Animal 函数的prototype上,该怎么办? 然后我们所要做的就是不使用Object.create委托给animalMethods,我们可以用它来委托Animal.prototype。 我们将这种模式称为原型实例化。

首先,最显而易见的原因就是便携性。因为不是所有的JavaScript执行环境都支持这一属性,所以使用了_ proto _ 之后,代码就不能在那些不支持_ proto _的环境中运行了。 其次,是性能上的考虑。现在的JavaScript引擎的实现都会针对对象属性的存取作出大量的优化,因为这些操作是最常用的。当修改了对象的_ proto _后,就相当于修改了对象的整个继承结构,这样做回导致很多优化都不再可用。 最后,最重要的原因是需要保证程序的可靠性。因为改变_ proto _属性后,对象的原型继承链也许会被完全地改变。当程序中有其他代码依赖于原来的继承链时,就会出现不可意料的错误。通常而言,原型继承链需要保持稳定。

原型(prototype)实例化

当需要为一个新创建的对象赋予一个原型对象时,可以使用ES5提供的Object.create方法。对于未实现ES5标准的环境,可以给出来一个不依赖于_ proto _的Object.create方法的实现。

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)

四、解决 _ proto _兼容问题, 让构造函数不再依赖new关键字

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

在将function当做构造函数使用时,需要确保该函数是通过new关键字进行调用的。

更进一步

function User { this.name = name; this.passwordHash = passwordHash; } 

现在我们知道三个点:

如果在调用上述构造函数时,忘记了使用new关键字,那么:

如何创建构造函数。如何向构造函数的原型添加方法。如何使用Object.create将失败的查找委托给函数的原型。

var u = User("baravelli", "d8b74df393528d51cd19980ae0aa028e"); u; // undefined this.name; // "baravelli" this.passwordHash; // "d8b74df393528d51cd19980ae0aa028e" 

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

可以发现得到的u是undefined,而this.name以及this.passwordHash则被赋了值。但是这里的this指向的则是全局对象。

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

如果将构造函数声明为依赖于strict模式:

function Animal (name, energy) { let animal = Object.create(Animal.prototype) // 1 animal.name = name animal.energy = energy return animal // 2}
function User { "use strict"; this.name = name; this.passwordHash = passwordHash; } var u = User("baravelli", "d8b74df393528d51cd19980ae0aa028e"); // error: this is undefined 

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

那么在忘记使用new关键字的时候,在调用this.name= name的时候会抛出TypeError错误。这是因为在strict模式下,this的默认指向会被设置为undefined而不是全局对象。

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

那么,是否有种方法能够保证在调用一个函数时,无论使用了new关键字与否,该函数都能够被当做构造函数呢?下面的代码是一种实现方式,使用了instanceof操作:

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 User { if (!) { return new User; } this.name = name; this.passwordHash = passwordHash; } var x = User("baravelli", "d8b74df393528d51cd19980ae0aa028e"); var y = new User("baravelli", "d8b74df393528d51cd19980ae0aa028e"); x instanceof User; // true y instanceof User; // true 

正常如下:

以上的if代码块就是用来处理没有使用new进行调用的情况的。当没有使用new时,this的指向并不是一个User的实例,而在使用了new关键字时,this的指向是一个User类型的实例。

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)

另一个更加适合在ES5环境中使用的实现方式如下:

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

function User { var self = this instanceof User ? this : Object.create; self.name = name; self.passwordHash = passwordHash; return self; } 
function Animal (name, energy) { this.name = name this.energy = energy}const leo = Animal('Leo', 7)console.log(leo) // undefined

Object.create方法是ES5提供的方法,它能够接受一个对象作为新创建对象的prototype。那么在非ES5环境中,就需要首先实现一个Object.create方法:

这种模式称为伪类实例化

if (typeof Object.create === "undefined") { Object.create = function { function C() { } C.prototype = prototype; return new C(); }; } 

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

实际上,Object.create方法还有接受第二个参数的版本,第二个参数表示的是在新创建对象上赋予的一系列属性。

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

当上述的函数确实使用了new进行调用时,也能够正确地得到返回的新建对象。这得益于构造器覆盖模式(Constructor Override Pattern)。该模式的含义是:使用了new关键字的表达式的返回值能够被一个显式的return覆盖。正如以上代码中使用了return self来显式定义了返回值。

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

当然,以上的工作在某些情况下也不是必要的。但是,当一个函数是需要被当做构造函数进行调用时,必须对它进行说明,使用文档是一种方式,将函数的命名使用首字母大写的方式也是一种方式(基于JavaScript语言的一些约定俗成)。

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)

以上就是针对javascript的prototype,getPrototypeOf和__proto__进行的深入学习,希望对大家的学习有所帮助。

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

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

至此,我们已经介绍了 JavaScript 原型的基本原理。这篇文章的其余部分将致力于理解与之相关的其他好话题。在另一篇文章中,我们将研究如何利用这些基本原理,并使用它们来理解JavaScript中的继承是如何工作的。

数组方法

我们在上面深入讨论了如何在一个类的实例之间共享方法,你应该将这些方法放在类(或函数)原型上。 如果我们查看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) { const sortedByLeastEnergy = animals.sort((a,b) = { return a.energy - b.energy }) return sortedByLeastEnergy[0].name }}

现在,因为我们在类上添加了nextToEat作为静态属性,所以它存在于Animal类本身(而不是它的原型)上,并且可以使用Animal.nextToEat进行调用 。

const leo = new Animal('Leo', 7)const snoop = new Animal('Snoop', 10)console.log(Animal.nextToEat([leo, snoop])) // Leo

因为我们在这篇文章中都遵循了类似的模式,让我们来看看如何使用 ES5 完成同样的事情。 在上面的例子中,我们看到了如何使用static关键字将方法直接放在类本身上。 使用 ES5,同样的模式就像手动将方法添加到函数对象一样简单。

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}Animal.nextToEat = function (nextToEat) { 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(Animal.nextToEat([leo, snoop])) // Leo

获取对象的原型

无论您使用哪种模式创建对象,都可以使用Object.getPrototypeOf方法完成获取该对象的原型。

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 proto = Object.getPrototypeOf(leo)console.log(proto )// {constructor: ƒ, eat: ƒ, sleep: ƒ, play: ƒ}proto === Animal.prototype // true

上面的代码有两个重要的要点。

首先,你将注意到proto是一个具有 4 个方法的对象,constructor、eat、sleep和play。这是有意义的。我们使用getPrototypeOf传递实例,leo取回实例原型,这是我们所有方法的所在。

这也告诉了我们关于prototype的另一件事,我们还没有讨论过。默认情况下,prototype对象将具有一个constructor属性,该属性指向初始函数或创建实例的类。这也意味着因为 JavaScript 默认在原型上放置构造函数属性,所以任何实例都可以通过。

第二个重要的点是:Object.getPrototypeOf(leo) === Animal.prototype。 这也是有道理的。Animal构造函数有一个prototype属性,我们可以在所有实例之间共享方法,getPrototypeOf允许我们查看实例本身的原型。

function Animal (name, energy) { this.name = name this.energy = energy}const leo = new Animal('Leo', 7)console.log(leo.constructor) // Logs the constructor function

为了配合我们之前使用Object.create所讨论的内容,其工作原理是因为任何Animal实例都会在失败的查找中委托给Animal.prototype。 因此,当你尝试访问leo.constructor时,leo没有constructor属性,因此它会将该查找委托给Animal.prototype,而Animal.prototype确实具有构造函数属性。

你之前可能看过使用__proto__用于获取实例的原型,这是过去的遗物。 相反,如上所述使用 Object.getPrototypeOf(instance)判断原型上是否包含某个属性

在某些情况下,你需要知道属性是否存在于实例本身上,还是存在于对象委托的原型上。 我们可以通过循环打印我们创建的leo对象来看到这一点。 使用for in循环方式如下:

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)for(let key in leo) { console.log(`Key: ${key}. Value: ${leo[key]}`)}

我所期望的打印结果可能如下:

Key: name. Value: LeoKey: energy. Value: 7

然而,如果你运行代码,看到的是这样的-

Key: name. Value: LeoKey: energy. Value: 7Key: eat. Value: function (amount) { console.log(`${this.name} is eating.`) this.energy += amount}Key: sleep. Value: function (length) { console.log(`${this.name} is sleeping.`) this.energy += length}Key: play. Value: function (length) { console.log(`${this.name} is playing.`) this.energy -= length}

这是为什么? 对于for in循环来说,循环将遍历对象本身以及它所委托的原型的所有可枚举属性。 因为默认情况下,你添加到函数原型的任何属性都是可枚举的,我们不仅会看到name和energy,还会看到原型上的所有方法 -eat,sleep,play。

要解决这个问题,我们需要指定所有原型方法都是不可枚举的,或者只打印属性位于leo对象本身而不是leo委托给失败查找的原型。 这是hasOwnProperty可以帮助我们的地方。

...const leo = new Animal('Leo', 7)for(let key in leo) { if (leo.hasOwnProperty(key)) { console.log(`Key: ${key}. Value: ${leo[key]}`) }}

现在我们看到的只是leo对象本身的属性,而不是leo委托的原型。

Key: name. Value: LeoKey: energy. Value: 7

果你仍然对hasOwnProperty感到困惑,这里有一些代码可以帮你更好的理清它。

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)leo.hasOwnProperty('name') // trueleo.hasOwnProperty('energy') // trueleo.hasOwnProperty('eat') // falseleo.hasOwnProperty('sleep') // falseleo.hasOwnProperty('play') // false

检查对象是否是类的实例

有时你想知道对象是否是特定类的实例。 为此,你可以使用instanceof运算符。 用例非常简单,但如果你以前从未见过它,实际的语法有点奇怪。 它的工作方式如下

object instanceof Class

如果object是Class的实例,则上面的语句将返回true,否则返回false。 回到我们的Animal示例,我们有类似的东西:

function Animal (name, energy) { this.name = name this.energy = energy}function User () {}const leo = new Animal('Leo', 7)leo instanceof Animal // trueleo instanceof User // false

instanceof的工作方式是检查对象原型链中是否存在constructor.prototype。 在上面的例子中,leo instanceof Animal为 true,因为Object.getPrototypeOf(leo) === Animal.prototype。 另外,leo instanceof User为 false,因为Object.getPrototypeOf(leo) !== User.prototype。

创建新的不可知的构造函数

你能找出下面代码中的错误吗

function Animal (name, energy) { this.name = name this.energy = energy}const leo = Animal('Leo', 7)

即使是经验丰富的 JavaScript 开发人员有时也会被上面的例子绊倒。因为我们使用的是前面学过的伪类实例模式,所以在调用Animal构造函数时,需要确保使用new关键字调用它。如果我们不这样做,那么this关键字就不会被创建,它也不会隐式地返回。

作为复习,注释掉的行是在函数上使用new关键字时背后发生的事情。

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

让其他开发人员记住,这似乎是一个非常重要的细节。 假设我们正在与其他开发人员合作,我们是否有办法确保始终使用new关键字调用我们的Animal构造函数? 事实证明,可以通过使用我们之前学到的instanceof运算符来实现的。

如果使用new关键字调用构造函数,那么构造函数体的内部this将是构造函数本身的实例。

function Aniam (name, energy) { if (this instanceof Animal === false) { console.warn('Forgot to call Animal with the new keyword') } this.name = name this.energy = energy}

现在,如果我们重新调用函数,但是这次使用new的关键字,而不是仅仅向函数的调用者打印一个警告呢?

function Animal (name, energy) { if (this instanceof Animal === false) { return new Animal(name, energy) } this.name = name this.energy = energy}

现在,不管是否使用new关键字调用Animal,它仍然可以正常工作。

重写 Object.create

在这篇文章中,我们非常依赖于Object.create来创建委托给构造函数原型的对象。 此时,你应该知道如何在代码中使用Object.create,但你可能没有想到的一件事是Object.create实际上是如何工作的。 为了让你真正了解Object.create的工作原理,我们将自己重新创建它。 首先,我们对Object.create的工作原理了解多少?

它接受一个对象的参数。它创建一个对象,在查找失败时委托给参数对象它返回新创建的对象。

Object.create = function (objToDelegateTo) {}

现在,我们需要创建一个对象,该对象将在失败的查找中委托给参数对象。 这个有点棘手。 为此,我们将使用new关键字相关的知识。

首先,在Object.create主体内部创建一个空函数。 然后,将空函数的prototype设置为等于传入参数对象。 然后,返回使用new关键字调用我们的空函数。

Object.create = function (objToDelegateTo) { function Fn(){} Fn.prototype = objToDelegateTo return new Fn()}

当我们在上面的代码中创建一个新函数Fn时,它带有一个prototype属性。 当我们使用new关键字调用它时,我们知道我们将得到的是一个将在失败的查找中委托给函数原型的对象。

如果我们覆盖函数的原型,那么我们可以决定在失败的查找中委托哪个对象。 所以在上面的例子中,我们用调用Object.create时传入的对象覆盖Fn的原型,我们称之为objToDelegateTo。

请注意,我们只支持 Object.create 的单个参数。 官方实现还支持第二个可选参数,该参数允许你向创建的对象添加更多属性。箭头函数

箭头函数没有自己的this关键字。 因此,箭头函数不能是构造函数,如果你尝试使用new关键字调用箭头函数,它将引发错误。

const Animal = () = {}const leo = new Animal() // Error: Animal is not a constructor

另外,因为我们在上面说明了伪类实例模式不能与箭头函数一起使用,所以箭头函数也没有原型属性。

const Animal = () = {}console.log(Animal.prototype) // undefined

原文:JavaScript 原型的深入指南作者:前端小智

本文由澳门威斯尼人平台登录发布于Web前端,转载请注明出处:跟我学习javascript的prototype,JavaScript原型深入浅出

相关阅读