We can create an Object
with {}
:
let chara = {};
Objects can have properties:
chara.x = 0;
chara.y = 0;
> chara
{ x: 0, y: 0 }
Objects can have methods we can invoke:
chara.hi = function () {
console.log('Hello, stranger');
};
> chara.hi();
Hello, stranger
More about this
at https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this
Unless changed, when we call a function as an object's method, this
points to that object instance:
chara.move = function () {
this.x += 10;
};
> chara.move();
> chara
{ x: 10, y: 0, move: [function] }
This can be changed with apply
, call
, or bind
:
let other = {x: 100, y: 100};
> (chara.move.bind(other))();
> other
{ x: 110, y: 100 }
> chara
{ x: 10, y: 0, move: [Function] }
this
will point to the window
(in a browser), or to the global
object (in Node).
When a in a function (that is not a method), if we are in strict mode, this
will be set to undefined
; if we are not in strict mode, this
will point to that window
or global
.
function waka() { console.log(this); }
use strict;
Sometimes, this
can be changed by an API we are using.
For instance, Array.forEach
, which takes a function as a first parameter, by the default will leave this
untouched. It will be either the global object or undefined
(as seen previously).
let printer = {};
printer.prompt = '>>';
printer.array = function (arr) {
arr.forEach(function (x) {
console.log(this.prompt, x);
});
};
> printer.array([1, 2, 3]);
TypeError: Cannot read property 'prompt' of undefined
But it can be changed with bind
:
printer.array = function (arr) {
arr.forEach(function (x) {
console.log(this.prompt, x);
}.bind(this));
};
> printer.array([1, 2, 3]);
>> 1
>> 2
>> 3
Or, better yet, with the second argument of forEach
, that is able to set the this
for us!
printer.array = function (arr) {
arr.forEach(function (x) {
console.log(this.prompt, x);
}, this);
};
> printer.array([1, 2, 3]);
>> 1
>> 2
>> 3
Note: keep in mind that bind
returns a new function. Be careful when using it inside a loop, or while creating indefinite loops (such as requestAnimationFrame
), because you might end up consuming a lot of memory.
Events handlers in the browser also modify this
. For instance, giving this object:
let dog = {name: 'Conan' };
dog.hi = function () {
document.body.innerHTML += '<p>Hello, ' + this.name + '.</p>';
};
When subscribing to click
, if we do nothing, this
will be the Element
that has triggered the event:
document.querySelector('button').addEventListener('click', dog.hi);
But we can manually set the this
that we want:
document.querySelector('button').addEventListener('click', dog.hi.bind(dog));
See online at https://jsfiddle.net/jaox55ck/.
{}
is a shortcut for this:
new Object();
A good way of code reusing, if we need to have several instances that behave in the same way, is to use a function as a constructor. We can get this with the new
operator:
function Character(name) {
this.name = name;
}
> new Character('Binky');
Character { name: 'Binky' }
In a constructor, this
will point to the object that has been instantiated, and it will be returned automatically implictly.
We could add methods to that object in the constructor…
function Character(name) {
this.name = name;
this.hi = function () { console.log("Hi, I'm", this.name); }
}
And if we create two instances, it would work as expected:
> let wizard = new Character('Gandalf');
> let warrior = new Character('Aragorn');
> warrior.hi();
Hi, I'm Aragorn
However, we would be wasting memory because two different functions have been created for each instance:
> wizard.hi === warrior.hi
false
To solve this, JavaScript introduces a mechanism known as prototypes. All objects have a prototype, which is itself an object, and is stored in the property __proto__
:
> warrior.__proto__
Character {}
The key is that prototypes are shared between objects created with the same constructor.
> warrior.__proto__ === wizard.__proto__
true
So if we add methods to that prototype, it will be shared by all objects and only one function would be created. This not only saves memory, but allow us to edit the prototype later on and have those changes "propagated" to all instances.
We can modify the prototype of a constructor accessing its prototype
property:
Character.prototype.alive = true;
> warrior.alive;
true
> wizard.alive;
true
So this is great to define the methods we want all the instances of the same constructor to share:
Character.prototype.die = function () {
this.alive = false;
};
> warrior.die();
> warrior.alive
false
Note that the new ES6 class declaration (See https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Classes) is just syntactic sugar over this prototype model. This is why it is important to understand what a prototype is.
Inheritance is another re-using mechanism. It allows an instance to extend another one, sharing most of the code but providing custom behaviour.
For example, we could need a new type Enemy
that extends Character
.
To share the methods and properties declared in the prototype, we need to follow these steps:
- Make a copy of it and establish that copy as the prototype of the new constructor.
- Override the
constructor
property of the copy and set it to the new constructor. - If we need it, we can call the parent's constructor with
apply
orcall
:
function Enemy(name, level) {
// call the parent constructor
Character.call(this, name);
this.level = level;
}
Enemy.prototype = Object.create(Character.prototype);
Enemy.prototype.constructor = Enemy;