Immediately Invoked Function Expression!
Keep on Learning!
If you liked what you've learned so far, dive in! Subscribe to get access to this tutorial plus video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeOur code is growing up! And to keep going, it's really time to move our RepLogApp
into its own external JavaScript file. For now, let's keep this real simple: inside the web/
directory - which is the public document root for the project - and in assets/
, I'll create a new js/
directory. Then, create a new file: RepLogApp.js
. Copy all of our RepLogApp
object and paste it here:
var RepLogApp = { | |
initialize: function ($wrapper) { | |
this.$wrapper = $wrapper; | |
this.$wrapper.find('.js-delete-rep-log').on( | |
'click', | |
this.handleRepLogDelete.bind(this) | |
); | |
this.$wrapper.find('tbody tr').on( | |
'click', | |
this.handleRowClick.bind(this) | |
); | |
}, | |
updateTotalWeightLifted: function () { | |
var totalWeight = 0; | |
this.$wrapper.find('tbody tr').each(function () { | |
totalWeight += $(this).data('weight'); | |
}); | |
this.$wrapper.find('.js-total-weight').html(totalWeight); | |
}, | |
handleRepLogDelete: function (e) { | |
e.preventDefault(); | |
var $link = $(e.currentTarget); | |
$link.addClass('text-danger'); | |
$link.find('.fa') | |
.removeClass('fa-trash') | |
.addClass('fa-spinner') | |
.addClass('fa-spin'); | |
var deleteUrl = $link.data('url'); | |
var $row = $link.closest('tr'); | |
var self = this; | |
$.ajax({ | |
url: deleteUrl, | |
method: 'DELETE', | |
success: function () { | |
$row.fadeOut('normal', function () { | |
$(this).remove(); | |
self.updateTotalWeightLifted(); | |
}); | |
} | |
}); | |
}, | |
handleRowClick: function () { | |
console.log('row clicked!'); | |
} | |
}; |
Add a good old-fashioned script
tag to bring this in:
// ... lines 1 - 64 | |
{% block javascripts %} | |
{{ parent() }} | |
<script src="{{ asset('assets/js/RepLogApp.js') }}"></script> | |
<script> | |
$(document).ready(function() { | |
var $table = $('.js-rep-log-table'); | |
RepLogApp.initialize($table); | |
}); | |
</script> | |
{% endblock %} |
If you don't normally use Symfony, ignore the asset()
function: it doesn't do anything special.
To make sure we didn't mess anything up, refresh! Let's add a few items to our list. Then, delete one. It works!
Private Functions in JavaScript
One of the advantages of having objects in PHP is the possibility of having private functions and properties. But, that doesn't exist in JavaScript: everything is publicly accessible! That means that anyone could call any of these functions, even if we don't intend for them to be used outside of the object.
That's not the end of the world, but it is a bummer! Fortunately, by being clever, we can create private functions and variables. You just need to think differently than you would in PHP.
Creating a Faux-Private Method
First, create a function at the bottom of this object called _calculateTotalWeight
:
var RepLogApp = { | |
// ... lines 2 - 49 | |
_calculateTotalWeight: function() { | |
// ... lines 51 - 56 | |
} | |
}; |
Its job will be to handle the total weight calculation logic that's currently inside updateTotalWeightLifted
:
var RepLogApp = { | |
// ... lines 2 - 49 | |
_calculateTotalWeight: function() { | |
var totalWeight = 0; | |
this.$wrapper.find('tbody tr').each(function () { | |
totalWeight += $(this).data('weight'); | |
}); | |
return totalWeight; | |
} | |
}; |
We're making this change purely for organization: my intention is that we will only use this method from inside of this object. In other words, ideally, calculateTotalWeight
would be private!
But since everything is public in JavaScript, a common standard is to prefix methods that should be treated as private with an underscore. It's a nice convention, but it doesn't enforce anything. Anybody could still call this from outside of the object.
Back in updateTotalWeightLifted
, call it: this._calculateTotalWeight()
:
var RepLogApp = { | |
// ... lines 2 - 13 | |
updateTotalWeightLifted: function () { | |
this.$wrapper.find('.js-total-weight').html( | |
this._calculateTotalWeight() | |
); | |
}, | |
// ... lines 19 - 57 | |
}; |
Creating a Private Object
So how could we make this truly private? Well, you can't make methods or properties in an object private. BUT, you can make variables private, by taking advantage of variable scope. What I mean is, if I have access to the RepLogApp
object, then I can call any methods on it. But if I didn't have access to this, or some other object, then of course I wouldn't be able to call any methods on it. I know that sounds weird, so let's do it!
At the bottom of this file, create another object called: var Helper = {}
:
// ... lines 1 - 54 | |
var Helper = { | |
// ... lines 56 - 67 | |
}; |
Commonly, we'll organize our code so that each file has just one object, like in PHP. But eventually, this variable won't be public - it's just a helper meant to be used only inside of this file.
I'll even add some documentation: this is private, not meant to be called from outside!
// ... lines 1 - 51 | |
/** | |
* A "private" object | |
*/ | |
var Helper = { | |
// ... lines 56 - 67 | |
}; |
Just like before, give this an initialize, function with a $wrapper
argument. And then say: this.$wrapper = $wrapper
:
// ... lines 1 - 54 | |
var Helper = { | |
initialize: function ($wrapper) { | |
this.$wrapper = $wrapper; | |
}, | |
// ... lines 59 - 67 | |
}; |
Move the calculateTotalWeight()
function into this object, but take off the underscore:
// ... lines 1 - 54 | |
var Helper = { | |
// ... lines 56 - 59 | |
calculateTotalWeight: function() { | |
var totalWeight = 0; | |
this.$wrapper.find('tbody tr').each(function () { | |
totalWeight += $(this).data('weight'); | |
}); | |
return totalWeight; | |
} | |
}; |
Technically, if you have access to the Helper
variable, then you're allowed to call calculateTotalWeight
. Again, that whole _
thing is just a convention.
Back in our original object, let's set this up: call Helper.initialize()
and pass it $wrapper
:
var RepLogApp = { | |
initialize: function ($wrapper) { | |
this.$wrapper = $wrapper; | |
Helper.initialize(this.$wrapper); | |
// ... lines 5 - 13 | |
}, | |
// ... lines 15 - 49 | |
}; | |
// ... lines 51 - 69 |
Down below, call this: Helper.calculateTotalWeight()
:
var RepLogApp = { | |
initialize: function ($wrapper) { | |
this.$wrapper = $wrapper; | |
Helper.initialize(this.$wrapper); | |
// ... lines 5 - 13 | |
}, | |
updateTotalWeightLifted: function () { | |
this.$wrapper.find('.js-total-weight').html( | |
Helper.calculateTotalWeight() | |
); | |
}, | |
// ... lines 20 - 49 | |
}; | |
// ... lines 51 - 69 |
Double-check that everything still works: refresh! It does!
But, this Helper
object is still public. What I mean is, we still have access to it outside of this file. If we try to console.log(Helper)
from our template, it works just fine:
// ... lines 1 - 64 | |
{% block javascripts %} | |
// ... lines 66 - 69 | |
<script> | |
console.log(Helper); | |
// ... lines 72 - 75 | |
</script> | |
{% endblock %} |
What I really want is the ability for me to choose which variables I want to make available to the outside world - like RepLogApp
- and which I don't, like Helper
.
The Self-Executing Function
The way you do that is with - dun dun dun - an immediately invoked function expression. Also known by its friends as a self-executing function. Basically, that means we'll wrap all of our code inside a function... that calls itself. It's weird, but check it out: open parenthesis, function, open parenthesis, close parenthesis,
open curly brace, then indent everything. At the bottom, add the closing curly, closing parenthesis
and then ()
:
(function() { | |
var RepLogApp = { | |
// ... lines 3 - 50 | |
}; | |
// ... lines 52 - 55 | |
var Helper = { | |
// ... lines 57 - 68 | |
}; | |
})(); |
What?
There are two things to check out. First, all we're doing is creating a function: it starts on top, and ends at the bottom with the }
. But by adding the ()
, we are immediately executing that function. We're creating a function and then calling it!
Why on earth would we do this? Because! Variable scope in JavaScript is function based. When you create a variable with var
, it's only accessible from inside of the function where you created it. If you have functions inside of that function, they have access to it too, but ultimately, that function is its home.
Before, when we weren't inside of any function, our two variables effectively became global: we could access them from anywhere. But now that we're inside of a function, the RepLogApp
and Helper
variables are only accessible from inside of this self-executing function.
This means that when we refresh, we get Helper
is not defined. We just made the Helper
variable private!
Unfortunately... we also made our RepLogApp
variable private, which means the code in our template will not work. We still need to somehow make RepLogApp
available publicly, but not Helper
. How? By taking advantage of the magical window
object.
Hey there! This year I've been trying to relearn everything from scratch with the help of you guys. Since most of the projects I've done had to be done in a very tight time frame and with very little budget, leaving me little to no room to "properly" learn things. After finishing the PHP courses, the namespace bonus one and Composer, I've come to go through javascript- I have very little to no javascript experience and while it is "okay" to follow this course, it gets very complicated very quickly. While I can understand core concepts, I feel like I don't quite hit the mark if I finisih this course.
Also making functions to calculate something on the fly *feels* wrong, is this really how it's done in the "real world"? I would've expected that we eventually use functions to get the new weight lifted from the database, to get accurate numbers- I mean these are not wrong, but we're rewriting a logic that could be solved by a simple databse query or am I wrong?
More rambling than question(s), but hey :D