TL;DR: In the first part of the Glossary of Modern JS Concepts series, we learned about functional, reactive, and functional reactive programming. In Part 2, we'll gain an understanding of concepts like scope, closures, tree shaking, components, and more, as well as JavaScript application topics such as data flowand change detection.
Introduction
Modern JavaScript has experienced massive proliferation over recent years and shows no signs of slowing. Numerous concepts appearing in JS blogs and documentation are still unfamiliar to many front-end developers. In this post series, we'll learn intermediate and advanced concepts in the current front-end programming landscape and explore how they apply to modern JavaScript, JS frameworks, and JS applications.
Concepts
In this article, we'll address concepts that are crucial to understanding modern JavaScript and JS applications, including scope and closures, data flow, change detection, components, compilation, and more.
You can jump straight into each concept here, or continue reading to learn about them in order.
Scope (Global, Local, Lexical) and Closures
One-Way Data Flow and Two-Way Data Binding
Change Detection in JS Frameworks: Dirty Checking, Accessors, Virtual DOM
Web Components
Smart and Dumb Components
JIT (Just-In-Time) Compilation
AOT (Ahead-of-Time) Compilation
Tree Shaking
Scope (Global, Local, Lexical) and Closures
"Explain closures" is an infamous JavaScript technical interview question. The truth is that plenty of skilled JS developers have difficulty explaining closures, even if they conceptually understand (and even use) them. Let's back up and talk about the concepts necessary to explain a closure.
Scope
In order to grasp closures, we need to understand scope first. Scope is simply the context of our code: where variables and functions are accessible.
The following example demonstrates two scopes, global scope and local scope:
// Global scopevar globalVar = 'Hello, ';console.log(localVar); // Uncaught ReferenceError: localVar is not definedsomeFunction() { // Local scope var localVar = 'World!'; console.log(globalVar + localVar); // 'Hello, World!'}
Everything has access to the global scope. If we open an empty .js
file and type var globalVar
, this variable is accessible to anything else we'll create. If we executed the file in a browser, globalVar
's function scope would be window
.
Note: If we declare a new variable without the var
keyword, it will be placed in the global scope no matter where it is in the code. You may have encountered this before (perhaps accidentally).
The someFunction
function creates its own local scope. It also inherits access to the global scope. We can freely use globalVar
inside someFunction
. However, the global scope does not have access to nested contexts, such as someFunction
's local scope. If we try to log localVar
from the global scope, we will receive an error because localVar
is not defined in the global scope.
In a nutshell, nested functions have their own scope. Functions declared inside another function also have access to their parent functions' scopes. This is called the scope chain.
Lexical scope (or static scope) refers to the fact that every nested function can access the functions that contain it.
Consider this example:
// Lexical scope and scope chainvar a = 1;function outerFunc() { var b = 2; console.log(a + b); function middleFunc() { var c = 3; console.log(a + b + c); function innerFunc() { var d = 4; console.log(a + b + c + d); } innerFunc(); // logs 10 (1 + 2 + 3 + 4) } middleFunc(); // logs 6 (1 + 2 + 3)}outerFunc(); // logs 3 (1 + 2)
This code is available to run at this JSFiddle: JS Scope (open your browser's console to see results). innerFunc
is the innermost function. It is declared inside middleFunc
, which is in turn declared in outerFunc
.
The innerFunc
function can access variables declared in all of its parent scopes. Its scope chain allows access to:
a
from the global scope,
b
from outerFunc
,
c
from middleFunc
, and
d
from innerFunc
's own local scope.
This only works down the nested functions, not up. For instance, the locally scoped variable d
is declared in innerFunc
and is not accessible to middleFunc
, outerFunc
, or the global scope.
Closures
In the first part of the Glossary of Modern JavaScript Concepts, we learned about higher-order functions and functions as first-class objects. If this doesn't sound familiar, take a moment to review the section on Higher-order Functions.
Now let's revisit the following higher-order function example we saw in Part 1:
// Higher-order functionfunction whenMeetingJohn() { return function() { alert('Hi!'); }}var atLunchToday = whenMeetingJohn();atLunchToday(); // alerts "Hi!"
This is a function that returns another function. Let's update this example to add an argument (salutation
) and a greeting
variable to whenMeetingJohn
's local scope. We'll also name the previously anonymous returned function alertGreeting
so we can refer to it more easily:
// Closuresfunction whenMeetingJohn(salutation) { var greeting = salutation + ', John!'; function alertGreeting() { alert(greeting); } return alertGreeting;}var atLunchToday = whenMeetingJohn('Hi');atLunchToday(); // alerts "Hi, John!"whenMeetingJohn('Whassup')(); // alerts "Whassup, John!"
This code is available to run at this JSFiddle: JS Closures.
A closure is formed when a function (alertGreeting
) declared inside an outer function (whenMeetingJohn
) references variables from the outer function's local scope (such as the greeting
variable).
The term "closure" refers to the function and the lexical environment (any local variables that were in scope when the closure was created) in which that function was declared.
When we execute atLunchToday()
, we receive an alert with the argument we passed during assignment ('Hi'
in this case) and the greeting
variable that was accessible in alertGreeting
's lexical environment.
Note: We can also call the returned function (alertGreeting
) without assigning it. Doing so looks like this: whenMeetingJohn('Whassup')()
.
Hopefully you can see the value in closures when looking at this simple example. We can greet John with several different salutations. Each time, we create a closure with access to the particular salutation data in scope at the time of creation.
Another common example demonstrating closures uses a simple addition expression:
// Closuresfunction addCreator(x) { return function(y) { alert(x + y); }}var add1 = addCreator(1);var add5 = addCreator(5);add1(2); // alerts 3add5(2); // alerts 7
This code can be run at this JSFiddle: JS Closures - Adder.
Both add1
and add5
are closures with different lexical environments storing different values for the x
argument. These values are protected by the fact that they're "enclosed" in the lexical environment of each closure. We could use the addCreator(x)
factory function to create as many add_
functions as we needed.
Scope and Closures Takeaways
Scope is something that many JS developers learn early on, but may not have had to explain in words or with specific examples. Understanding scope is vital to writing good JavaScript.
Note: There is more to scope than we covered here. There are some great resources available to achieve greater understanding, especially with regard to the this
keyword. See below for more links.
Closures associate data with a function utilizing the lexical environment the function was declared in.
To learn more about scope and closures (and this
), check out the following resources:
Everything you wanted to know about JavaScript scope
Explaining JavaScript Scope and Closures
Scope in JavaScript
What is lexical scope?
MDN: Closures
What is "this"?
MDN: this
Understand JavaScript's "this" With Clarity, and Master It
JavaScript: How does the "this" keyword work?
One-Way Data Flow and Two-Way Data Binding
With the proliferation of JavaScript frameworks and Single Page Applications (SPAs), it's important for JS developers to understand concepts like data flow / binding, and how the tools we're using manage this.
One-Way Data Flow
An application or framework with one-way data flow uses the model as the single source of truth. React is a widely recognized example of one-way data flow (or one-way data binding). Messages are sent from the UI in the form of events to signal the model to update.
Take a look at the following React example:
// One-way data flow with Reactclass OneWay extends React.Component { constructor() { super(); this.handleChange = this.handleChange.bind(this); // set initial this.state.text to an empty string this.state = { text: '' }; } handleChange(e) { // get new input value from the event and update state this.setState({ text: e.target.value }); } render() { return (
Text: {this.state.text}
This code is available to run at JSFiddle: React One-Way Data Flow.
We can see that the state
object model is established in the constructor
function. The initial value of this.state.text
is an empty string. In our render()
function, we add an onChange
handler to our
element. We use this handler to setState()
, signalling the state
object model to update the text
property with the new value of the input field.
Data is only flowing in one direction: from the model down. The UI input does not have direct access to the model. If we want to update state in response to changes from the UI, the input must send a message carrying the payload. The only way the UI can influence the model is through this event and the setState()
method. The UI will never automagically update the model.
Note: In order to reflect changes from the model to the UI, React creates a new virtual DOM and diffs the old virtual DOM with the updated virtual DOM. Only the changes are then rendered in the real DOM. We'll talk more about this in the section on change detection.
Two-Way Data Binding
In two-way data binding, the data flows in both directions. This means that the JS can update the model and the UI can do so as well. A common example of two-way data binding is with AngularJS.
Note: In this article, AngularJS refers specifically to version 1.x of the framework while Angular refers to versions 2.x and up, as per the Branding Guidelines for Angular.
Let's implement the same example from above, but with AngularJS two-way data binding:
// AngularJS two-way data binding// script.js(function() { angular .module('myApp', []) .controller('MyCtrl', function($scope) { // set initial $scope.text to an empty string $scope.text = ''; // watch $scope.text for changes $scope.$watch('text', function(newVal, oldVal) { console.log(Old value: ${oldVal}. New value: ${newVal}
); }); });}());
Text: {{text}}
This code is available to run at Plunker: AngularJS Two-Way Binding.
In our controller, we set up the $scope.text
model. In our template, we associate this model with the
using ng-model="text"
. When we change the input value in the UI, the model will also be updated in the controller. We can see this in the $watch()
.
Note: Using $watch()
in a controller is debateable practice. We've done it here for example purposes. In your own AngularJS apps, take into consideration that there are alternatives to using $watch()
in controllers (such as events), and if you do use $watch()
, always deregister your watches $onDestroy
.
This is two-way binding in AngularJS. As you can see, we didn't set up any events or handlers to explicitly signal the controller that the model was updated in the UI. The text
data binding in the template automatically uses a watcher to display changes to the model. We can also $watch()
the model. Watching should generally be done in services or directive link
functions, not in controllers.
Note: AngularJS uses what's called the digest cycle (dirty checking) to compare a value with the previous value. You can read more about dirty checking in AngularJS in the section on change detection.
Aside: Two-Way Data Binding in Angular
But wait! Angular (v2+) has the "banana-in-a-box" [(ngModel)]
, right? On the surface, this may look like persistence of automagical two-way data binding. However, that is not the case. Angular's two-way binding [()]
syntax simply shortcuts property and event binding in a template, and the ngModel
directivesupplies an ngModelChange
event for you. To learn more about this, check out this article on two-way binding in Angular.
The following are functionally equivalent and demonstrate the ngModel
directive:
// ngModel directive: two-way binding syntax
{{text}}
// ngModel property and event binding{{text}}
The Angular docs on two-way binding cover this syntax thoroughly.
Data Flow and Binding Takeaways
Many modern JavaScript frameworks and libraries utilize unidirectional data flow (React, Angular, Inferno, Redux, etc.). Why? One-way data flow encourages clean architecture with regard to how data moves through an application. Application state is also easier to manage, updates are more predictable, and performance can be better as well.
Although automagical two-way data binding was one of AngularJS's most popular demos back in 2009, Angular has left it behind. Some Angular developers lamented this at first, but ultimately, many found that performance gains and greater control outweighed automagic.
As we saw in the React example above, it's important to remember that one-way data flow does not mean that it's difficult to update the store from the UI. It only means that such updates are done deliberately, with specific instruction. It's less magical, but much more manageable.
Note: Generally when developers mention "implementing two-way data binding" in frameworks with one-way data flow (such as React), they are referring to the steps necessary to have UI changes notify the state that it should be updated. They are not looking for a way to implement automagical two-way binding.
To learn more about one-way data flow and two-way data binding, check out the following resources:
Video: Introducing One-Way Data Flow
Diagrams comparing one-way and two-way data binding
Why does React emphasize unidirectional data flow and Flux architecture?
Data binding code in 9 JavaScript frameworks (from 2015, but still worth a look)
Two-way Data Binding in Angular (v2+)
AngularJS Docs - Data Binding
Thinking in React
Change Detection in JS Frameworks: Dirty Checking, Accessors, Virtual DOM
Change detection is important for any dynamic JavaScript Single Page Application (SPA). When the user updates something, the app must have a way to detect and react to that change appropriately. Some kind of change detection is therefore vital to SPA frameworks.
At a fairly high level, let's explore a few methods of change detection used in popular JavaScript frameworks today.
Dirty Checking
Although Angular was released, AngularJS still accounts for multitudes of apps in production or development right now. AngularJS uses what's known as the digest cycle to detect changes in an application. Under the hood, the digest cycle is dirty checking. What does this mean?
Dirty checking refers to a deep comparison that is run on all models in the view to check for a changed value. AngularJS's digest cycle adds a watcher for every property we add to the $scope
and bind in the UI. Another watcher is added when we want to watch values for changes using $scope.$watch()
.
"AngularJS remembers the value and compares it to a previous value. This is basic dirty-checking. If there is a change in value, then it fires the change event." —Miško Hevery, creator of AngularJS and Angular
The digest cycle is a loop. AngularJS runs through its list of watchers and checks to see if any of the watched $scope
variables have changed (aka, are "dirty"). If a variable has not changed, it moves on to the next watched variable. If it finds one that is dirty, it remembers its new value and re-enters the loop. When no new changes are detected in the entire watch list, the DOM is updated.
The major advantages of dirty checking are that it's simple and predictable: there is no extending of objects and there are no APIs involved. However, it's also inefficient. Whenever anything changes, the the digest cycle is triggered. Therefore, it's important that care is taken when creating watchers in AngularJS. Every time a $scope
property is bound to the UI, a watcher is added. Every time a $watch()
is implemented, another watcher is added. Many directives also add watchers, and so do scope variables, filters, and repeaters.
Though dirty checking is sufficiently fast in a simple app, we can easily see how this can get out of hand in a complex implementation. This has led to articles such as 11 Tips to Improve AngularJS Performance: 1. Minimize/Avoid Watchersand Speeding up AngularJS's $digest loop.
Note: Angular (v2+) no longer uses dirty checking.
Accessors
Ember and Backbone use data accessors (getters and setters) for change detection. Ember objects inherit from Ember's APIs and have get()
and set()
methods that must be used to update models with data binding. This enables the binding between the UI and the data model and Ember then knows exactly what changed. In turn, only the modified data triggers change events to update the app.
Note: In Backbone, this is done with Backbone models with get()
and set()
methods.
This method is straightforward and enforces that the application author be very deliberate regarding their data bindings. However, on the flipside of the same coin, it can occasionally lead to confusion because Ember.Object
s are only used when data binding to templates. If there is no UI data binding, updates do not use Ember.Object
s. This mixed approach can result in the developer scratching their head when things aren't updating because of a forgotten setter or getter.
Virtual DOM
Virtual DOM is used by React (and Inferno.js) to implement change detection. React doesn't specifically detect each change. Instead, the virtual DOM is used to diff the previous state of the UI and the new state when a change occurs. React is notified of such changes by the use of the setState()
method, which triggers the render()
method to perform a diff.
Virtual DOM (occasionally known as V-DOM) is a JavaScript data model that represents the real DOM tree. When a virtual DOM is generated, nothing is rendered to the browser. The old model is compared to the new model and once React determines which parts of the virtual DOM have changed, only those parts are patched in the real DOM.
Change Detection Takeaways
There are many ways that JavaScript frameworks manage change detection, including more that weren't covered here. They each have strengths and weaknesses, but the modern trend is toward more deliberate and less automagical methods, many utilizing observer pattern under the hood.
To learn more about change detection in JS frameworks, check out the following resources:
Change and its Detection in JavaScript Frameworks
Change Detection Overview
Dirty checking in AngularJS
The Digest Loop and Apply
Ember.Object Class
Accessors vs. Dirty Checking in JavaScript Frameworks
React.JS internals: Virtual DOM
The Difference Between Virtual DOM and DOM
React: Reconciliation
virtual-dom
VueJS: How Changes are Tracked
Polymer 2.0: Observers and Computed Properties
Understanding Angular 2 Change Detection
Angular Change Detection Explained
Web Components
Web components are encapsulated, reusable widgets based on web platform APIs. They are composed of four standards:
Custom Elements
HTML Templates
Shadow DOM
HTML Imports
Web components allow us to architect and import custom elements that automatically associate JS behavior with templates and can utilize shadow DOM to provide CSS scoping and DOM encapsulation.
Web components consist of a set of web platform APIs. There are libraries (such as Polymer) and polyfills (such as webcomponents.js) to bridge the gap between current browser support and future web API support.
Let's say we want to create a simple web component (my-component
) that shows some static text. We'd like to use HTML attributes to have the component change its text color and log something to the console. To display the
custom element in our website or app, we might import and use it like so:
...
To create the my-component This is a custom element!
web component utilizing shadow DOM, our my-component.html
might look something like this:
This code is available to run at Plunker: Web components.
The
defines the element's CSS styling and HTML markup. Then, to take advantage of shadow DOM and JS functionality, we add a