p> Não se pode ir muito longe em JavaScript sem lidar com objectos. Eles são fundamentais para quase todos os aspectos da linguagem de programação JavaScript. De facto, aprender a criar objectos é provavelmente uma das primeiras coisas que se estudou quando se estava a começar. Dito isto, para aprender mais eficazmente sobre protótipos em JavaScript, vamos canalizar o nosso desenvolvedor Jr. interno e voltar ao básico.
Objectos são pares chave/valor. A forma mais comum de criar um objecto é com chaves de caracóis {}
e adiciona-se propriedades e métodos a um objecto usando notação de pontos.
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}
Simples. Agora, as probabilidades estão na nossa aplicação e precisaremos de criar mais do que um animal. Naturalmente, o passo seguinte para isso seria encapsular essa lógica dentro de uma função que podemos invocar sempre que precisemos de criar um novo animal. Chamaremos a este padrão Functional Instantiation
e chamaremos à própria função uma “função construtora” uma vez que é responsável por “construir” um novo objecto.
Functional 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)
"I thought this was an Advanced JavaScript course...?" - Your brain
É. Chegaremos lá.
Agora sempre que quisermos criar um novo animal (ou, de uma forma mais geral, uma nova “instância”), tudo o que temos de fazer é invocar a nossa função Animal
, passando-lhe a função name
e energy
nível. Isto funciona muito bem e é incrivelmente simples. No entanto, é possível detectar quaisquer fraquezas com este padrão? O maior e o que vamos tentar resolver tem a ver com os três métodos – eat
sleep
, e play
. Cada um desses métodos não só são dinâmicos, como também são completamente genéricos. O que isso significa é que não há razão para recriar esses métodos como estamos actualmente a fazer sempre que criamos um novo animal. Estamos apenas a desperdiçar memória e a tornar cada objecto animal maior do que precisa de ser. Consegue pensar numa solução? E se em vez de recriarmos esses métodos cada vez que criamos um novo animal, os deslocarmos para o seu próprio objecto, então podemos ter cada animal de referência a esse objecto? Podemos chamar a este padrão Functional Instantiation with Shared Methods
, verboso mas descritivo.
Instanciação funcional com métodos partilhados
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)
movendo os métodos partilhados para o seu próprio objecto e referenciando esse objecto dentro da nossa função Animal
, resolvemos agora o problema do desperdício de memória e dos objectos animais demasiado grandes.
Object.create
Vamos melhorar o nosso exemplo mais uma vez usando Object.create
. Simplificando, Object.create permite criar um objecto que será delegado a outro objecto em pesquisas falhadas. Pondo de outra forma, Object.create permite-lhe criar um objecto e sempre que houver uma pesquisa de propriedade falhada nesse objecto, pode consultar outro objecto para ver se esse outro objecto tem a propriedade. Isso foi um monte de palavras. Vamos ver algum código.
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
Então no exemplo acima, porque child
foi criado com Object.create(parent)
, sempre que houver uma pesquisa de propriedade falhada em child
, JavaScript delegará essa pesquisa até ao objecto parent
. O que isso significa é que embora child
não tenha um heritage
propriedade, parent
fá-lo quando regista child.heritage
obterá o parent
‘s heritage que era Irish
.
Agora com Object.create
no nosso barracão de ferramentas, como podemos utilizá-lo para simplificar o nosso Animal
código de antes? Bem, em vez de adicionar todos os métodos partilhados ao animal um a um, como estamos a fazer agora, podemos usar Object.create para delegar no animalMethods
objecto em vez disso. Para parecer realmente inteligente, vamos chamar a este Functional Instantiation with Shared Methods and Object.create
🙃
Functional Instantiation with Shared Methods and 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)
📈 Então agora quando chamamos leo.eat
, o JavaScript procurará o método eat
no objecto leo
. Essa pesquisa falhará, então, por causa do Object.create, delegará no animalMethods
objecto que é onde encontrará eat
.
p> até agora, tudo bem. No entanto, ainda há algumas melhorias que podemos fazer. Parece apenas um pouco “hacky” ter de gerir um objecto separado (animalMethods
) a fim de partilhar métodos através de instâncias. Isso parece ser uma característica comum que gostaria de implementar na própria linguagem. Acontece que é e é toda a razão pela qual está aqui –prototype
.
Então o que é exactamente prototype
em JavaScript? Bem, pondo simplesmente, cada função em JavaScript tem uma propriedade prototype
que faz referência a um objecto. Anticlimático, certo? Teste-o por si próprio.
function doThing () {}console.log(doThing.prototype) // {}
E se em vez de criarmos um objecto separado para gerir os nossos métodos (como estamos a fazer com animalMethods
), apenas colocamos cada um desses métodos no Animal
protótipo da função? Então tudo o que teríamos de fazer seria em vez de usar o Object.create para delegar a animalMethods
, poderíamos usá-lo para delegar a Animal.prototype
. Vamos chamar a este padrão Prototypal Instantiation
.
Instanciação Protótipo
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)
👏👏👏 Esperemos que tenha tido um grande momento “aha”. Mais uma vez, prototype
é apenas uma propriedade que todas as funções em JavaScript têm e, como vimos acima, permite-nos partilhar métodos em todas as instâncias de uma função. Toda a nossa funcionalidade é ainda a mesma mas agora, em vez de termos de gerir um objecto separado para todos os métodos, podemos simplesmente utilizar outro objecto que vem incorporado no Animal
própria função, Animal.prototype
.
Let’s. Vai. Deeper.
Neste ponto sabemos três coisas:
- Como criar uma função construtora.
- Como adicionar métodos ao protótipo da função construtora.
- Como usar Object.create para delegar pesquisas falhadas ao protótipo da função.
Tres tarefas parecem bastante fundacionais para qualquer linguagem de programação. O JavaScript é realmente assim tão mau que não existe uma forma mais fácil, “incorporada” de realizar a mesma coisa? Como provavelmente pode adivinhar neste ponto existe, e é usando o new
palavra-chave.
O que é bom na abordagem lenta e metódica que fizemos para chegar aqui é que agora terá uma compreensão profunda do que o new
palavra-chave em JavaScript está a fazer debaixo do capô.
Voltando ao nosso Animal
construtor, as duas partes mais importantes eram a criação do objecto e a sua devolução. Sem criar o objecto com Object.create
, não seríamos capazes de delegar no protótipo da função nas pesquisas falhadas. Sem a declaração return
, nunca recuperaríamos o objecto criado.
function Animal (name, energy) { let animal = Object.create(Animal.prototype) animal.name = name animal.energy = energy return animal}
Aqui está a coisa fixe sobre new
– quando se invoca uma função usando a palavra-chave new
, essas duas linhas são feitas implicitamente (“debaixo da capota”) e o objecto que é criado chama-se this
.
Usando comentários para mostrar o que acontece debaixo da capota e assumindo o Animal
construtor é chamado com a palavra-chave new
, pode ser reescrita como isto.
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)
e sem os comentários “debaixo do capô”
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)
Apanhar a razão disto funciona e que o this
objecto é criado para nós é porque chamamos a função construtor com o new
palavra-chave. Se deixar de fora new
quando invoca a função, esse this
objecto nunca é criado nem é implicitamente devolvido. Podemos ver o problema com isto no exemplo abaixo.
function Animal (name, energy) { this.name = name this.energy = energy}const leo = Animal('Leo', 7)console.log(leo) // undefined
O nome para este padrão é Pseudoclassical Instantiation
.
Se o JavaScript não for a sua primeira linguagem de programação, poderá estar a ficar um pouco inquieto.
“WTF este gajo acabou de recriar uma versão mais porcaria de uma Classe” – You
Para os não familiarizados, uma Classe permite-lhe criar um plano para um objecto. Depois, sempre que cria uma instância dessa Classe, obtém um objecto com as propriedades e métodos definidos no projecto.
Som familiar? Foi basicamente o que fizemos com a nossa Animal
função construtora acima. Contudo, em vez de usarmos a palavra-chave class
, utilizámos apenas uma antiga função JavaScript regular para recriar a mesma funcionalidade. É verdade que foi preciso um pouco de trabalho extra, bem como algum conhecimento sobre o que acontece “debaixo da capa” do JavaScript, mas os resultados são os mesmos.
Aqui estão as boas notícias. O JavaScript não é uma linguagem morta. Está constantemente a ser melhorado e adicionado pelo comité TC-39. O que isso significa é que, embora a versão inicial do JavaScript não suportasse as aulas, não há razão para que não possam ser acrescentadas à especificação oficial. Na verdade, foi exactamente isso que o comité TC-39 fez. Em 2015, EcmaScript (a especificação oficial JavaScript) 6 foi lançado com suporte para Classes e a class
palavra-chave. Vejamos como o nosso Animal
função construtora acima ficaria com a nova sintaxe de classe.
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)
P>Pretty clean, right?
Então, se esta é a nova forma de criar classes, porque é que passámos tanto tempo a percorrer o caminho antigo? A razão para isso é porque a nova maneira (com o class
palavra-chave) é principalmente apenas “açúcar sintáctico” sobre a maneira existente a que chamámos padrão pseudo-clássico. Para compreender completamente a sintaxe de conveniência das classes ES6, é preciso primeiro compreender o padrão pseudo-clássico.
Neste ponto cobrimos os fundamentos do protótipo do JavaScript. O resto deste post será dedicado à compreensão de outros tópicos “bons de saber” relacionados com o mesmo. Noutro post, veremos como podemos pegar nestes fundamentos e usá-los para compreender como funciona a herança em JavaScript.
Array Methods
Falámos em profundidade acima sobre como se quiser partilhar métodos através de instâncias de uma classe, deve colar esses métodos no protótipo da classe’ (ou função). Podemos ver este mesmo padrão demonstrado se olharmos para o Array
classe. Historicamente, é provável que tenha criado as suas arrays desta forma
const friends =
Sai apenas açúcar ao criar um new
instância do Array
classe.
const friendsWithSugar = const friendsWithoutSugar = new Array()
Uma coisa em que talvez nunca tenha pensado é como é que cada instância de um array tem todos esses métodos incorporados (splice
slice
pop
, etc.)?
bem como sabe agora, é porque esses métodos vivem em Array.prototype
e quando cria uma nova instância de Array
, usa a palavra-chave new
que estabelece essa delegação a Array.prototype
em pesquisas falhadas.
Podemos ver todos os métodos da matriz através de simples registo 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()*/
A mesma lógica existe exactamente também para os Objectos. Todos os objectos serão delegados a Object.prototype
em pesquisas falhadas, razão pela qual todos os objectos têm métodos como toString
e hasOwnProperty
.
Métodos estáticos
Up até este ponto, cobrimos o porquê e como partilhar métodos entre instâncias de uma Classe. Contudo, e se tivéssemos um método que fosse importante para a Aula, mas que não precisasse de ser partilhado entre instâncias? Por exemplo, e se tivéssemos uma função que incluía um conjunto de Animal
instâncias e determinasse qual delas precisava de ser alimentada a seguir? Vamos chamar-lhe nextToEat
.
function nextToEat (animals) { const sortedByLeastEnergy = animals.sort((a,b) => { return a.energy - b.energy }) return sortedByLeastEnergy.name}
Não faz sentido ter nextToEat
viver em Animal.prototype
uma vez que não queremos partilhá-lo entre todas as instâncias. Em vez disso, podemos pensar nisto como um método mais de ajuda. Portanto, se nextToEat
não deveria viver em Animal.prototype
, onde devemos colocá-lo? Bem, a resposta óbvia é que podíamos simplesmente colar nextToEat
no mesmo âmbito que o nosso Animal
classe e depois referenciá-la quando precisamos dela como normalmente faríamos.
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.name}const leo = new Animal('Leo', 7)const snoop = new Animal('Snoop', 10)console.log(nextToEat()) // Leo
Agora isto funciona, mas há uma maneira melhor.
Quando se tem um método que é específico de uma classe em si, mas não precisa de ser partilhado entre instâncias dessa classe, pode-se adicioná-lo como um
static
propriedade da classe.
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.name }}
Now, porque adicionámos nextToEat
como uma static
propriedade da classe, vive no Animal
classe em si (não o seu protótipo) e pode ser acedido usando Animal.nextToEat
.
const leo = new Animal('Leo', 7)const snoop = new Animal('Snoop', 10)console.log(Animal.nextToEat()) // Leo
Porque seguimos um padrão semelhante ao longo deste post, vejamos como realizaríamos esta mesma coisa usando ES5. No exemplo acima vimos como a utilização da palavra-chave static
colocaria o método directamente na própria classe. Com ES5, este mesmo padrão é tão simples como adicionar manualmente o método ao objecto de função.
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 (animals) { const sortedByLeastEnergy = animals.sort((a,b) => { return a.energy - b.energy }) return sortedByLeastEnergy.name}const leo = new Animal('Leo', 7)const snoop = new Animal('Snoop', 10)console.log(Animal.nextToEat()) // Leo
Conseguir o protótipo de um objecto
Independentemente do padrão utilizado para criar um objecto, a obtenção do protótipo desse objecto pode ser conseguida utilizando o método 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 prototype = Object.getPrototypeOf(leo)console.log(prototype)// {constructor: ƒ, eat: ƒ, sleep: ƒ, play: ƒ}prototype === Animal.prototype // true
Existem dois importantes takeaways do código acima.
P>Primeiro, irá notar que proto
é um objecto com 4 métodos, constructor
eat
sleep
, e play
. Isso faz sentido. Utilizámos getPrototypeOf
passando na instância, leo
recuperando o protótipo dessas instâncias, que é onde todos os nossos métodos estão a viver. Isto diz-nos mais uma coisa sobre prototype
também que ainda não falámos. Por defeito, o objecto prototype
terá uma propriedade constructor
que aponta para a função original ou para a classe a partir da qual a instância foi criada. O que isto também significa é que porque o JavaScript coloca uma propriedade constructor
no protótipo por defeito, qualquer instância poderá aceder ao seu construtor através de instance.constructor
.
A segunda importante retirada de cima é que Object.getPrototypeOf(leo) === Animal.prototype
. Isso também faz sentido. O Animal
função construtora tem uma propriedade protótipo onde podemos partilhar métodos em todas as instâncias e getPrototypeOf
permite-nos ver o protótipo da própria instância.
function Animal (name, energy) { this.name = name this.energy = energy}const leo = new Animal('Leo', 7)console.log(leo.constructor) // Logs the constructor function
para ligar o que falámos anteriormente com Object.create
, a razão porque isto funciona é porque quaisquer instâncias de Animal
vão delegar em Animal.prototype
em consultas falhadas. Assim, quando tentar aceder a leo.constructor
leo
não tem uma propriedade constructor
pelo que irá delegar essa consulta a Animal.prototype
que de facto tem uma propriedade constructor
. Se este parágrafo não fazia sentido, voltar atrás e ler sobre Object.create
acima.
p>Talvez já tenha visto __proto__ usado antes para obter um protótipo de instâncias. Isso é uma relíquia do passado. Em vez disso, use Object.getPrototypeOf(instance) como vimos acima.
Determinar se uma propriedade vive no protótipo
Há certos casos em que é necessário saber se uma propriedade vive na própria instância ou se vive no protótipo a que o objecto delega. Podemos ver isto em acção, passando por cima do nosso leo
objecto que temos vindo a criar. Digamos que o objectivo era o loop over leo
e registar todas as suas chaves e valores. Usando um for in
loop, que se pareceria provavelmente com isto.
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}`)}
O que esperaria ver? Muito provavelmente, era algo parecido com isto –
Key: name. Value: LeoKey: energy. Value: 7
No entanto, o que viu se correu o código foi isto –
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}
Porquê? Bem, um for in
loop vai fazer loop sobre todas as propriedades enumeráveis tanto no próprio objecto como no protótipo que delega. Porque por defeito qualquer propriedade que se acrescenta ao protótipo da função é enumerável, vemos não só name
e energy
, mas também vemos todos os métodos no protótipo – eat
sleep
, e play
. Para corrigir isto, ou precisamos de especificar que todos os métodos do protótipo são inumeráveis ou precisamos de uma forma de apenas consolar.log se a propriedade estiver no leo
objecto em si e não o protótipo que leo
delegados em consultas falhadas. É aqui que hasOwnProperty
nos pode ajudar.
hasOwnProperty
é uma propriedade em cada objecto que devolve um booleano indicando se o objecto tem a propriedade especificada como sua própria propriedade e não no protótipo a que o objecto delega em caso de falha de pesquisa. É exactamente o que precisamos. Agora com este novo conhecimento, podemos modificar o nosso código para tirar partido de hasOwnProperty
dentro do nosso for in
loop.
...const leo = new Animal('Leo', 7)for(let key in leo) { if (leo.hasOwnProperty(key)) { console.log(`Key: ${key}. Value: ${leo}`) }}
E agora o que vemos são apenas as propriedades que estão no leo
objecto em si e não no protótipo leo
delegados também.
Key: name. Value: LeoKey: energy. Value: 7
Se ainda estiver um pouco confuso sobre hasOwnProperty
, aqui está algum código que pode clarificá-lo.
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
Verifica se um objecto é uma instância de uma Classe
Por vezes quer saber se um objecto é uma instância de uma classe específica. Para o fazer, pode usar o operador instanceof
. O caso de utilização é bastante directo, mas a sintaxe real é um pouco estranha se nunca a tiver visto antes. Funciona assim
object instanceof Class
A afirmação acima retornará verdadeira se object
for uma instância de Class
e falsa se não for. Voltando ao nosso Animal
exemplo, teríamos algo assim.
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
A forma como instanceof
funciona é verificar a presença de constructor.prototype
na cadeia de protótipos do objecto. No exemplo acima, leo instanceof Animal
true
porque Object.getPrototypeOf(leo) === Animal.prototype
. Além disso, leo instanceof User
false
porque Object.getPrototypeOf(leo) !== User.prototype
.
Criar novas funções de construtor agnóstico
P>Pode detectar o erro no código abaixo?
function Animal (name, energy) { this.name = name this.energy = energy}const leo = Animal('Leo', 7)
Even experientes programadores de JavaScript irão por vezes tropeçar no exemplo acima. Porque estamos a utilizar o pseudoclassical pattern
de que tomámos conhecimento anteriormente, quando a Animal
função construtora é invocada, temos de nos certificar de que a invocamos com a palavra-chave new
. Se não o fizermos, então o this
palavra-chave não será criada e também não será implicitamente devolvida.
Como uma actualização, as linhas comentadas são o que acontece nos bastidores quando se usa o new
palavra-chave sobre uma função.
function Animal (name, energy) { // const this = Object.create(Animal.prototype) this.name = name this.energy = energy // return this}
Isto parece demasiado importante de um detalhe para deixar para os outros programadores se lembrarem. Assumindo que estamos a trabalhar em equipa com outros programadores, há alguma forma de garantir que o nosso Animal
construtor é sempre invocado com a palavra-chave new
? Acontece que existe e é usando o instanceof
operador de que tomámos conhecimento anteriormente.
Se o construtor foi chamado com a palavra-chave new
, então this
dentro do corpo do construtor será uma instanceof
a própria função do construtor. Isso foi um monte de palavras grandes. Aqui está algum código.
function Animal (name, energy) { if (this instanceof Animal === false) { console.warn('Forgot to call Animal with the new keyword') } this.name = name this.energy = energy}
agora em vez de apenas registar um aviso ao consumidor da função, e se voltarmos a invocar a função mas com o new
palavra-chave desta vez?
function Animal (name, energy) { if (this instanceof Animal === false) { return new Animal(name, energy) } this.name = name this.energy = energy}
Agora independentemente de se Animal
for invocado com a new
palavra-chave, ainda assim funcionará correctamente.
Re-creating Object.create
Através deste post, confiamos fortemente em Object.create
a fim de criar objectos que delegam no protótipo da função construtora. Neste ponto, deve saber como usar Object.create
dentro do seu código, mas uma coisa que talvez não tenha pensado é como Object.create
funciona realmente debaixo do capô. Para que possa realmente compreender como Object.create
funciona, vamos recriá-lo nós próprios. Primeiro, o que sabemos sobre como Object.create
funciona?
- Cria um argumento que é um objecto.
- Cria um objecto que delega no argumento objecto em pesquisas falhadas.
- Devolve o novo objecto criado.
/ol>
Vamos começar com #1.
Object.create = function (objToDelegateTo) {}
Simples o suficiente.
Agora #2 – precisamos de criar um objecto que delegue no objecto argumento em pesquisas falhadas. Este é um pouco mais complicado. Para tal, usaremos o nosso conhecimento de como o new
palavra-chave e protótipos funcionam em JavaScript. Primeiro, dentro do corpo do nosso Object.create
implementação, vamos criar uma função vazia. Em seguida, vamos definir o protótipo dessa função vazia como sendo igual ao objecto de argumento. Depois, para criar um novo objecto, invocaremos a nossa função vazia usando a palavra-chave new
. Se devolvermos esse objecto recém-criado, isso terminará também #3.
Object.create = function (objToDelegateTo) { function Fn(){} Fn.prototype = objToDelegateTo return new Fn()}
Wild. Vamos percorrê-lo.
Quando criamos uma nova função, Fn
no código acima, vem com uma propriedade prototype
. Quando o invocamos com a palavra-chave new
, sabemos que o que vamos receber de volta é um objecto que será delegado no protótipo da função em pesquisas falhadas. Se anularmos o protótipo da função, então podemos decidir qual o objecto a delegar em caso de pesquisa falhada. Assim, no nosso exemplo acima, anulamos Fn
‘s prototype with the object that was passed in when Object.create
‘s invoked which we call objToDelegateTo
.
Nota que só estamos a apoiar um único argumento ao Object.create. A implementação oficial também suporta um segundo argumento opcional que lhe permite adicionar mais propriedades ao objecto.criado.
Funções de seta
Funções de seta não têm as suas próprias this
palavra-chave. Como resultado, as funções de seta não podem ser funções construtoras e se tentar invocar uma função de seta com a new
palavra-chave, irá lançar um erro.
const Animal = () => {}const leo = new Animal() // Error: Animal is not a constructor
Também, porque demonstrámos acima que o padrão pseudo-clássico não pode ser usado com funções de seta, as funções de seta também não têm um prototype
propriedade.
const Animal = () => {}console.log(Animal.prototype) // undefined