If you don't learn the object well, you won't be able to achieve much in JavaScript.
They are the foundation of almost every aspect of the JavaScript programming language. In this article, you'll learn about the various patterns used to instantiate new objects, and in doing so, you'll gradually gain insight into JavaScript prototypes.
Objects are key/value pairs. The most common way to create an object is to use curly brackets {} and to add attributes and methods to the object using point representation.
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 }
As shown in the above code, in our application, we need to create multiple animals. The next step, of course, is to encapsulate logic inside functions that we can call when we need to create new animals. We call this pattern Functional Instantiation, and we call the function itself a constructor because it is responsible for "constructing" a new object.
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) web Front end 1-3 Advanced year Q Junyang: 731771211 //Free Sharing of Frontier Technologies
"I think it's advanced JavaScript...?"
Now, whenever we want to create a new animal (or, more broadly, a new "instance"), all we have to do is call on our animal function and pass on the animal's name and energy level to it. This is very effective and very simple. But can you find the weakness of this model? The biggest problem we are trying to solve is related to three ways - eating, sleeping and playing. Each of these methods is not only dynamic, but also completely universal. This means there is no reason to recreate these methods, as we did when we created new animals. Can you think of a solution? What if we didn't recreate these methods every time we created a new animal, we moved them to our own object, and then we could have each animal refer to that object? We can call this pattern a function instantiation and sharing method.
Functional instantiation using shared methods
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)
By moving shared methods to their own objects and referring to them in Animal functions, we have now solved the problem of memory waste and excessive animal objects.
Object.create
Let's use Object.create again to improve our example. Simply put, Object.create allows you to create an object. In other words, Object.create allows the creation of an object that can query another object to see if it has the property as long as the property search on that object fails. Let's look at some code.
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
So in the example above, because the child was created using Object.create (parent), JavaScript delegates the search to the parent object whenever a failed attribute is found at the child level. This means that even if the child has no inheritance, parents will do so when you record the child. In this way, you will get the inheritance of your parents (the transfer of attribute values).
Now we use Object.create in our toolkit, how can we use it to simplify the previous Animer code? Well, we can delegate animalMethods objects using Object.create instead of adding all shared methods to animals one by one as we do now.
Sounds smart. Let's call this a function instantiation and sharing method implemented in Object.create.
Function instantiation using shared method 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)
So now when we call leo.eat, JavaScript looks for eat methods on Leo objects. That lookup will fail because Object.create, which delegates to animalMethods objects.
So far so good. Nevertheless, we can still make some improvements. In order to share methods across instances, it seems a bit "hacky" to have to manage a single object (animalMethods). This seems to be a common function that you want to implement in the language itself. That's all you're doing here - prototype.
So what is the prototype of JavaScript? Well, in a nutshell, every function in JavaScript has a prototype attribute that references an object.
Am I right? Test it yourself.
function doThing () {} console.log(doThing.prototype) // {}
What if instead of creating a separate object to manage our methods (for example, we're using animalMethods), we just put each method on the prototype of the Animal function? Then all we have to do is not use Object.create to delegate to animalMethods, which we can use to delegate to Animal.prototype. We call this pattern Prototype Instantiation.
Prototype instantiation
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)
Similarly, prototypes are attributes of each function in JavaScript, and as mentioned above, they allow us to share methods among all instances of functions. All of our functions are still the same, but now we don't have to manage a single object for all methods. We can use another object, Animal.prototype, built into the Animal function itself.
In this regard, we know three things:
1. How to create a constructor.
2. How to add methods to the prototype of constructor.
3. How to use Object.create to delegate failed lookups to prototypes of functions.
These three tasks seem to be the basis of any programming language. Is JavaScript really that bad, and there's no simpler "built-in" way to do the same thing? But it's not. It's done by using the new keyword.
What's the benefit of our slow, methodical approach? Now we can get a better understanding of the new keywords in JavaScript.
Looking back at our Animal constructor, the two most important parts are creating an object and returning it. If we do not use Object.create to create objects, we will not be able to delegate prototypes of functions on failed lookups. If there is no return statement, we will never return the created object.
function Animal (name, energy) { let animal = Object.create(Animal.prototype) animal.name = name animal.energy = energy return animal }
This is a cool thing about new - when you call a function using the new keyword, these two lines are implicitly completed (JavaScript engine), and the object created is called this.
Use annotations to show what happens behind the scenes and assume that the Animal constructor is called using the new keyword, which can be overridden.
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)
Let's see how to write:
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)
The reason for this work and the reason for creating this object for us is that we invoked the constructor using the new keyword. If you do not use new when calling a function, the object will never be created or returned implicitly. We can see this in the following example.
function Animal (name, energy) { this.name = name this.energy = energy } const leo = Animal('Leo', 7) console.log(leo) // undefined
The name of this pattern is Pseudoclassical Instantiation (prototype instantiation).
For those unfamiliar, Class allows you to create blueprints for objects. Then, whenever an instance of this class is created, an object with properties and methods defined in the blueprint is obtained.
Sounds familiar? This is basically what we did with the above Animal constructor. However, instead of using the class keyword, we only use regular old JavaScript functions to recreate the same functionality. Of course, it requires some extra work and some knowledge about how the JavaScript engine works, but the results are the same.
JavaScript is not a dead language. It is constantly improving.
Look at how the Animal constructor above uses the new class syntax.
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)
Clean?
So if this is a new way to create classes, why do we spend so much time digging through the old ways? The reason is that the new approach (using class keywords) is mainly the "grammatical sugar" of the existing approach, which we call the pseudo-classical model. In order to better understand the ES6 class of convenient grammar, we must first understand the pseudo-classical model.
Array method
We discussed in depth above that if you want to share methods between instances of a class, you should place these methods on the class (or function) prototype. If we look at the Array class, we can see the same pattern. Historically, you may have created such arrays
const friends = []
It turns out that creating a new Array class is actually a grammatical sugar.
const friendsWithSugar = [] const friendsWithoutSugar = new Array()
One thing you probably never thought about is where the built-in methods in each instance of an array came from (splice, slice, pop, etc)?
As you now know, this is because these methods exist on Array.prototype, and when you create a new Array instance, use the new keyword to set the delegate to Array.prototype.
We can simply record Array.prototype to see all the methods of arrays.
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() */
Objects also have exactly the same logic. All objects will be delegated to Object.prototype in the failed lookup, which is why all objects have methods such as toString and hasOwnProperty.
Static method
So far, I have explained why and how to share methods between instances of classes. But what if we have a method that is important to lass but does not require cross-instance sharing? For example, if we have a function that accepts an array of Animal instances and determines which one to accept next? We call it nextToEat.
function nextToEat (animals) { const sortedByLeastEnergy = animals.sort((a,b) => { return a.energy - b.energy }) return sortedByLeastEnergy[0].name }
Because we don't want to share it among all instances, using nextToEast on Animal.prototype is meaningless. On the contrary, we can regard it as an auxiliary method. So if nextToEast should not exist in Animal.prototype, where should we put it? The obvious answer, then, is that we can put nextToEat within the same scope as the Animal class and then refer to it as we usually do when needed.
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
Now that's possible, but there's a better way.
As long as there is a method specific to the class itself, but it does not need to be shared between instances of the class, it can be added as a static property of the class.
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 } }
Now, because we have added nextToEat as a static attribute to the class, it exists on the Animal class itself (rather than its prototype) and can be accessed using Animal.nextToEat.
const leo = new Animal('Leo', 7) const snoop = new Animal('Snoop', 10) console.log(Animal.nextToEat([leo, snoop])) // Leo
This article follows a similar pattern. Let's see how to use ES5 to do the same thing. In the example above, we saw how to use the static keyword to place methods directly on the class itself. With ES5, the same pattern is as simple as adding methods to function objects manually.
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
Get the prototype of the object
Whichever mode you use to create an object, you can use the Object.getPrototypeOf method to complete the prototype acquisition of the object.
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
The code above has two important points.
First of all, you will notice that proto is an object with four methods: constructors, meals, sleep and games. That makes sense. We use getPrototypeOf transfer in the instance, and leo gets the prototype of the instance, which is where all our methods exist. This tells us another thing about prototypes that we haven't talked about yet. By default, the prototype object will have a constructor property that points to the original function or the class that created the instance. This also means that because JavaScript defaults to placing constructor properties on prototypes, any instance can access their constructors through instance.constructor.
The second important thing above is Object. getPrototype Of (leo) ==== Animal.prototype. This is also reasonable. The Animal constructor has a prototype attribute that allows us to share methods among all instances, and getPrototype Of allows us to view the prototype of the instance itself.
function Animal (name, energy) { this.name = name this.energy = energy } const leo = new Animal('Leo', 7) console.log(leo.constructor) // Logs the constructor function
Determine whether attributes exist on the prototype
In some cases, you need to know whether attributes exist on the instance itself or on the prototype of the object delegation. We can see this by looping through the leo objects we created. Let's say that the goal is to loop leo and record all its keys and values. Using the for loop, it might look like this.
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]}) }
Most likely, it's like this.
Key: name. Value: Leo Key: energy. Value: 7
But if you run the code, what you see is this
Key: name. Value: Leo Key: energy. Value: 7 Key: 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 }
Why is that? The for loop iterates through all enumerable properties of the object itself and the prototype it delegates. Because by default, any attributes you add to a function prototype are enumerable, we will not only see the name and energy, but also see all the methods on the prototype - eat, sleep, play. To solve this problem, we need to specify that all prototype methods are non-enumerable or we need a console.log-like approach if the attribute is the leo object itself rather than the prototype that leo delegates to fails to find. This is where hasOwn Property can help us.
Why is that? The for loop iterates through all enumerable properties of the object itself and the prototype it delegates. Because by default, any attributes you add to the function prototype are enumerable, we will not only see the name and energy, but also see all the methods on the prototype - eat, sleep, play. To solve this problem, we need to specify that all prototype methods are non-enumerable or we need a console.log-like approach if the attribute is the leo object itself rather than the prototype that leo delegates to fails to find. This is where hasOwn Property can help us.
const leo = new Animal('Leo', 7) for(let key in leo) { if (leo.hasOwnProperty(key)) { console.log(Key: ${key}. Value: ${leo[key]}) } }
What we see now is the attributes of the leo object itself, not the prototype of the leo delegation.
Key: name. Value: Leo Key: energy. Value: 7
If you're still confused about hasOwn Property, here are some code that might clean it up.
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') // true leo.hasOwnProperty('energy') // true leo.hasOwnProperty('eat') // false leo.hasOwnProperty('sleep') // false leo.hasOwnProperty('play') // false
Check whether the object is an instance of the class?
Sometimes you want to know if an object is an instance of a particular class. To do this, you can use the instanceof operator. Use cases are very simple, but if you've never seen them before, the actual grammar is a little strange. Its working principle is as follows.
object instanceof Class
If the object is an instance of Class, the statement above returns true or false. Back to our animal example, we'll have something similar.
function Animal (name, energy) { this.name = name this.energy = energy } function User () {} const leo = new Animal('Leo', 7) leo instanceof Animal // true leo instanceof User // false
instanceof works by checking whether constructor.prototype exists in the object prototype chain. In the example above, Leo instance of Animal is true because Object. getPrototype Of (leo) ==== Animal.prototype. In addition, Leo instance of User is false, because Object. getPrototype Of (leo)! == User.prototype.
Create a new agnostic constructor
Can you find any errors in the following code?
function Animal (name, energy) { this.name = name this.energy = energy } const leo = Animal('Leo', 7)
Even experienced JavaScript developers sometimes stumble over the examples above. Because we're using the pseudo-classical pattern we've learned before, when calling the Animal constructor, we need to make sure it's called using the new keyword. If we don't, we won't create this keyword or return it implicitly.
As a refresher, part of the comments in the following code is what happens when new keywords are used on functions.
function Animal (name, energy) { // const this = Object.create(Animal.prototype) this.name = name this.energy = energy // return this }
This seems to be a very important detail for other developers to remember. Assuming that we are working with other developers, is there any way to ensure that our Animal constructor is always called using the new keyword? It turns out to be achieved by using the instanceof operator we learned earlier.
If the constructor is called using the new keyword, the interior of the constructor body will be an instance of the constructor itself. This is some code.
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 }
Now, instead of just recording warnings to the user of the function, what if we call the function again, but this time without using the new keyword?
function Animal (name, energy) { if (this instanceof Animal === false) { return new Animal(name, energy) } this.name = name this.energy = energy }
Now whether or not Animal is invoked using the new keyword, it still works.
Recreate Object.create
In this article, we rely heavily on Object.create to create objects delegated to constructor prototypes. At this point, you should know how to use Object.create in your code, but one thing you might not think about is how Object.create actually works. In order for you to really understand how Object.create works, we will recreate it ourselves. First, how much do we know about the working principle of Object.create?
It accepts the parameters of an object.
It creates an object that is delegated to a parameter object in a failed lookup.
It returns the newly created object.
Let's start with #1.
Object.create = function (objToDelegateTo) { }
It's simple.
Now # 2 - We need to create an object that will delegate to the parameter object in the failed lookup. This is a bit tricky. To do this, we will use our knowledge of how new keywords and prototypes work in JavaScript. First, within the body of the Object.create implementation, we will create an empty function. Then, we set the prototype of the empty function to be equal to the parameter object. Then, in order to create a new object, we will use the new keyword to call our empty function. If we return to the newly created object, it will also complete #3.
Object.create = function (objToDelegateTo) { function Fn(){} Fn.prototype = objToDelegateTo return new Fn() }
Let's have a look.
When we create a new function Fn in the code above, it has a prototype attribute. When we call it with the new keyword, we know that we will get an object that will delegate the prototype of the function in the failed lookup. If we cover the prototype of a function, we can decide which object to delegate in a failed lookup. So in our example above, we override the prototype of Fn with the object passed in when we call Object.create, which we call objToDelegateTo.
Arrow function
The arrow function does not have its own this keyword. Therefore, the arrow function cannot be a constructor. If you try to call the arrow function using the new keyword, it will throw an error.
const Animal = () => {} const leo = new Animal() // Error: Animal is not a constructor
In addition, in order to prove that the arrow function can not be a constructor, as follows, we see that the arrow function has no prototype attributes.
const Animal = () => {} console.log(Animal.prototype) // undefined