Someone told me that I don't know JS, but ...
In all seriousness, these are the personal notes I took while learning JavaScript. I'll continue to occasionally add to them, and PRs are accepted if you want to add to them, too!
- JavaScript
- Primitives
- Objects basics
- Falsiness
- Expressions
- Statements
- Equality
- Mutability
- Objects
- Conversion
- Global object
- Variable hoisting
- Functions
- Objects
this
- Cloning / copying objects
- Factory functions
- Constructors
- Object literals and constructors
- Prototype
- Prototypes & "Inheritance"
- Prototypal inheritance
- Methods can be overridden locally
- Prototype accessor/shortcut
- Constructors + prototype chain
- Constructors and prototype internals
- Frozen non-writeable methods & properties
- OLOO vs Pseudoclassical
- Functions Pt. II (contexts)
- Loosing context
- Function closures
- Garbage collection
- DOM
- XML HTTP Requests
- Window
- Tricks
- Debugging
- Preventing errors
- jQuery
- Basics
- Creating elements
- jQuery vs. vanilla JS
- Traversal
- Other search methods
closest()
vs.parents()
index()
each()
methodeq()
- Form management
- Other traversal methods
- DOM display values
- DOM manipulation
- jQuery patterns
- Animations
- Stoping animations
- Events
- Functions
- Ajax
- Binding ajax method calls as event handlers - extending objects
- Good jquery code
- Troubleshoting
- HTML Data Attributes
- Handlebars
- Localstorage
- ES6
- Linting
- Node
- number
- string
- boolean
- null
- undefined
Primitives are immutable
a = 'hello';
a.toUpperCase(); // "HELLO"
a; // 'hello'
You can compare strings, which is interesting
'a' < 'b'; // true
'Ant' > 'Falcon'; // false
Character | Output |
---|---|
\n | New line |
\t | Tab |
\r | Carriage return |
\v | Vertical tab |
\b | Backspace |
Example of strange concatenation behavior.
JavaScript strings automatically concatenate Integers and Strings together without throwing an exception.
> 2 + "two"
< "2two"
Number => String
This also leads to the simplest way to convert a number
> 12 + ""
< "12"
Although it may be more explicit to do this
Number(12)
Other mathemetical operands will not work in this way
"123" * 3 //369
"8" - "1" // 7
String => Number
`Number('12')`
> 12
Or you can use a shorthand
+'12'
> 12
Formatting long strings
Concatenation is also useful for formatting multiline strings in JavaScript
var lipsum = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do " +
"eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut "
This method uses escaping newline characters
var lipsum = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do \
eiusmod tempor incididunt ut labore et dolore magna aliqua."
ES6 - Concatenation
In ES6 you can use interpolation to combine variables and strings.
let name = "world"
console.log(`Hello ${name}`)
Use whole numbers (1 cent rather than .01 dollars) - division with floats is less precise.
0.1 + 0.2; // returns 0.30000000000000004, not 0.3!
Infinity
: when you need a number that's greater than all other numbers-Infinity
: when you need a number that's less than all other numbersNaN
: typically a math function failed
Modulo
Often the %
represents a modulo operator, but in JS it's actually [remainder] operator(https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Arithmetic_Operators#Remainder_()). To demonstrate the differences between the two, let's look at how 10 % -3
would be treated in each case:
var a = 10;
var b = -3;
var remainder = a - (Math.trunc(a/b) * b); // 10 - -4 * -3 => 2
var modulo = a - (Math.floor(a/b) * b); // 10 - -3 * -3 => -2
The difference here is that a remainder is using trunc
and a modulo would use floor
.
> Math.trunc(-3.333)
-3
> Math.floor(-3.333);
-4
Removing a value
splice(index_of_first_value_to_remove, index_following_last_value_to_remove)
var arr = [0,1,2];
arr.splice(0,1) // 0
arr // [1,2]
Getting an object's keys
keys = Object.keys(obj)
Use bracket notation with variables and integers
The key in the []
must be a string value. If a variable is placed in the brackets, it will use the value of the variable (converted to a string if necessary) as the key to look up.
var obj = { a: 1, "b": 2, 1: 3, $1: 4 }
obj.a // 1
obj[a] // Reference Error
obj["a"] // 1
obj.b // 1
obj[b] // Reference Error
obj["b"] // 1
obj[1] // 3
In the examples above, Referrence Error
happens because a
and b
are not instantiated variables.
An integer can serve as an object key, but you cannot use it with the object dot notation. From the MDN docs
To use dot notation, the property must be a valid JavaScript identifier, i.e. a sequence of alphanumerical characters, also including the underscore (
_
) and dollar sign ($
), that cannot start with a number. For example,object.$1
is valid, whileobject.1
is not.
// continued
obj.1 // Error
obj.$1 // 4
Access object property with a string
firstKey = "name";
var lead = { name : "Me", email: "me@domain.com" };
lead[firstKey] // "Me" (lead.firstKey would not work - it would like for firstKey as a key)
lead.name // "Me"
Iterating through an object's keys
for (var property in obj) { ... }
Or if you need to return an array or make access the current index in a callback
Object.keys(obj).map(function(key, index) {
obj[key] = ... ;
});
Falsy values in JavaScript
0
NaN
- Return value from a math operation involvingundefined
""
undefined
null
false
NaN
vs. undefined
vs. null
NaN
- never equals any other value, including itself, so to test for NaN use the function isNaN.""
undefine
d - a variable (or property or array item) hasn’t yet been initialized to a value.null
- "no object"
Some ways to test this out are below:
!!(NaN) // false
Boolean(0); // false
Expression is any valid code that resolves to a value.
Incrementing & Decrementing shorthand
b = a++; // equivalent of "b = a; a++;". so now b is 1 and a is 2
c = ++a; // equivalent of "++a; b = a;". so now c is 3 and a is 3
Short circuting in Boolean operators
&&
- Returns expr1
if it can be converted to false
; otherwise, returns expr2
.
In other words, if the first expression is falsey, then it doesn't need to evaluate the second value. If it's not falsey, returns the second value.
||
- Returns expr1
if it can be converted to true
; otherwise, returns expr2
.
var a = 1;
var b = false;
a || (b = true); // b is still false after this, because (b = true) is never evaluated
# => 1
b && (a = false); // a is still true, because (a = false) is never evaluated
# => 2
- Not meant to return a value (help with logic
if...else
or instantiationvar a;
) - Cannot be part of an expression
- Statements are expressions
// These both return errors. A statement cannot be part of an expression.
var c = (var a = 1);
var c = (a = 1);
Type coercion
Implicit type coercion is an interesting feature of JS:
If two operands have different types, the equality operator (==) will try to convert one of the operands into another type before testing for equality.
3 == "3"
true
3 === "3"
false
Object equality
Two variables containing objects are only equal if they reference the same object.
// Arays are objects, too
var array = [1];
array === [1]; // false
Here's how you might compare two objects by using JSON.stringify()
. In this case, each object is info on a flight route
function isValidRoute(invalidRoutes, route) {
var invalidRouteFound = invalidRoutes.filter(invalidRoute => {
var invalidRouteValues = JSON.stringify(Object.values(invalidRoute));
var routeValues = JSON.stringify(Object.values(route));
return invalidRouteValues === routeValues;
});
return invalidRouteFound.length > 0;
}
isValidRoute(
{"airline":24,"src":"DFW","dest":"YYA"},
{"airline":24,"src":"DFW","dest":"YYA"}
)
The primitives are: numbers, strings, booleans, null
and undefined
. Everything else is an object.
var name = "Dylan"; // string primitive
name.indexOf("D"); // temporarily an Object ???
Primitives are immutable, even in conversion (new value is returned)
alpha[0] = 'z'; // "z"
alpha; // "abcde"
This is also the case with primitves passed to functions
function change(a) {
a = 2;
console.log(a);
}
var b = 1;
change(b); // logs 2
console.log(b); // logs 1
Variables don't hold values; they hold references to those values. A Function can't change the value of a variable that is passed to it as an argument.
An object can have its internal values changed within a function without having the reference change. Read more below.
Objects are mutable. Arrays are objects
alpha[0] = 'z'; // "z"
alpha; // [ 'z', 'b', 'c', 'd', 'e' ]
Objects passed to Functions can be modified by those functions
function increment(thing) {
thing.count += 1;
console.log(thing.count);
}
var counter = { count: 0 };
increment(counter); // { count: 1 }
count // { count: 1 }
If a Function can't change the value of a variable passed in, how is this possible?
increment
does not reassignthing
(which is the same ascounter
). Instead,increment
modifies a property of the Object: thecount
property. This alters the state of the object, but it does not create a new Object, sothing
andcounter
continue to point to the same thing.
String => Number
Number('12')
> 12
+("12")
12
Object => Boolean
Boolean('true') // true
Boolean(0) // false
Simpler method
!!('abc') // true
!!(0) // false
Global variables are properties of the global window
object
var foo = 1;
window.foo; // 1
function bar() {
return 1;
}
window.bar; // function bar() { return 1; }
Read the code and the comments below and try to figure out the answer.
function help() {
return a; // This line returns undefined. Shouldn't it return a ReferenceError. Why is that?
var a = "Help!"; // If you comment this code out, this function returns 123 instead of undefined. Why?
}
var a = 123; // undefined
help();
When variables are declared in Javascript, they are hoisted to the beginning of their current scope. Therefore, this is equivalent code to the snippet below:
var a;
function help() {
var a;
return a;
a = "Help!";
}
a = 123;
help();
The function returns undefined
because var a;
was hoisted to above the return expression, shadowing the global 123
. The function can access the global 123
because the global object is the implicit context.
Blocks
Local variables are scoped to functions in JavaScript, not blocks. Therefore the same behavior will be present for the following code
function say(b) {
if (b) {
var a = 'hello from inside a block';
}
console.log(a);
}
var b = 0;
say(b); // undefined
Without understanding this rule one would expect the code to have a ReferrenceError
for a missing variable a
.
ES6 variables, are not hoisted and will therefore throw a reference error if accessed too early.
console.log( a ); // undefined
console.log( b ); // ReferenceError!
var a;
let b;
There are four types of functions in JavaScript
function invocation: alert('Hello World!')
method invocation: console.log('Hello World!')
constructor invocation: new RegExp('\\d')
indirect invocation: alert.call(undefined, 'Hello World!')
Terms
Function vs. method invocation
If a receiver must be specified, it is a method. If no receiver needs to be specified, it is a function
Function Invocation
- Invocation is executing the code that makes up the body of a function (simply calling the function). For example
parseInt
function invocation isparseInt('15')
. - Context of an invocation is the value of
this
within function body.
Example:
[1,5].join(',')
is not a function invocation, it's a method invocation.
The fact that join is defined asArray.prototype.join
means that join
is a method on the Array prototype.
Function Definition
If a method is assigned in function expression, when it's invoked it counts as a function declaration.
var otherFunction = obj.myFunction;
otherFunction(); // function invocation
Return value
Functions without a return
statement return undefined
.
- Calling a Function with too few or too many arguments does not raise an error.
- Within a Function, an argument that wasn't provided in the call will have the value
undefined
.
function takeTwo(a, b) {
console.log(a);
console.log(b);
console.log(a + b);
}
takeTwo(1);
// logs out:
1
undefined
NaN
Scope of a function is a set of variables, objects, functions accessible within a function body.
var bunnyName = "Flopsy";
function hippityHoppity() {
console.log(bunnyName);
}
hippityHoppity(); // "Flopsy"
Variables defined in the global scope are accessible inside methods without being expressly passed in
Variable local/global scoping
A local variable has a seperate scope from a global one.
var bunnyName = "Flopsy";
function hippityHoppity() {
var bunnyName = "BUNSTER";
}
hippityHoppity();
console.log(bunnyName); // "Flopsy"
If you remove the var
, however, then the method will access the global variable and modify it
var bunnyName = "Flopsy";
function hippityHoppity() {
bunnyName = "BUNSTER";
}
hippityHoppity();
console.log(bunnyName); // "BUNSTER"
This 'global shadowing' behavior will not happen if you pass the variable to the function as a parameter.
var bunnyName = "Flopsy";
function hippityHoppity(bunnyName) {
bunnyName = "BUNSTER";
}
hippityHoppity();
console.log(bunnyName); // "Flopsy"
This is an example of JS's "pass by value" property in action. Passing variable bunnyName
binds its value to the local variable bunnyName
variable inside hippityHoppity
function scope. Therefore reassignment doesn't affect the outer variable because they just reference the same value (not reference).
If you do want to make use of the global/nesting scope behavior, there needs to be an outer variable reference for the function scoped variable
function hippityHoppity(bunnyName) {
bunnyName = "BUNSTER";
}
hippityHoppity();
console.log(bunnyName); // Uncaught Reference Error
The rules are a bit different if you're passing in an object. JS is pass by value, but when passing in objects (i.e arrays) the value is the reference
var bunnyNames = ["Flopsy", "Mopsy"];
function hippityHoppity(bunnyNames) {
bunnyName[0] = "BUNSTER";
}
hippityHoppity();
console.log(bunnyNames); // "[BUNSTER, Mopsy]"
Primitives: are passed in as reference-by-value - the function does not modify the passed in object.
function lessExcitable(text) {
return text.replace(/!+/g, '.'); // replaces ! with .
}
var message = 'Hello!';
lessExcitable(message); // "Hello."
message; // "Hello!"
Assign values to variables instead.
Objects: are passed into functions using pass-by-reference - (kinda, the value is the reference) modifications to objects inside the function are destrutive
Functions can not only call global variables inside methods, they can also destructively modify global variables.
var a = 1;
function Foo() {
this.a = 2;
this.bar = function() {
console.log(this.a);
};
this.bar();
}
var foo = new Foo();
a // 1
foo.bar(); // 2
foo.a // 2
a // 1
Foo(); // 2
a // 2
obj = {};
obj.bar(); // UndefinedMethod
Foo.call(obj); // 2
obj.bar(); // 2
Nondestructive
- In
foo.bar()
,bar
is called on thefoo
object which has access toa
Destructive
Foo();
modified the global object so thatwindow.a = 2
obj.bar();
will only return 2, becauseFoo.call(obj)
permanently modified the obj already. If we would leave it out, then the result will beUncaught TypeError: obj.bar is not a function
.
Functions are first class objects, meaning they can be passed into other functions like other primitive objects.
sort
is a flexible example of this:
function compareSize(num1, num2) {
return num1 - num2;
}
[5,3,8].sort(compareSize)
> [3,5,8]
The function could also be passed in as an anonymous function expression as well.
function outer() { return "hello world" }
var foo = outer; // assign the function to another variable
foo();
Essentially, declarations are doing extra work by creating a variable behind the scenes. The variable obeys general scoping rules.
function quack() {
console.log("Quack!")
}
Expressions create a reference which needs to be handled (you usually assign it to a variable). You can assign functions to variables to invoke later.
var fly = function() {
console.log("Fly!");
}
Named function expressions can only access their name inside the function
var hello = function foo() {
console.log(typeof foo); // function
};
hello();
foo(); // Reference Error
One important distinction between function expressions vs. declarations is hoisting.
Function declarations hoisting
Function declarations are defined first while function expressions are evaluated during code execution.
That's why line 2 (which is a call to a function declaration) does not throw an error.
console.log(quack());
function quack() {
return "Quack"
}
Function expression hoisting
But not function expressions
console.log(fly); // undefined
var fly = function() {
return "Fly!";
}
Function expression hoisting rules are the same as regular variables. The equivalent code looks like this:
var fly;
console.log(fly);
var fly = function() {
return "Fly!";
}
The same hoisting behavior happens with function assignment.
console.log(hello());
var hello = function() {
return 'hello world';
}
// "Uncaught TypeError: hello is not a function
Therefore as a rule you should:
- Declare functions before calling them.
- Declare variables on the top of the their scopes.
Execution
Code first looks for function declarations, and sets the function to the return value.
Function expressions are evaluated with the rest of the code.
Short ciruiting loops
It's possible to short circut iteration in a for
loop
function run() {
for(var i = 1; i < 4; i++) {
alert(i)
if (i === 2) {
return false;
}
}
}
run()
// 1
// 2
The execution was short circuited before it could reach 3
.
It's not possible in a vanilla JS forEach
callback.
[1,2,3].forEach(function(v) {
console.log(v);
if (v === 2) {
return false;
}
});
// 1
// 2
// 3
If it was, the code would never have reached 3
. The reason this is is because a new function is invoked in each iteration. This isn't necessarily the case though for JS library's like jQuery or lodash. Read more here.
Immediately-Invoked Function Expressions
Useful for creating your own private scope. For example, say you need to do a for loop in a messy piece of code where i
or the name of the function might be defined already.
(function() {
for(var i=0; i<100; i++) {
console.log(i);
}
})();
IIFE don't have to use the function declaration style. Here's a simple example using a function expression and a parameter:
var message = (function(name) {
return 'Hello ' + name + '!';
})('World');
console.log(message) // => 'Hello World!'
The first pair of parenthesis in (function(name) {...})
is an expression that evaluates to a function object. The second pair of parenthesis is the name parameter's argument 'World'
.
Wrapping parens
It would seem like you wouldn't need always need wrapping parens as these two expression both return function objects.
var func = function() { console.log('Hi'); } // [Function: fun]
var funct = (function() { console.log('Hi'); }) // [Function: funt]
However, when dealing with function declarations we need to pay attention to the wrapping parens. A clue to why we need wrapping parens is from looking at the return values from a function declaration vs. a function expression.
(function() {
for(var i=0; i<100; i++) {
console.log(i);
}
});
// => [Function]
function() {
for(var i=0; i<100; i++) {
console.log(i);
}
};
// => undefined
When assigned in a function expression as above, there is a return value (this strikes me as a little odd that declarations don't by default, but oh well), so we can avoid wrapping parens if we use a variable assignment to a function expression.
var func = function() { console.log('Hi'); }() // Hi
Functions that take a Function as an argument.
This Function will be invoked for each element in the Array
and allow the developer to define specifics about how the chosen abstraction should be implemented. Because it is "called back" later with each element, this Function
is called a callback.
Gives you the ability not only to write a function to solve a specific problem, such as "iterate through an array and log out the elements", but to write an abstraction, such as "iterate through an array and do something". This allows you to focus on what to do with each element in the Array, and not concern itself with how to iterate through an Array
.
Abstraction | Used To | Returns | Array Methods |
---|---|---|---|
Iteration | Perform an operation on each element in an Array | nothing | forEach |
Filtering / Selection | Limit to a subset of elements | new Array | filter |
Transformation | Compute a value for each element | new Array | map |
Ordering | Arrange elements by sorting | new Array | sort |
Reducing / Folding | Iteratively compute a result using each element | single value | reduce , reduceRight |
Interrogation | Determine if an Array's elements pass a test | single value | every , some |
To demonstrate how reduce
works, we'll go over a common task - gathering and manipulating form inputs.
In JS development you usually aren't going to rely on the form sending data to the server - instead you're going to grab it when an event handler fires. If you're using jQuery you might use serializeArray()
to create an object for each input
field inside your event callback.
<label> Starting Point
<input type="number" name="startX" placeholder="x:">
<input type="number" name="startY" placeholder="y:">
</label>
<label> Ending Point
<input type="number" name="endX" placeholder="x:">
<input type="number" name="endY" placeholder="y:">
</label>
$(function() {
// Called from inside form callback
function getFormObject($form) {
var $inputs = $form.serializeArray();
console.log($inputs);
}
...
});
// [Object, Object, Object, ... ]
Each object looks something like this { startX: 'foo', value: 'bar' }
. This structure nests the data a bit too deeply for convenient access, so you might combine all these into a single object with unique key/value pairs; i.e { startX: 'foo', startY: ... }
.
A common, and arguably still fairly readable way would be to iterate through and 'decorate' a new object.
...
var formObject = {};
$inputs.forEach(function(item) {
formObject[item.name] = item.value;
});
return formObject;
But maybe we want to work with pure functions, so this gives us a chance to learn about reduce
. Here's an example of how:
var formObject = $inputs.reduce(function(list, item) {
formObject[item.name] = item.value;
return formObject;
});
console.log(formObject);
Reduce returns an accumulator, which is be default the object that calls reduce (in this case $inputs
). However, the output looks like this:
{ endX : "3", endY : "4", name : "startX", startY : "2", value : "1" }
But wait... why is startX
a value instead of a key? Also, instead of 1 being the value pair to the startX
, 1 is the value to key value
. What's going on here?
As with many JS mysteries, the answer is revealed in the MDN documentation. In this example, it helped to look and what they wrote on reduce.
arr.reduce(callback, [initialValue])
The first time the callback is called, accumulator and currentValue can be one of two values. If initialValue is provided in the call to reduce, then accumulator will be equal to initialValue and currentValue will be equal to the first value in the array. If no initialValue was provided, then accumulator will be equal to the first value in the array and currentValue will be equal to the second.
So we fell for the trap of assuming our accumulator was one thing ( an empty object) when it was actually another the first object (the startX
key-value pair) in our collection. To correct this issue we simply supply an empty object as the initial value.
var formObject = $inputs.reduce(function(list, item) {
formObject[item.name] = item.value;
return formObject;
}, {});
Which formats our values correctly this time :)
...
console.log(formObject);
// { endX : "3", endY : "4", startX: "1", startY : "2" }
An example of how you would iteratively create a number of methods on a specific object
var _ = function(element) {
u = { .... }
(["Boolean", "String", "Number"]).forEach(function(method) {
_["is" + method] = function(obj) {
return toString.call(obj) === ["object " + method];
};
});
return u;
}
_.isBoolean(false) // true
- refers to the current execution context of a function
- is used to access properties defined on objects from inside that object
var car = {
make: "Fiat",
started: false,
start: function () {
this.started = true;
}
};
Making safe copies of objects code is fairly straightforward.
object = { 'a': 1 };
newObject = Object.assign({}, first); //
newObject['a'] = 2;
newObject // { 'a': 2 }
object // { 'a' : 1 }
Here are the ways we could create new_object
to accomplish this:
newObject = Object.create(first);
Cloning deeply nested objects is a bit trickier.
object = { 'a': 1 };
array = [object];
newArray[0]['a'] = 2
newArray // [{ 'a': 2 }]
array // [{ 'a': 1 }]
How do we make this code work? It depends on how we create (or rather, clone/duplicate) newArray
. We could try these two ways
var newArray = array.slice(0);
newArray[0]['a'] = 2
array // [{ 'a': 2 }]
But this doesn't work. Here's a similar method
var newArray = Array.prototype.slice.apply(array)
newArray[0]['a'] = 2
array // [{ 'a': 2 }]
These both copy references so they won't work. The same thing will happened with Object.assign([], array)
. What does work is using JSON.
JSON deep object cloning
var newArray = JSON.parse(JSON.stringify(array))
newArray[0]['a'] = 2
array // [{ 'a': 1 }]
ES6
The splat operator replaces apply, so we could try using that one as well. However, it too copies references.
let newArray = [...array]
newArray[0]['a'] = 2
array // [{ 'a': 2 }]
function createPerson(firstName, lastName) {
return {
firstName: firstName,
lastName: lastName || '',
fullName: function() {
return (this.firstName + ' ' + this.lastName).trim();
}
};
}
var person = createPerson('Jon', 'Doe');
- Help build objects using a template
- Built with all methods, which may be duplicative
- No way to know whether it's created from a factory function. Can't tell "type"
- Blueprint for creating objects.
- Do not need a return statement
- Can take functions as properties
- Use
new
and capitalization as conventions
When a function is called with the new
keyword, the following happens:
- A new object is created
this
in the function is pointed to the new object- the code in the function is executed
this
is returned if there's not an explicit return
function Dog(name, weight) {
this.name = name;
this.weight = weight;
this.getSize = function() {
if (this.weight > 20) {
return "large";
} else {
return "small";
}
}
}
var pippin = new Dog('Pippin', 20)
new
and the implicit context
We can tell that this
in the above function was a new object because the window
object was unmodified after execution.
window.getSize() // getSize is not a function
However the outcome is different if we leave off the new
keyword
Dog("Sami", 30);
window.getSize(); // large
Although constructors are very useful, object literals still have a place. In particular, when your constructor has many parameters and/or you are only making one object.
A constructor that needs changing could look like this:
function Car(make, model, year, color, passengers, convertible, mileage) {
this.make = make;
this.model = model;
this.year = year;
this.color = color;
this.passengers = passengers;
this.convertible = convertible;
this.mileage = mileage;
this.started = false;
}
var cadi = new Car("GM", "Cadillac", 1955, "tan", 5, false, 12893);
Change the literal to this:
var cadiParams = { make: "GM",
model: "Caddilac",
year: 1955,
color: "tan",
passengers: 5,
convertible: false,
mileage: 12892 };
Now you can just pass in the literal to the constructor.
function Car(cadiParams)
And modify the constructor to make this work.
function Car(params) {
this.make = params.make;
this.model = params.model;
this.year = params.year;
this.color = params.color;
this.passengers = params.passengers;
this.convertible = params.convertible;
this.mileage = params.mileage;
this.started = false;
}
All objects have a prototype chain, and a prototype
property. When an object is created, it receives an internal prototype property (although it's possible for it to be blank - Object.create(null)
) - which references another object.
What does this linkage do? If a property or method is called on an object which doesn't have that property itself, it will look up the prototype chain for the first instance of that property.
For example, an object literal's prototype
property is the Object.prototype
object, which provides object methods to all objects.
var foo = {
a: 1,
b: 2
}; // created with object literal
foo.toString(); // [object Object]
toString()
was not defined on foo
, but it was defined on Object.prototype.foo
. And as mentioned above, the fact that foo
s prototype is Object.prototype
gives it access to these methods.
Object.getPrototypeOf(foo) === Object.prototype; // true
Setup
function Foo() {};
var a = new Foo();
a
object is the prototype object of theFoo
object OR- object
a
is created with objectFoo
as its prototype
Using a constructor changes the returned object's prototype to the constructor function's own prototype
We'll use Object.isPrototypeOf(obj)
- which returns the prototype of object - to test that hypothesis
Object.getPrototypeOf(a) === Foo.prototype // true
Each object created from calling new Foo()
will end up (somewhat arbitrarily) [[Prototype]]
-linked to this "Foo dot prototype" object.
...
Foo.prototype.isPrototypeOf( a ); // true
This is essentially asking, in the entire [[Prototype]]
chain of a
, does Foo.prototype
ever appear?
You can create prototype chains with Object.create
as well.
Here's the inverse of the prototype relationship testing examples from above, using Object.create
.
var foo = {};
var a = Object.create(foo);
Object.getPrototypeOf(a) === foo; // true
foo.isPrototypeOf(a); // true
Object.create(..)
creates a "new" object out of thin air, and links that new object's internal [[Prototype]]
to the object you specify (foo
in this case).
Not using Object.create
for cloning
You usually would not use Object.create
for cloning because it modifies the prototype inheritance chain for the newly created object as described above.
// Continued from above ...
var b = Object.assign({}, foo);
Object.getPrototypeOf(a) === Object.prototype; // false
Object.getPrototypeOf(b) === Object.prototype; // true
Below are some stripped down examples that demonstrate a bit of the mechanics on how JS objects are linked along the prototype chain.
var foo = { a: 1, b: 2 };
var bar = Object.create(foo);
Object.getPrototypeOf(bar) === foo // true
bar.__proto__ === foo; // true
// `foo` object is the prototype (object) of the bar object
var baz = Object.create(bar);
Object.getPrototypeOf(foo) === Object.prototype; // true
Object.getPrototypeOf(baz) === bar; // true
foo.isPrototypeOf(baz); // true - because foo is on qux's prototype chain
JavaScript objects inherit behavior from other objects. JS object linked on the prototype chain can share behavior. Below are some simple examples:
function Dog() {
this.species = "Dog";
}
Dog.prototype.speak = function() {
console.log("Bark!")
}
var rover = new Dog();
rover.speak(); // Bark!
It's completely fine if a new method is assigned after the new object is instantiated since they refer to the same prototype object.
function Dog() {
this.species = "Dog";
}
var rover = new Dog();
Dog.prototype.speak = function() {
console.log("Bark!")
}
rover.speak(); // Bark!
When speak()
is called on rover
, it looks for any speak()
method on the instance. When it can't find one, it looks on the prototype for one. This same things is done for properties on instances (like rover.species
). Here are other formats to use if you want to create multiple methods on the Dog
prototype:
- To avoid repeating
Dog.prototype
, you might choose to assign an object literal toDog.prototype
:
...
Dog.prototype = {
speak: function() {
console.log("Bark!")
},
walk: function() {
console.log("Walks")
}
}
...
However, to use this code safely you should first understand why the following code won't work:
function Dog() {
this.species = "Dog";
}
var rover = new Dog();
Dog.prototype = {
speak: function() {
console.log("Bark!");
}
}
rover.speak(): // Reference Error
The Dog
prototype object was reassigned, and the speak
method on rover
still looks to the original prototype object for this method definition. Finding none, a ReferenceError
is thrown.
There will be no error if:
- a)
rover
is instead instantiated on a line after the methodspeak
is added toDog.prototype
(and beforerover.speak()
is called), or
// ...
// Dog.prototype = { speak: function { ... } }
var rover = new Dog();
rover.speak(): // Reference Error
- b) If the
speak
method was added to the existingDog.prototype
like above (i.e,Dog.prototype.speak = function () {}
), rather than reassigning the prototype object. Even ifrover
was created before thespeak
method was defined,rover
would still look to its original prototype object and findspeak
defined there.
- You can also pass in a (singleton / object literal) object to
Object.create()
:
var dog = {
say: function() {
console.log(this.name + ' says Woof!');
}
};
var fido = Object.create(dog);
fido.name = 'Fido';
fido.say(); // Fido says Woof!
The say
method is found on fido
object's prototype object, then called with context of fido
object itself. This is roughly the basis for the OLOO pattern, which we go into more later.
Objects can be created directly from other objects. Behaviors (methods) and state (properties) can be shared via the prototype chain.
- We can create dogs much more easily with the
dog
prototype - we don't have to duplicatesay
andrun
on every single dog object. - If we need to add/remove/update behavior to apply to all dogs, we can just modify the prototype object, and all dogs will pick up the changed behavior automatically.
Objects on the bottom of the prototype chain can "delegate" requests to the upstream objects to be handled.
var dog = {
say: function() {
console.log(this.name + ' says Woof!');
},
};
dog.name = 'Tim';
var fido = Object.create(dog);
fido.name = 'Fido';
fido.say = function() {
console.log(this.name + ' says Woof Woof!');
}
fido.say(); // Fido says Woof Woof!
dog.say() // Tim says Woof!
var foo = { a: 1 };
var bar = Object.create(foo);
bar.b = 2;
bar.hasOwnProperty('a'); // true
Object.getOwnPropertyNames(bar); // ["b"]
Other prototype methods
Object.prototype.toString()
: returns a string representation of the object
Object.prototype.isPrototypeOf(obj)
: tests if the object is in another object's prototype chain
Object.prototype.hasOwnProperty(prop)
: tests whether the property is defined on the object itself
Once you change the object's property, it is no longer a constructor (prototype?) derived property.
var foo = {
a: 2
};
var myObject = Object.create( foo );
foo.a; // 2
myObject.a; // 2
foo.hasOwnProperty( "a" ); // true
myObject.hasOwnProperty( "a" ); // false
myObject.a++; // oops, implicit shadowing!
foo.a; // 2
myObject.a; // 3
myObject.hasOwnProperty( "a" ); // true
Constructors have roughly the same delegation behavior - newly created do not "have" the property, but if the property is modified on that instance it changes"
function Foo() {}
Foo.prototype.a = 2;
var foo = new Foo();
var bar = new Foo();
foo.hasOwnProperty( "a" ); // false
bar.hasOwnProperty( "a" ); // false
bar.a++
bar.hasOwnProperty( "a" ); // true
However with constructors the use of this
copies the properties directly on the new linked objects, making newly created project "have" the property.
function anotherObject() {
this.a = 2;
}
var foo = new anotherObject();
var bar = new Foo();
foo.hasOwnProperty( "a" ); // true ... but should be false
bar.hasOwnProperty( "a" ); // false
Array.prototype
\ []
Array.prototype.slice.call(arguments)
vs.[].slice.call(arguments)
Object.prototype
\ {}
Does not work in all cases
Object.getPrototypeOf([]) === [] \\ false
Object.getPrototypeOf([]) === Array.prototype \\ true
- Every function has a special
prototype
property.
A function's prototype property is only useful when the function is used as a constructor, in which case all objects that it constructs will have this special object set as their prototype. This is the reason why the constructor property of the constructed object points back to the constructor function.
pippin.constructor; // function Dog(...)
pippin.constructor === Dog // true
Dog.prototype // Object {} - the constructor function's prototype object
new Dog() === Dog.prototype // true
Dog.prototype.constructor // function Dog(...)
Dog.prototype.constructor === Dog // true
Object.getPrototypeOf(pippin) === Dog.prototype // true
pippin instanceof Dog; // true
Evidence that function prototype property without new
keyword is not useful
Object.getPrototypeOf(Dog) // function () {}
Dog.prototype // Object {}
Object.getPrototypeOf(Dog) === Dog.prototype; // false
Object.getPrototypeOf(pippin) // Object {}
Object.getPrototypeOf(pippin) === Object.getPrototypeOf(Dog) // false
Object.getPrototypeOf(pippin) === Dog.prototype // true
If we wanted to share the behavior of Dog
with Poodle
, you could manually set the constructor (prototype?).
function Dog () {}
function Poodle () {}
Poodle.prototype = Object.create(Dog.prototype); // Worse, but also new Dog();
Poodle.prototype.type = "Poodle";
var poodle = new Poodle();
console.log(poodle instanceof Dog); // true
console.log(poodle instanceof Poodle); // true
poodle.constructor; // f Dog() {}
poodle.constructor === Dog; // false - should be true?
Object.getPrototypeOf(poodle) === Poodle.prototype; // true
Dog.prototype.isPrototypeOf(poodle); // true
The Poodle
prototype is an instance of Dog
, so poodle
is linked to Dog
on the prototype inheritance chain.
Constructor vs. prototype
Note that poodle
s constructor is Dog
but the prototype is Poodle.prototype
. If you reset a constructor's prototype the constructor property won't always reliable.
Modify the constructor prototype, not the constructed object
We do Poodle.prototype = Object.create(Dog.prototype)
, not poodle.prototype = Object.create(Dog.prototype)
. An instance of the object is not writeable in this way. In ES6 you can use Object.setPrototypeOf
.
Don't share prototype references
Poodle.prototype
= Dog.prototype
=> not good
If you you start adding functions onto Poodle.protoype
you'll be modifying Dog.prototype
.
Be careful of Obj.prototype
= new AnotherObj()
Poodle.prototype = new Dog()
creates a separate, linked object but may execute unplanned code or delegate certain properties that you don't want attached.
Using Object.create
Poodle.prototype = Object.create(Dog.prototype)
is a good way to go
Using Object.create
"make[s] a new 'Poodle dot prototype' object that's linked to 'Foo dot prototype'."
Use Object.setPrototypeOf
in ES6
// modifies existing `Bar.prototype`
Object.setPrototypeOf( Bar.prototype, Foo.prototype );
Object.getPrototypeOf(Dog)
// function () {}
Every function is a Function object created from the Function Constructor and as such also has a prototype (Function.prototype
).
Note that the prototype of a Function Object (i.e Dog
) can not be modified, unlike objects constructed (i.e dog
) from it.
Proto
__proto__
is an[[Prototype]]
accessor__proto__
returnsObject.getPrototypeOf
Object literal & prototype
What do you do if you want to add a , well since obj.prototype
is undefined you could use the __proto__
method
function Foo() {
// ...
}
Foo.prototype.constructor === Foo; // true
var a = new Foo();
a.constructor === Foo; // true
The Foo.prototype
object by default (at declaration time on line 1 of the snippet!) gets a public, non-enumerable property called .constructor
, and this property is a reference back to the function (Foo
in this case) that the object is associated with.
Moreover, we see that object a
created by the "constructor" call new Foo()
seems to also have a property on it called .constructor
which similarly points to "the function which created it".
This is just unfortunate confusion. In actuality, the .constructor
reference is also delegated up to Foo.prototype
, which happens to, by default, have a .constructor
that points at Foo
.
The .constructor
property on Foo.prototype
is only there by default on the object created when Foo
the function is declared. If you create a new object, and replace a function's default .prototype
object reference, the new object will not by default magically get a .constructor
on it.
function Foo() { /* .. */ }
Foo.prototype = { /* .. */ }; // create a new prototype object
var a1 = new Foo();
a1.constructor === Foo; // false!
a1.constructor === Object; // true!
a1
has no .constructor
property, so it delegates up the [[Prototype]]
chain to Foo.prototype
. But that object doesn't have a .constructor
either (like the default Foo.prototype
object would have had!), so it keeps delegating, this time up to Object.prototype
, the top of the delegation chain.
Object.defineProperties()
This function returns an object with an un-writeable log
property
function newPerson(name) {
return Object.defineProperties({ name: name }, {
log: {
value: function() {
console.log(this.name);
},
writable: false
}
});
}
var me = newPerson('Shane Riley');
me.log(); // Shane Riley
me.log = function() { console.log("Amanda Rose"); };
me.log(); // Shane Riley
Object.freeze()
- Object property values cannot be reassigned
- Internal property object values can be reassigned
- Unreversible operation on an object.
The Pseudo-classical pattern is a combination of the constructor pattern and the prototype pattern. With this pattern, we:
- use a constructor to set object states, and
- put shared methods on the constructor function's prototype:
var Point = function(x, y) { // capitalized constructor name as a convention
this.x = x || 0; // initialize states with arguments
this.y = y || 0; // 0 as default value
};
Point.prototype = {
onXAxis: function() { // shared behaviors added to constructor's prototype property
return this.x === 0;
},
onYAxis: function() { // can reassign prototype object (watch instanceof changes) or can add one by one, Point.prototype.onXAxis = function() {...}
return this.y === 0;
}
}
var pointA = new Point(30, 40); // use new to create objects
var pointB = new Point(20);
console.log(pointA instanceof Point); // use instanceof to check type
console.log(pointB instanceof Point);
console.log(pointA.distanceToOrigin()); // 50
console.log(pointB.onYAxis()); // true
Here JavaScript sheds its pretense to be a "class oriented" language, where it uses constructor functions as fake classes. Instead, it embraces its prototype based object model. With the OLOO pattern, we
- define the shared behaviors on a prototype object, then
- use
Object.create()
to create objects that inherit directly from that object, without going through the roundabout way that involves "constructors and their prototype properties" at all.
var Point = { // capitalized name for the prototype as a convention
x: 0, // default value defined on the prototype
y: 0,
onXAxis: function() { // shared methods defined on the prototype
return this.y === 0;
},
onYAxis: function() {
return this.x === 0;
},
init: function(x, y) { // optional init method to set states
this.x = x;
this.y = y;
return this;
},
};
var pointA = Object.create(Point).init(30, 40);
var pointB = Object.create(Point);
Point.isPrototypeOf(pointA); // use isPrototypeOf to check type
Point.isPrototypeOf(pointB);
pointB.onXAxis(); // true
First-class Functions initially have no context when created; they receive a context when the program executes them.
In other words, this
changes based on how a function is invoked, not how it was defined.
The global object window
is the implicit function execution context for global functions and expressions
var foo = 1;
function bar() { return 1; }
window.foo; // 1
window.bar; // function bar() { return 1; }
Global functions are defined on it such as isNaN()
window.isNan() // function isNaN() { ... }
Deleting
Properties defined on window cannot be deleted (sorta). You can delete global variables that you don't define, but NOT those that you did.
var foo = 1;
bar = 2;
delete window.foo; // false
delete window.bar; // true
window.foo; // 1
window.bar; // undefined
let
and const
variables aren't accessible by writing window.variable
name.
var object = {
foo: function() {
return "this here is: " + this;
}
};
object.foo(); // "this here is: [object Object]"
bar = object.foo;
bar(); // "this here is: [object Window]"
This is an example that shows that the binding of a function and context object happens when the function is executed, not when the function is defined.
JavaScript provides tools to manipulate function context.
call
Simple example
var obj = {
a: 1,
foo: function() {
console.log(this.a);
}
}
obj.foo(); // 1
var bar = obj.foo;
bar(); // undefined
var context = {
a: 2
}
bar.call(context); // 2
Using call
to give a function context with mutliple parameters
var iPad = { name: 'iPad', price: 40000 },
var kindle = { name: 'kindle', price: 30000 };
function printLine(line_number, punctuation) {
console.log(line_number + ': ' + this.name + ', ' + this.price / 100 + ' dollars' + punctuation);
}
printLine.call(iPad, 1, ';'); // "1: iPad, 400 dollars;"
printLine.call(kindle, 2, '.'); // "2: kindle, 300 dollars."
apply
The same example, except using apply
printLine.apply(kindle, [2, '.']);
Call: Count the Commas (you have to count the number of arguments to match the called function)
Apply: Arguments as Array
apply
can reduce the length of function parameters by organizing them into an array.
null
or undefined
work for the thisArg
when we don't have a specific object context to supply.
function add (a, b, c, d, e, f, g, h, i) {
return a + b + c + d + e + f + g + h + i;
}
var nums = [1,2,3,4,5,6,7,8,9];
console.log(add.apply(this, nums)); // 45
console.log(add.call(this, 1,2,3,4,5,6,7,8,9)); // 45
console.log(add.call(null, 1,2,3,4,5,6,7,8,9)); // 45
Here's a more realistic example with Math.max
, which expects arguments of number like Math.max(10,20)
var numbers = [1,2,3]
Math.max(numbers) # NaN
Math.max.apply(null, numbers)
=> 3
Math.max.apply(this, numbers)
=> 3
ES6 version using the spread operator ...
would simply be:
Math.max(...numbers)
bind
bind
permanently binds the execution context so it cannot be reassigned
var a = 'goodbye';
var object = {
a: 'hello',
b: 'world',
foo: function() {
return this.a + ' ' + this.b;
}
};
object.foo(); // 'hello world'
var bar = object.foo;
bar(); // 'goodbye undefined'
var baz = object.foo.bind(object);
baz(); // 'hello world'
var object2 = {
a: 'hi',
b: 'there'
};
baz.call(object2);
baz(): // 'hello world'; - baz context is still bound to object
The context of object
has been permanently bound to baz()
(the method object.foo
). Therefore call
has no affect.
bind
is a good option for fixing the loss of context problem because you can call it multiple times without having to use call
or self
each time.
- A bound function is a function bind with an object.
.bind()
makes a permanent context link and will always keep it. A bound function cannot change its linked context when using.call()
or.apply()
with a different context. Even callingbind
again has no effect.
function getThis() {
'use strict';
return this;
}
var one = getThis.bind(1);
one.call(2); // => 1
one.bind(2)(); // => 1
new one(); // => Object // Call the bound function as a constructor
Only new one
was able to change the context.
var john = {
first_name: "John",
last_name: "Doe",
greetings: function() {
console.log("hello, " + this.first_name + ' ' + this.last_name);
},
}
var foo = john.greetings;
foo(); // "hello, undefined undefined"
foo.call(john)
will work, but won't work if foo
is passed somewhere where it doens't have access to john
function repeatThreeTimes(func) {
func(); // can't do func.call(john), out of scope
func();
func();
}
function foo() {
var john = {
first_name: "John",
last_name: "Doe",
greetings: function() {
console.log("hello, " + this.first_name + ' ' + this.last_name);
},
}
repeatThreeTimes(john.greetings);
};
foo();
// "hello, undefined undefined"
// "hello, undefined undefined"
// "hello, undefined undefined"
foo.call(john);
// ReferenceError: john is not defined
You can also pass in context to functions as parameters
function repeatThreeTimes(func, context) {
func.call(context);
func.call(context);
func.call(context);
};
...
repeatThreeTimes(john.greetings, john);
};
foo();
// "hello, John Doe"
// "hello, John Doe"
// "hello, John Doe"
If you can't change the function context (for example passing it to a library method that you don't own), then bind
is more useful:
function repeatThreeTimes(func) { // Do not need to explicitly call context object
func();
func();
func();
}
...
repeatThreeTimes(john.greetings.bind(john));
};
foo();
The function below is missing the correct object context.
var temperatures = [53, 86, 12, 43];
function average() {
var total = 0;
for (var i = this.length - 1; i >= 0; i--) {
total += this[i];
}
return total / this.length;
}
console.log(average(temperatures)); // NaN
We could provide a context with methods like call
, apply
, and bind
. However, we can also add a method to the array (it's also an object), and simply call it from there
temperatures.average = average
console.log(temperatures.average()); // 48
var obj = {
a: "hello",
b: "world",
foo: function() {
function bar() {
console.log(this.a + ' ' + this.b);
}
bar();
},
}
obj.foo(); // undefined undefined
Nested functions (within an object) lose context, which is a problem.
Functions execute with their explicit context. The foo
method is called on obj
, which explicitly provides context for the foo
method. However, the call to bar()
on line 9 has no such explicit context - it's just a function declaration call bar()
. Since how a function is invoked determines the context, and not where it is defined, the JS (compiler\engine?) sees that there's no explicit context for this call and therefore binds it to the default context - global object. As a result, this
on line 6 is the global object, not obj
, thus a
and b
are undefined within the bar
function body.
Side note: Nested functions outside an object
The concept of closure in JavaScript makes it so nested function can avoid this problem. Obviously a
and b
will still be undefined
, nested functions retain access to to variables defined in outer scopes so c
and d
will be available to any inner functions nested in foo
.
var obj = {
a: "hello",
b: "world",
foo: function() {
var c = "bye";
var d = "world"
function bar() {
function baz() {
console.log(c + " " + d);
}
baz();
}
bar();
},
}
obj.foo(); // bye world
A local variable in an outer scope is available to inner/nested functions. Therefore the self = this
idiom will our context issue.
var obj = {
a: "hello",
b: "world",
foo: function() {
var self = this;
function bar() {
console.log(self.a + ' ' + self.b);
}
bar();
},
}
obj.foo(); // hello world
Providing context
var obj = {
a: "hello",
b: "world",
foo: function() {
function bar() {
console.log(this.a + ' ' + this.b);
}
bar.call(this);
},
}
obj.foo(); // hello world
This will bind the bar
function to its surrounding context when the function itself is defined. Note that we can only use bind
with a function expression, not a function declaration.
obj = {
a: 'hello',
b: 'world',
foo: function() {
var bar = function() {
console.log(this.a + ' ' + this.b);
}.bind(this);
bar();
}
};
obj.foo();
function logThis() {
console.log(this);
}
var myObject = {
objectMethod: logThis.bind(this),
test: 1,
}
myObject.objectMethod(); // Window
This is confusing, but I think the trick is to think about when myObject.objectMethod()
runs, it's effectively just calling logThis
. Just because logThis
is called within an object does not mean it gains the context of that object. This is a source of a lot of context / this
confusion in JS.
One way to change context is to provide it. You can do this by 1) supplying a calling object directly or 2) explicitly provide it on execution
var myObject = {
objectMethod: this.logThis, // 1) Caller used
anotherMethod: logThis.call(this), // 2) Calling context provided
}
myObject.objectMethod(); // myObject
Another way to work around the problem is to use the context that's provided inside of function execution
var myObject = {
objectMethod: function() {
console.log(this)
},
}
myObject.objectMethod(); // myObject
But can be bound to a new variable while executing a function expression.
var myObject = {
objectMethod: function() {
var bar = logThis.bind(this);
bar()
},
}
myObject.objectMethod(); // myObject
Without an expression, binding does nothing.
logThis.bind(this)
logThis(); // Window
function repeatThreeTimes(func) {
func();
func();
func();
};
var john = {
first_name: "John",
last_name: "Doe",
greetings: function() {
repeatThreeTimes(function() {
console.log("hello, " + this.first_name + " " + this.last_name);
});
}
};
john.greetings();
// "hello, undefined undefined"
// "hello, undefined undefined"
// "hello, undefined undefined"
The this
context is also not going to be preserved, because even though the function expression is defined within the object, it's not executed with the context of the object.
...
greetings: function() {
self = this;
repeatThreeTimes(function() {
console.log("hello, " + self.first_name + " " + self.last_name);
});
}
};
...
The callback function within repeatThreeTimes
is a function expression, and therefore we can use bind
to set the context.
...
greetings: function() {
repeatThreeTimes(function() {
console.log("hello, " + this.first_name + " " + this.last_name);
}.bind(this));
}
};
The same thing happens with certain built in iterator/callback methods
- Use
bind
Without bind
, the function expression that contains console.log
is executed in JavaScript's built in forEach
method losses the obj
context.
obj = {
a: 'hello',
b: 'world',
foo: function() {
[1, 2, 3].forEach(function(number) {
console.log(number + ' ' + this.a + ' ' + this.b);
}.bind(this));
},
};
obj.foo();
// 1 hello world
// 2 hello world
// 3 hello world
A function expression (the callback) occurs in the forEach
loop, and therefore allows for the use of bind
.
thisArg
The optional param thisArg
can be used as the value of this
(reference Object) when executing the callback function. Otherwise, the value undefined
will be used as its this
value. Available on:
Array.prototype.forEach
Array.prototype.map
Array.prototype.every
Array.prototype.some
obj = {
a: 'hello',
b: 'world',
foo: function() {
[1, 2, 3].forEach(function(number) {
console.log(number + ' ' + this.a + ' ' + this.b);
}, this);
},
};
obj.foo();
var myObject = {
count: 1,
myChildObject: {
myMethod: function() {
console.log(this.count);
}
}
};
myObject.myChildObject.myMethod(); // undefined
myMethod
above is called on the myChildObject
, so myChildObject
is the execution context for myMethod
. The object property count
is in the myObject
context, and therefore undefined when myMethod
is invoked like it is above.
I believe the only way to pass internal information about a given object is through specifying context (using indirect invocation) when calling or binding the context. We can do this either with call
myObject.myChildObject.myMethod.call(myObject); // 1
or bind
var countPrint = myObject.myChildObject.myMethod.bind(myObject) console.log(countPrint());
// 1
Passing around context can be used to create class hierarchies in ES5 to call the parent constructor. This is an interesting way to delegate behavior.
function Identify(name) {
this.name = name.toUpperCase();
}
function Rabbit(name, countLegs) {
Identify.call(this, name);
this.countLegs = countLegs;
}
var myRabbit = new Rabbit('White Rabbit', 4);
myRabbit; // { name: "WHITE RABBIT', countLegs: 4 }
Identity.call(this, name)
inside Rabbit makes an indirect call of the parent function to initialize the object.
For more about indirection invocation and an explanation of this
in general, see here for a good blog post
- JavaScript functions create closures when they are invoked.
- Closures allow code within a function to access any variable that was accessible at the time the function was declared.
- Values that are no longer accessible from anywhere in the code will be garbage collected and any memory they were using can then be used by other parts of the program.
- Closures can be used to make variables "private" to a function or set of functions and inaccessible from anywhere else.
- Closures allow functions to "carry around" values for later use.
- Every function declaration creates a new scope
- All variables in the same or surrounding scope are available to your code.
Higher-order functions - functions that take a function as an argument, return a function, or both.
Another snippet about closures:
- Code within a function can access any variables defined within the function.
- Code within a function can also access any variables that can be accessed from the context where it was defined.
The first point is more obvious, the second may not be. What does it mean, the variables from the context from where it was defined?
To be more specific, an inner function has access to variables defined in any outer/surrounding scopes (includes function and global scopes). It doesn't matter how deeply nested a function is.
function beginGreeting() {
var name = 'Julian';
function rememberName() {
function greet() {
console.log(name);
}
greet();
}
rememberName();
}
beginGreeting(); // 'Julian'
This would still work if name
was defined in the global scope, above beginGreeting()
.
This same principle works for objects, and functions within objects
var b = 1;
function makeObj() {
var a = 0;
return {
returnA: function() {
return a;
},
returnB: function() {
return b;
},
}
}
makeObj().returnA(); // 0
makeObj().returnB(); // 1
Variables in upper (or parent) scopes are available to lower (nested) scopes.
Objects within functions have the same scope as the function are they are in.
JS uses the structure of the source code to determine the variable's scope.
- It can access any variables defined within it.
- It can access any variables that were in scope code can access from the context where the function was defined.
When searching for a variable, JS searches this hierarchy from the bottom to the top. It stops and returns the first variable it finds with a matching name. This means that variables in a lower scope can shadow, or hide, a variable with the same name in a higher scope.
var name = 'Julian';
function greet() {
var name = 'Logan';
console.log(name);
}
greet(); // Logan
When greet()
is invoked it looks in the function scope first for name
, and does not look further so you can only access the inner scope name
A parameter would shadow the outer scope as well
var name = 'Logan';
function greet(name) {
console.log(name);
}
greet('Sam'); // Sam
The value of a variable can change after creating a closure that includes the variable. When this happens, the closure sees the new value; the old value is no longer available.
Outer reference
var count = 1;
function logCount() { // create a closure
console.log(count);
}
logCount(); // logs: 1
count++; // reassign count
logCount(); // closure sees new value for count; logs: 2
"Closed over" reference
Examples using global variables can sometimes just seem like "global magic", so here's an example that's a bit more abstract.
var Foo = function(value){
this.bar = function(){
return value;
};
};
var foo = new Foo("Hey");
foo.bar(); // ?
What does this return? It might not be apparent, but x
returns "Hey"
when baz
is called. Let's look at something a little more familiar - a slightly re-worked example of the logCount
function above except that we'll modify it to use a constructor and a method like the Foo
example.
var Counter = function(count) {
this.addOne = function() {
count += 1;
console.log(count);
}
}
var count = new Counter(0);
count.addOne(); // 1
- When
new Counter
is called,0
is set tocount
in the new execution context. - The anonymous function assigned to
addOne
now takes in all local / context variables from it's outer execution context to assign to its local execution context. In this case it's justcount
. - The
count
variable is incremented, and then logged on the next line.
It should now make more sense why foo.bar
returned "Hey"
. There's more on closures below, but if you want to see some other people's thoughts on the subject here are two good articles.
https://medium.com/dailyjs/i-never-understood-javascript-closures-9663703368e8 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures#Closure
function createAdder() {
let value = 0;
const add = (amount) => (value = value + amount);
const getValue = () => (value);
return {
add,
getValue,
}
}
value
is a private variable. Only the functions declared insidecreateAdder
have access to it.value
is inside of this closure, so the variable “lives on” between function calls.
adder.add(1);
adder.getValue(); // => 1
adder.add(1);
adder.getValue(); // => 2
Pass a function to a function
function makeTimer(doneMessage, n) {
setTimeout(function() {
console.log(doneMessage);
}, n);
}
makeTimer("Cookies are done!", 1000);
Again, function INNER has access to vars in function OUTER if function INNER is nested in OUTER. No matter how deeply nested.
Return a function's return value
var secret = "007";
function getSecret() {
var secret = "008";
function getValue() { return secret; }
return getValue();
}
getSecret(); // "008"
Essentially getSecret()
returns the result of executing the getValue()
function.
Returning a function reference:
var secret = "007";
function getSecret() {
var secret = "008";
function getValue() { return secret; }
return getValue; // Note lack of parens - getValue was not invoked
}
var getValueFun = getSecret();
getValueFun(); // "008"
Here getSecret();
returns a reference to the getValue
function so it can be executed later.
Returning an anonymous function could work fine here as well
var secret = "007";
function getSecret() {
var secret = "008";
function getValue() { return secret; }
return function() { return getValue(); }
}
var getValueFun = getSecret();
getValueFun(); // "008"
Improving getSecret
What if you wanted to add a reveal
method, and add a better user interface too?
- Change to return an object
function getSecret() {
var secret = "008";
return {
reveal: function() {
return secret;
}
}
}
var secret = getSecret();
secret.reveal(); // "008"
- Add a
set
method
Hmm, this way doesn't seem to work.
secret.set = function(newSecret) {
// there is no way to access the items from within this method
// because the closure that contains them is inaccessible
secret // => throws Reference error!
this.secret // => undefined
};
For the reasons in the comments in the code snippet above, we need to change the getSecret
definition itself to get this to work.
function getSecret() {
var secret = "008";
return {
reveal: function() {
return secret;
},
set: function(newSecret) {
secret = newSecret;
}
}
}
var secret = getSecret();
secret.reveal(); // "008"
secret.set('005');
secret.reveal(); // "005"
b) Return an anonymous function which returns a value
var makeCounter = function() { // using a function expression here, but it's the same as above
var count = 0;
return function() {
return count++;
}
};
var doCount = makeCounter();
doCount(); // 0 Can you tell why this isn't 1? :)
doCount(); // 1
You can use a function expression (FE) to create an IIFE if you do not want to assign the function
var makeCounter = function() {
var count = 0;
return function() {
return count++;
}
}();
makeCounter() // 0
makeCounter() // 1
This also has the added benefit of making it so their scope is local. I.e a global count variable might cause problems in a large code base.
You could even avoid having to invoke the returned function
...
return function() {
return count++;
}()
}();
makeCounter // 0
makeCounter // 0
makeCounter() // TypeError makeCounter is not a function
However this returns a value, not a function.
Or, passing arguments to callback functions. Closure functions can take arguments
function makeCounterLogger(start) {
// Inner function also takes an argument here, but sometimes scoping means it doesn't need to.
return function(stop) {
for (var i=start; i <= stop; i++) {
console.log(i);
}
}
}
makeCounterLogger(5)(8)
Persistent reference
An idea we touched on earlier was that you can pass in params that can be accessed later. The idea is also called currying, or partial application. Let's see it applied to our counter logger.
var countFromFiveTo = makeCounterLogger(5);
var countFromFiveTo = makeCounterLogger(7);
countFromFiveTo(8); // 5 6 7 8
countFromFiveTo(8); // 7 8
This behavior allows callbacks to take arguments, which can be useful when dealing with callback heavy libraries such as jQuery or standard library methods that take a callback, like filter.
http://www.jstips.co/en/javascript/passing-arguments-to-callback-functions/
To shorten up some code, I wanted to pass in a function as a callback to filter
this.activeFilters = this.activeFilters.filter((filterName) => {
return filter.name !== filterName;
})
Extracting the filtering work to this function works, but doesn't feel great.
const removeFilter = (filter) => {
return this.activeFilters.filter((filterName) => {
return filter.name !== filterName;
});
}
this.activeFilters = removeFilter(filter)
So we looked at closures to pass in to our filter. The problem was we needed an outside value, something like this array.filter(removeItem(12))
.
const removeItem = function(item) => {
return function(currentItem, index, array) => {
return item !== value
}
}
This looks a bit better, but notice that there's a couple areas we can improve. 1) We included several extra parameters here for illustrative purposes we don't need. 2) Also, we can use ES6 to make this more terse. The result is this:
const removeItem = item => currentItem => currentItem !== item;
...
this.activeFilters = this.activeFilters.filter(removeItem(filter.name))
Sometimes another function will have a variable your function wants access to. Local scope is hard to share especially between methods, so passing functions that take callbacks can help here.
function otherFunc(mainCallback, local) {
var otherVar = 2;
return mainCallback(local)(otherVar)
}
function main() {
var local = 1;
var callback = function(local) {
return function(otherVar) {
return local + otherVar;
};
};
// able to access to otherVar here
return otherFunc(callback, local)
}
main()
If you didn't want to pass in local, and instead "close" over it
function otherFunc(mainCallback) {
var otherVar = 2;
return mainCallback(otherVar)
}
function main() {
var local = 1;
var callback = function(otherVar) {
return (function(local) {
return local + otherVar;
})(local)
};
}
main();
// ES6 version of `callback`
const callback = otherVar => (local => local + otherVar)(local);
// ...
You do an IIFE on the inner closure function. Which allows mainCallback
to retain access to any local variable passed to it’s inner function. In this case we passed in local
. So when the mainCallback
is invoked in the new scope (otherFunc
) you can take in additional params without worry about passing in your previous local ones.
This is some normal JS OOP code
function Person() {
this.name = "Bob";
this.age = 47;
}
Person.prototype.info = function() {
console.log(this.name + ' ' + this.age);
}
var bob = new Person();
bob.info() // Bob 47
A problem arises if you try to use a method as a callback.
setTimeout(bob.info, 100); // undefined
This is because specifying a method of an object as the callback function will cause the function by itself to be the callback, not the object associated with it.
this
is assigned on function call (it's not something tied to the function itself), so binding the function manually will work
setTimeout(function () {
bob.greet();
}, 100);
Variables from the closure, however are part of the function itself, no matter how it's called. Therefore if we change our code to using 100% closures the behavior will work.
function newPerson() {
var name = "Tim";
var age = 28;
return {
info: function() { console.log(name + ' ' + age); }
}
}
var tim = newPerson();
tim.info() // Tim 28
setTimeout(tim.info, 100); // Tim 28
This works the same if you're referencing a function expression assigned to a variable from within the closure
function worker() {
var title = 'garbage man';
var logTitle = function() { console.log(title); };
return {
publicLogTitle: function() { logTitle() }
}
}
var newWorker = worker();
newWorker.publicLogTitle(); // 'garbage man'
setTimeout(newWorker.publicLogTitle(), 1000); // 'garbage man'
Call certain arguments ahead of time to be called later. Here's a very simple example:
function later(func, arg) {
return function() {
func(arg);
}
}
var logWarning = later(console.log, 'The system is shutting down!');
logWarning();
Here's a slightly more complicated example
function greet(greetString, nameString) {
return greetString[0].toUpperCase() + greetString.slice(1) + ', ' + nameString + '!';
}
function partial(primary, arg1) {
return function(arg2) {
return primary(arg1, arg2);
};
}
var sayHi = partial(greet, "hi")
sayHi("Sarah"); // "Hi, Sarah!
JS allocates memory when new variables or properties are declared, and deallocates the memory when those variables go out of scope or are deleted.
Go out of scope means no closures still in the system that contain a reference to the memory.
Variables defined at the top level of a JavaScript program remain accessible for the duration of the program's execution. They are also accessible within any functions defined in the program.
This would be garbage collected:
function logName() {
var name = "Sarah"; // Declare a variable and set its value. The JavaScript runtime
// automatically allocates the memory.
console.log(name); // Do something with name
}
This would NOT be garbage collected:
function makeHello(name) {
var question = "how are you";
return function() {
console.log('Hello, ' +question + name + '?');
}
}
var helloSteve = makeHello('Steve');
When you create a closure, it stores a reference to all variables it can access.As long as the closure exists, those variables remain in existence, which means that the objects or values they reference must also endure.
Until you do something like this message
and question
will be accessible, as the function will no longer be availabe in the global scope.
helloSteve = null;
DOM - an in-memory object representation of an HTML document. It provides a way to interact with a web page using JavaScript
The DOM is a hierarchy of nodes. Each node can contain any number of child nodes.
- All DOM objects are Nodes.
- All DOM objects have a specific type that inherits from Node, which means they all have properties or methods along with those they inherit from Nodes.
- The most common DOM object types you will encounter are elements and text nodes.
Node types
EventTarget
- provides the event-handling behavior that supports interactive web applications.Node
provides common behavior to all nodes.Text
andElement
are the chief subtypes of Node.Text
nodes hold text.- Element nodes represent HTML tags.
- Most HTML tags map to specific element subtypes that inherit from
HTMLElement
.
Testing node types
toString
is a good way to test a node's type
var t = document.querySelector("p").firstChild
t.toString() [object Text]
instanceof
is better when writing code
var p = document.querySelector('p');
p instanceof HTMLParagraphElement; // true
p instanceof Element; // true
Useful node properties
nodeName
- Caps name of a html tag (or #comment / #text)nodeType
- Integer value representing a node's typenodeValue
- contains the textual content of the NodetextContent
- returns Element's textual content of every Text node that is a child of the Element Node.tagName
- same asnodeName
, except that it only works with Elements (which can also usenodeName
)
Nodes have properties that traverse the DOM tree. All elements have both parent and child nodes, and therefore all methods below.
Parent Node Properties | Value |
---|---|
firstChild |
childNodes[0] or null |
lastChild |
childNodes[childNodes.length-1] or null |
childNodes |
Live collection of all child nodes |
Child Node Properties | Value |
---|---|
nextSibling |
parentNode.childNodes[n+1] or null |
previousSibling |
parentNode.childNodes[n-1] or null |
parentNode |
Immediate parent of this node |
A given DOM element may return a null
value, depending on the DOM.
Walking the dom
This is similar to how you would write your own forEach
method.
function walk(node, callback) {
// do something with node
callback(node);
// for each child node
for (var i = 0; i < node.childNodes.length; i++) {
// recursively call walk()
walk(node.childNodes[i], callback);
}
}
// log the nodeName of every node
walk(document.body, function(node) {
console.log(node.nodeName);
});
Elements have properties to traverse the DOM tree:
Parent Element Properties | Value |
---|---|
firstElementChild |
children[0] or null |
lastElementChild |
children[children.length-1] or null |
children |
Live collection of all child elements |
childElementCount |
children.length |
Child Element Properties | Value |
---|---|
nextElementSibling |
parentNode.children[n+1] or null |
previousElementSibling |
parentNode.children[n-1] or null |
In IE, use these methods on document.body
instead of document
.
document.getElementById(id)
finds a single Element with the specified iddocument.querySelector(selector)
returns the first Element that matches a CSS selector.document.querySelectorAll(selector)
is similar but returns all matching elements.
HTMLCollection or NodeList
- Array like objects
- HTMLCollections provide no support for
forEach
- NodeLists on some browsers do support
forEach
Searching for sub-list of elements
You can call get
and query
method on any element, document is usually the most common. Calling these on elements down a specific DOM branch will restrict the results.
Method | Description | Returns |
---|---|---|
getAttribute(name) |
Retrieve value of attribute name |
Value of attribute as String |
setAttribute(name, newValue) |
Set value of attribute name to newValue |
undefined |
hasAttribute(name) |
Check whether element has attribute name | true or false |
> p.hasAttribute('class'); // true
> p.getAttribute('class'); // "intro"
> p.getAttribute('id'); // "simple"
> p.setAttribute('id', 'complex'); // <p class="intro" id="complex">…</p>
JavaScript exposes these certain attributes as gettable and settable properties of the DOM Element: id
, name
, title
, and value
p.id; // "complex
p.className // "intro"
JavaScript doesn't provide all these properties on every Element
type: the name
and value
attributes, in particular, are invalid on most elements.
Understanding attribute properties is helpful when working with the href
attribute.
If you have selected an a
tag element and try to access it's href
property, most browers will supply the host and port and well.
console.log(e.target.href) // http://localhost:3000/checkout
Say you were trying to test if you had a relative link - this wouldn't make things easier.
However using getAttribute(href)
gets you the href content, which is more helpful.
> console.log(e.target.getAttribute(href))
< /checkout
Incidentally, pathname
seems like will also work here as it also gives you /checkout
. Be careful though if you were checking for a leading /
to see if the link was relative. An href like this, href="#"
, will return a pathname of /
.
jQuery makes this process a bit easier, allowing you do to something like this a[href=^'/']
.
Working with the class
attribute via className
is inconvenient when elements have more than one class.
Name | Description |
---|---|
add(name) |
Add class name to element |
remove(name) |
Remove class name from element |
toggle(name) |
Add class name to element if it doesn't exist, remove if it does exist. |
contains(name) |
Return true or false depending on whether element has class name. |
length |
The number of classes to which element belongs. |
document.getElementById('primary_heading').classList.add('heading')
The style
attribute on an Element
references a CSSStyleDeclaration
Object:
h1.style; // CSSStyleDeclaration {alignContent: "", alignItems: "",
h1.style.color = 'red';
Setting RGB vs. hex
container.style.backgroundColor = '#ff1212';
container.style.backgroundColor = "rgb(255,0,0)";
Removing a CSS property
To remove a CSS property*, set the property to null
with the style
property
h1.style.color = null;
-
- I'm not sure this is possible with jQuery :)
Dasherized properties
Use camel case instead line-height
vs. lineHeight
Element properties do not include textNodes
, so use textContent
to element text.
<p>
You can <a href="?page=2">step backward</a> or <a href="/page/3">continue</a>.
</p>
var p = document.querySelector('p');
p.textContent // \n You can step backward or continue.\n
p.textContent = "Hey";
p.textContent; // "Hey
Be careful, because this removes all child nodes
<p>
Hey
</p>
Therefore wrapping editable sections in span
or div
is safer.
Node Creation Method | Returns |
---|---|
document.createElement(tagName) |
A new Element node |
document.createTextNode(text) |
A new Text node |
node.cloneNode(deepClone) |
Returns a copy of node , or all children if deepClone is true |
Parent Node Method | Description |
---|---|
parent.appendChild(node) |
Append node to the end of parent.childNodes |
parent.insertBefore(node, targetNode) |
Insert node into parent.childNodes before targetNode |
parent.replaceChild(node, targetNode) |
Remove targetNode from parent.childNodes and insert node at its former position |
document.createElement('P')
paragraph.textContent = 'This is a test.';
document.body.appendChild(paragraph);
- No Node can appear twice in the DOM.
- You can't call
document.appendChild
; it causes an error. Usedocument.body.appendChild
instead.
Element Insertion Method | Description |
---|---|
element.insertAdjacentElement(position, newElement) |
Inserts newElement at position relative to element |
element.insertAdjacentText(position, text) |
Inserts Text node containing text at position relative to element |
position
position
must be one of the following String values:
Position | Description |
---|---|
"beforebegin" | Before the element |
"afterbegin" | Before the first child of the element |
"beforeend" | After the last child of the element |
"afterend" | After the element |
node.remove()
and parent.removeChild(node)
remove nodes from the DOM.
Loading JS
window.onload = function() {}
Selecting elements
If you need to select an element by id do
document.getElementById()
However you can write your own jQuery-esque selector
function $(id_selector) {
return document.getElementById(id_selector);
}
Asynchronous code
setTimeout(callback, delay)
- used to invoke a Function after the specified number of milliseconds.setInterval(callback, delay)
- used to repeatedly invoke a Function, returns an id.clearInterval(id)
can be used to prevent future invocations of the Function.
Event - an object that represents some occurrence and contains a variety of information about what and where it happened. Events can be triggered by the browser as it loads a page, by a user as they interact with the page, and by the browser as it performs work as directed by a program.
Event listener - callbacks that will be invoked whenever a matching event is detected (The code that is run in response to an event firing)
Setting up an event listener
- Identify the event you are interested in. This can be an event caused by a user action, page lifecycle, and more.
- Identify what element the event will occur on. Depending on the event, the object could be a button, the page, or anything in between.
- Define a Function for the event . The function should be called when this event occurs. The Function will receive a single argument, which will be an
Event
object. - Register the Function as an event listener. This is where the first three steps come together into a working system.
element.addEventListener
or GlobalEventHandlers like element.onclick
are used to register listeners.
GlobalEventHandler
<button onclick="console.log('clicked', event)">
Click me
</button>
Notes:
- You must refer to the event as
event
. Passing in ane
will not work. - You can also technically pass in
this
to grab the target element, butevent.target
is probably more standard - You can also use an IIFE (but it still need to use
event
)
<button onclick="(function(that) {
console.log(event.target); // <button onClick="(function) { ... }"
console.log(that === event.target); // true
})(this)">
Click me
</button>
addEventListener
document.addEventListener('click',
function(e) { console.log('clicked!', e) };
);
Event listeners must be removed according to the approach by which they were added to the node.
addEventListener
For event listeners registered with addEventListener
, the DOM node method removeEventListener
must be used:
eventTarget.removeEventListener(eventType, eventHandler);
Following this pattern, we would remove the event listener we set above as follows:
document.removeEventListener('click', handler);
It's important to note that, because handler must refer to the same Function object used when it was registered, an event listener added with an anonymous function expression cannot be removed with removeEventListener
.
GlobalEventHandler
The object-property style of GlobalEventHandler makes removing an event listener added with this approach somewhat easier: simply set the desired eventProperty to undefined
.
eventTarget.eventProperty = undefined;
Following this pattern, we would remove the event listener set above like this:
document.onclick = undefined;
DOMContentLoaded
Code that needs access to the DOM should be invoked after the DOMContentLoaded
event is fired on document. For example, adding an event listener to one or more page elements.
// https://codepen.io/dylankb/pen/PmazLp
document.addEventListener('DOMContentLoaded', function() {
document.querySelector('.button-container').addEventListener('click', logger);
});
Like DOMContentLoaded, but not
Another, non event-driven way to execute JS after the DOMContentLoaded
loaded event fires would be to include it in a script tag just before the closing </body>
tag.
load
event - fires after all assets are loaded
Event Type | Example Events |
---|---|
Keyboard | keydown, keyup, keypress |
Mouse | mouseenter, mouseleave, mousedown, mouseup, click |
Touch | touchdown, touchup, touchmove |
Window | scroll, resize |
Form | submit |
Property | Description |
---|---|
button | Which button was pressed (null for events unrelated to mouse clicks) |
clientX | The horizontal position of the mouse when the event occured, relative to the visible area of the page. |
clientY | The vertical position of the mouse when the event occured, relative to the visible area of the page. |
Property | Description |
---|---|
which | The ASCII key code, which is a Number that specifies the key that was pressed. |
key | The String value of the pressed key. Not supported in Safari. |
shiftKey | Boolean value indicating if the shift key was pressed. |
altKey | Boolean value indicating if the alt (or option) key was pressed. |
ctrlKey | Boolean value indicating if the control key was pressed. |
metaKey | Boolean value indicating if the meta (or command) key was pressed. |
keypress
event doesn't fire when certain keys are pressed, such as Shift and the other modifier keys
More events here
Property | Description |
---|---|
type |
The type of event. |
currentTarget |
The object that is currently being targeted as the event bubbles up the DOM. This will be the object the event handler was attached to. |
target |
The object the event originally was fired upon. |
Event phases
There are three phases to events being fired: capturing, target, and bubbling.
By default, event listeners will listen for events that are triggered during the target and bubbling phases
- Capturing - The browser starts at the outermost element, which will be the
window
, and fires the event on each element it encounters on its way down to the target element. - Target - fires on the target element
- Bubbling - (Reverse of capturing) event is fired on each parent element , working through containing elements until the window is reached.
Controlling default behavior
event.preventDefault()
- used to prevent default browser behavior in response to an event- Submit event browser reset! - If you don't
preventDefault
when a form is submitted, the whole browser will refresh which acts can make you think something unrelated is wrong with your app :S
- Submit event browser reset! - If you don't
event.stopPropagation()
- stops the event from being triggered on other containing or contained elements
It is a good practice to call preventDefault()
or stopPropagation()
as early as possible in an event handler to show clear intent.
Event delegation - a technique used to handle events triggered by multiple elements using a single event handler.
Benefits of event delegation
- Don't have to wait for DOMContentLoaded event - reduces timing complexity
- New elements will have listeners
- Reduces number of event handlers => increases performance
You can do this as opposed to adding an event listener to every button on the page.
document.addEventListener('click', function(event) {
var target = event.target;
if (target.tagName === 'BUTTON') {
var text = target.textContent;
alert('Button ' + text + ' was clicked.');
}
});
Cons
- Code that has to handle multiple situations will become more complicated
function delayLog() {
for (var i = 1; i <= 10; i += 1) {
setTimeout(function(){
console.log(i);
}, i * 1000);
}
}
delayLog();
11
11
// ... logs 8 more times
JavaScript runtime has to finish executing one piece of code before it goes on to execute other code like the function you pass to setTimeout
. A piece of code or a function that has to be executed asynchronously is stacked up in something called the event loop. So the for
loop has to finish before your anonymous function gets called even if the timeout is 0 seconds.
After the loop is finished and after the given time out the anonymous functions are executed one by one. However since every anonymous function refers to the variable i
via closure, and the value of i
is now 11
, 11 is logged ten times.
There are two main ways to stop that from happening:
One is to make a new scope so that a new variable is declared in each iteration.
for (var i = 1; i <= 10; i += 1) {
(function(j) {
setTimeout(function(){
console.log(j);
}, 0);
})(i);
}
The other solution is based on the ES6 let
keyword. If you use let
instead of var
in your for
loop, what effectively happens is that in every iteration the variable i
is redefined for you.
for (let i = 1; i <= 10; i += 1) {
setTimeout(function(){
console.log(i);
}, 0);
}
Use the XMLHttpRequest object to send a HTTP request with JavaScript.
var request = new XMLHttpRequest(); // Instantiate new XMLHttpRequest object
request.open('GET', '/path'); // Set HTTP method and URL on request
request.send(); // Send request
Method | Description |
---|---|
open(method, url) |
Open a connection to url using method. |
send(data) |
Send the request, optionally sending along data. |
setRequestHeader(header, value) |
Set HTTP header to value. |
abort() |
Cancel an active request. |
getResponseHeader(header) |
Return the response's value for header. |
Property | Writable | Default Value | Description |
---|---|---|---|
timeout |
Yes | Maximum time a request can take to complete (in milliseconds) | |
readyState |
No | What state the request is in (see below) | |
responseText |
No | null |
Raw text of the response's body. |
response |
No | null |
Parsed content of response, not meaningful in all situations |
location.hash
- URL params after #
location.pathname
- current root url + dir paths
e.originalState.state
? - not sure what data structure is
Example:
// $('windows').on('popstate', function(e) { // Commented out due to markdown syntax problem https://github.com/atom/language-gfm/issues/21#issuecomment-299510304
var state = e.originalEvent.state;
if (location.hash) { switchPage(location.hash) }
)};
var thing = thing.slice() // ES5
new Array(3).join('x') // ES5
'x'.repeat(3) // ES6
First, you could append .split()
to each of the above two string creation examples.
// ES5 - If you wanted some flexibility
Array.apply(null, new Array(2)).map(function (x) { return 'x' });
// ES5 - more terse example
new Array(2).fill('x');
For example you can't map over new Array(2)
since it is an array of 2 elements without pointers. [undefined, undefined]
is an array of 2 elements with pointers to undefined
.
Array.apply(null, new Array(2)) // ES5
new Array(2).fill() // ES5
[...new Array(2)] // ES6
You could map over the result and collect the index to create a 1..N array of numbers.
new Array(3).join('x').split('').map(function(x, i) { return i }); // ES5
'x'.repeat(3).split('').map((x, i) => i); // ES6
> document.toString() // "[object HTMLDocument]"
console.table()
- displays an object in a table. (even table()
works)
console.dir()
- displays a tree expandable object
copy()
- copies to your clipboard
$(element)[0]
- returns the DOM element
inspect(DOM element)
- opens up the inspector to that element
values(object) / keys(object)
- keys/values for an object
$$(selector)
- array of dom elements (works w/out jQuery)
$_
- last returned value (in Chrome)
$0
- last selected element (at least in Chrom)
getEventListeners(obj)
- object of event listeners on element
- Check console for load issues
- Turn on exception pauses in the Sources tab
- Alerts debugging
- Enter
debugger;
into the script and debug from console - Enter
console.log(someVariable)
and debug from console - Nested breakpoints: Create a breakpoint/enter a debugger and disable breakpoints (tab icon in devtools) until, for example, the sixth iteration. Then re-enable them. Beats clicking continue X number of times.
- Node debugging - set
debugger
and runnode debug script.js
- Guard clauses
Use when you can't trust input
- Consider edge cases
- Error catching
- Simple guard clause is not possible
- Error could be thrown by built-in JS method
$(document).ready(function() {
// DOM is now loaded
});
$(window).load(function() {
// DOM is loaded, img tags are loaded
});
Shortcut for loading the dom
$(function() {
// DOM is now loaded
});
As long as a jQuery object is returned, you can keep chaining additional jQuery methods.
$ul.append($("<li />").text('1'));
<ul><li>1</li></ul>
Not chaining
$ul.append(('<li></li>').text('1'));
An error is thrown "<li></li>".text is not a function
, so nothing is appended.
<ul></ul>
You can create an HTML element by supplying the html as the first argument, and then an object of html attributes and/or inline CSS properties
var $element = $("<div />", {
"class": 'shapes ' + data.shapeType,
data: data,
css {
left: +data.startX,
top: +data.startY
}
});
Action | jQuery | JS |
---|---|---|
Select elements by id | $('#ID) |
document.getElementById("ID") |
Add class | $element.css('class', 'some-class') |
element.setAttribute("class", "some-class"); |
Add class (ii) | $element.addClass('some-class') |
element.classList.add("some-class"); |
Add other attribute | $element.attr("alt", "string") |
element.setAttribute('alt', 'string') |
Remove attribute | $element.removeAttr("style") |
element.removeAttribute("style") |
Get class | $element.attr('class') |
element.getAttribute("class") |
Get text | $element.text() |
element.innerHTML /textContent /innerText |
On submit | `$form.on("submit", function(e) { | form.onsubmit = function(e) {` |
Get value | $element.val() |
element.value |
Accessing jQuery vs. DOM objects
- Use
eq
to access the jQuery object - Use
[]
orget()
to retrieve the DOM element
var content = $('li');
content[0]
> <li>…</li>
content[0].jquery
> undefined
content.eq(2)
> [li]
content.eq(2).jquery
> "1.11.2"
- Generally uses CSS selector rules, but some jQuery specific ones
- Selectors inside
[]
are element attribute selectors (class
,data-id
, etc.)
("li").filter("[data-id]")
Multiple selector - $('.class', '#id', ...)
- Can selector multiple different, non-related elements
jQuery specific pseudo-selector
:contains
:odd
:text
- input elements type text:even
- zero based index:input
- includes textarea, button, etc., not just<input>
:checked
You can select certain items in a collection using the filter
method and passing in a valid CSS selector or jQuery psuedo-selector
$('li).filter('.even')
The return value is a jQuery collection, so it is of course chainable
$('article li li').filter(":contains('ac ante')").next();
Negation
$('td').not('.protected');
Find (specific) parent
p.parent(".highlight")
Fuzzy match
$('[class*=block]');
Selects an element if the selector's string appears anywhere within the element's attribute value
Starts
$('[id^=blind]')
Grabs all elements with an ID that begins with 'blind'.
Ends with
$('[id$=blind]')
Grabs all elements with an ID that ends with 'blind'.
closest
- Begins with the current element, and travels up the DOM tree until it finds a match for the supplied selector
parents
- Travels up the DOM tree until it finds matches for the supplied selector
<ul class="level-1">
<li class="item-i">I</li>
<li class="item-ii">II
<ul class="level-2">
<li class="item-a">A</li>
</ul>
</ul>
Examples:
$( "li.item-a" ).closest( "ul" ) // [ul.level-2])
closest
travels up DOM tree to find first matching element
$( "li.item-a" ).parents( "ul" ) // [ul.level-2, ul.level-1])
closest
travels up DOM tree to find parent elements
$( "li.item-a" ).closest( "li" ) // [li.item-a])
closest
searches by beginning with the current element, and therefore it returns itself as the result.
$( "li.item-a" ).parents( "li" ) // [li.item-ii]
.parents()
does not search the current element, so it returns the next matching element(s) up the DOM tree
index()
can be used in several ways. Refer to the examples below:
<p>Hey</p>
<p>You</p>
<p>There</p>
<div class="parent">
<span>Hey</span>
<p class="child">Child</p>
<p>class</p>
</div>
$element.index('selector')
- Element's index relative to other elements matched by the selector
`$('p.child').index('p'); // => 3`
Index of p.child
tag compared to all other p
tags in the document
$element.index()
- Index of$element
relative to other siblings
var $firstP = $('div.parent').find('p').eq(0); // Get first p tag in .parent - p.child
$first.index() // => 1
Index of p
tag in relation to all siblings (span
, etc.)
$collection.index($element)
- Index of$element
relative to collection elements
$('div.parent').find('p').index('p.child') // => 0 OR
$('div.parent p').index('p.child') // => 0
Index of p.child
relative to other p
elements in the collection (relative to other p
elements inside parent
)
A modified version of this last one is useful for finding the element's index you clicked on relative to the parent element and by element type. Using jQuery it might look something like this:
$(div.parent p').index($(this))
Iterates over a collection, setting this
to the current DOM element (not a jQuery object)
<ul>
<li data-id="1">Apple</li>
<li data-id="2">Apricot</li>
<li data-id="3">Banana</li>
</ul>
DOM vs jQuery element
each()
returns a DOM element and therefore you would have do something like this to get the data-id
attribute value
$("li").filter("[data-id]")[0].attributes[0].value // "1"
or
$("li").filter("[data-id]")[0].dataset.id
To make accessing attr
values a little bit easier using jQuery, pass the element into a jQuery object using $()
$("li").filter("[data-id]").each(function(index) {
console.log( "Index: " + index + " data-id: " + $(this).attr("data-id")); // data properties can also use $(this).data('id');
});
// Index: 0 data-id: 1
// Index: 1 data-id: 2
// Index: 2 data-id: 3
// [li, li, li]
- Return item by passed in index as jQuery object.
- Negative indexes work similar to Ruby (returns item n.length - 1)
All input
elements return value from the .val()
method.
$("form#some-form").submit(function(event) {
var someInput = $("input#some-input").val()
event.preventDefault();
});
Like many jQuery methods, it's also a setter
The DOM form element has a reset method. Accessing it from a jQuery method looks like this
$form.get(0).reset();
or $form[0]
The get
method is useful if you need to get a specific DOM element in a collection $('li').get(-1)
If you were in the element's context, you would already have access to the DOM element and could access it like this
this.reset();
This resets the form's input values to their values on page load. So if we had an input with a default value of 1, <input type="text" value="1">
, reset()
would provide a "1"
for that form field.
$('form').serializeArray();
requires name
attribute on input
elements
$(:radio).eq(0).prop(checked, true)
You could use attr
, but checked isn't an attribute of input
element - it's a property.
nextAll()
- grab all of the sibling elements that come after the current one
prevAll()
- grab all of the sibiling elements prior to the current one
position()
- relative to nearest parent with position: relative
- will not reflect other position-influencing factors (margin)
offset()
- offset from 0,0 of the window
height()
- element content dimensions
innerHeight()
- includes padding (not border)
outerHeight()
- optionally can include margin
- includes padding and border
box-sizing
jQuery dimensions vs. CSS dimenstions may be different because jQuery takes into account box-sizing
- Can set multiple CSS properties at once
- Needs dasherized properties as cameCased (
font-size
=>fontSize
)
When using the CSS convenience method be sure to convert your values into Number
type. Like so:
function resetElement($e) {
var data = $e.data();
$e.css({
top: +data.startX,
right: +data.startX,
});
}
Without this it won't treat the css as a px
, just a string
.
Retrieves current position of scroll window.
Example below sets element top
value to current screen position + 30px
$ele.css({
top: $(window).scrollTop() + 30
})
Inserts the specified content as the first child of each element in the jQuery collection
contentToPrepend.prependTo(matchingElementsToBePrependedTo)
containerToPreprendTo.prepend(matchingOrSuppliedContentToPrepend)
Besides syntax order, the main difference is that each method allows for multiple elements or an array to be supplied to the parameter. This means that prependTo
can prepend content to multiple target arguments*,
$("<p>Prepend Test</p>").prependTo([".inner", ".inner-last"]);
and prepend
can prepend multiple content arguments*
$( ".inner" ).prepend( "<p>Prepend Test</p>", "<p>Also</p>" );
Multiple argument format
prependTo()
requires multiple arguments to be passed in as a single array. prepend
can take an array or multiple arguments.
* - A jQuery selector can select multiple elements, I was just emphasizing multiple selectors or arguments could be passed to each. This is also a slightly untrue in the case of ... ?
Using pure jQuery, one way to close a modal you would first filter for visibile modals, then hide or fade those modals out while revealing the newly clicked one.
Hide all other tabs besides the first
ele + ele { display: none }
On click, hide all tabs and display the one you clicked on
//$('parent').on('click', 'a', function(e) { // Commented out because of problem w/ syntax highlighting https://github.com/atom/language-gfm/issues/21#issuecomment-299510304
e.preventDefault();
var $e = $(e.target),
idx = $e.attr('href');
$('ele').hide().filter(idx).show();
}
You might also want to toggle the active class
...
$('ele').removeClass('active');
$e.addClass('active');
Animations are chainable methods
- Can be called without arguments (except
fadeTo
) - Default 400 (millisecond) duration
- Two speed keywords: "slow" (600ms) and "fast" (200ms)
- Optionally take a callback that executes on animation completion
$p.fadeOut();
$p.fadeIn(250, function() {
$(this).addClass('visible'); // animating item (context) not a jQuery object, so using $()
});
$p.fadeToggle();
$p.fadeTo(400, .5);
$p.slideDown();
$p.slideUp(250);
$p.slideToggle(400, function() {
console.log('Sliding complete!');
});
- Can take
linear
as an argument to change animation style - Can take properties instead of arguments
$p.slideToggle({
duration: 400,
easing: 'linear',
complete: function() {
console.log('Sliding complete!');
},
});
- First argument - Object that represents the CSS properties to be animated and what values to end on for each.
- Second argument - duration, then the optional easing method, then the callback.
- Third arg - callback on completion
$p.animate({
left: 500,
top: 250
}, 400, function() {
$(this).text('All done!');
});
Two objects way
- First arg - Same CSS object
- Second arg - Duration, easing, and callback
$p.animate({
left: 500,
top: 250
}, {
duration: 1000,
complete: function() {
$(this).text('All done!');
}
});
Delay animation
.delay()
- Useful for delaying between different animations - $p.slideUp(250).delay(500).slideDown(250);
Useful for controlling animations queue.
stop()
- Stop current animations and starts next one. Passtrue
to stop all in current/future queue. Pass secondtrue
to skip to last frame.
I.e, you have a menu with fadeIn
and fadeOut
animations and a users skips over many of them frequently
$p.stop().fadeToggle(200);
.finish()
- Equivalent to runningstop(true, true)
for each animation.
Stoping all animations
Set $.fx.off
to true
. You can do this on other sites.
jQuery DOM interactions
Some examples below:
blur
- clicking out of a form field
An example of why we need pay attention to under which event a piece of code fires.
This does not highlight green elements as they appear:
$(document).ready(function() {
$("button#hello").click(function() {
$("ul#user").prepend("<li>Hello!</li>");
});
$('li').css('background-color', 'green');
});
$(document).ready
looks at the page when it's initially loaded, not on each click. When the page loads there are no li
(we're adding them via jQuery on each click).
Adding this to a click action will work
$(document).ready(function() {
$("button#hello").click(function() {
$("ul#user").prepend("<li>Hello!</li>");
$('li').css('background-color', 'green');
});
});
.submit()
- Can only be attached to a form
- The event callback context (
this
) is the DOM element - The event is an optional parameter
event.currentTarget === this
function highlight(e) {
$(this).toggleClass("highlight");
console.log(e.target === this); // true
}
`$('.simple a').on("click", highlight)`;
If you want to use the this
keyword in the event handlers, it’s good to bind it explicitly.
Sometimes e.currentTarget === e.target
The handler is not called when the event occurs directly on the bound element (the element calling the .on()
method, but only for descendants (inner elements) that match the selector (the selector passed into the on()
method call.
jQuery bubbles the event from the event target up to the element where the handler is attached (i.e., innermost to outermost element) and runs the handler for any elements along that path matching the selector.
Dynamically creating new elements with jQuery can cause a problem when the newly created events need to have associated event handlers.
<p><a class="simple" href="#">A simple click event</a></p>
function highlight() { ... }
$('.simple a').on("click", highlight);
$('.simple').filter("a").clone().appendTo($("#clone"));
}
The second line of JS would create a new DOM element, but it would not have a click event bound to it, and therefore we would not be able to trigger the highlight function.
Delegated events have the advantage that they can process events from descendant elements that are added to the document at a later time. Below is an example of how to solve this issue:
<div id="example">
<p><a href="#">A delegated click event</a></p>
</div>
`$('.example').on("click", "a", highlight)`;
`$('.example').filter("a").clone().appendTo($('#clone"));
}
The example above uses event delegation to all a
tags nested in .example
, and would work.
this
- Directly bound events -
this
is the element where the event was attached - Delegated events -
this
is the element which matches the selector
$('section > header').on('click', ".actions a", function(e) { ... }
In this case, this
inside the on
callback would be the a
tag.
e.currentTarget vs. e.target
Typically, e.currentTarget
his property will be equal to the this
of the function.
Benefits
- Opportunity to reduce code duplification
like
and favorite
do very similar things, with only the url being different
like: function(e) {
e.preventDefault();
// $.ajax({ // Commented out because of problem w/ syntax highlighting https://github.com/atom/language-gfm/issues/21#issuecomment-299510304
url: "/photos/like",
type: "POST",
context: this,
data: "photo_id=" + photosJSON[currentIndex].id,
success: function(json) {
photosJSON[currentIndex].likes = json.total;
this.renderPhotoInformation(currentIndex);
}
});
},
favorite: function(e) {
e.preventDefault();
// $.ajax({
url: "/photos/favorite",
type: "POST",
...
get: function(e) {
e.preventDefault();
//var $e = $(e.target); // Commented out because of problem w/ syntax highlighting https://github.com/atom/language-gfm/issues/21#issuecomment-299510304
// $.ajax({
url: $e.attr("href"),
type: "POST",
data: "photo_id=" + $e.attr("data-id"),
}).done(function(json) {
var socialTypePlural = this.url.substring(this.url.lastIndexOf('/') + 1) + "s"; // returns either favorites or likes
photosJSON[currentIndex][socialTypePlural] = json.total;
$('section > header').html(templates.photo_information(photosJSON[currentIndex]));
});
}
Creating an event hanlder for many elements is a bit expensive.
<ul>
<li>
<p>Bananas</p>
<a href="#">Remove</a>
</li>
<!-- 29 more list items, each with a remove anchor -->
</ul>
// This callback is bound to each anchor, making 30 event handlers in memory
$('a').on('click', function(e) {
e.preventDefault();
$(this).closest('li').remove();
});
Using delegated events can help
$('ul').on('click', 'a', function(e) {
e.preventDefault();
$(e.target).closest('li').remove();
});
This uses one event listener that checks for anchor clicks
Open external links in a new window
$("#list" ).on( "click", "a[href^='http']", function( event ) {
$( this ).attr( "target", "_blank" );
});
Note
The context of a delegated event callback (this
) will be the parent element. In the above example that would be #list
. The event will be the actual element acted upon, so you can get the specific element through something like e.target
.
You may have multiple click events bound to an element, but you only want one of them fired. Calling this $(this).off
would remove all events.
$("#namespaced").on("click", function(e) {
alert("Removing all events");
$(this).off("click");
});
$("#namespaced").on("click", highlight);
However you can use namespacing to only remove certain events. Below, the alert
event will now only fire once and the highlight
functionality will remain
$("#namespaced").on("click.alert", function(e) {
alert("Now removing only the alert event");
$(this).off("click.alert");
});
If you wanted to remove mutliple different events with the same namespace, you could leave off the event type
// $("#namespaced").on("click.alert" ... // Commented out because of problem w/ syntax highlighting https://github.com/atom/language-gfm/issues/21#issuecomment-299510304
...
$(this).off(".alert");
If you did not want to use namespacing at all you could also could specify the handler using the event from the callback
$("#namespaced").on("click", function(e) {
alert("Now removing only the alert event");
$(this).off(e);
});
It's often a good idea to remove previously bound events when creating new ones
$(document).off("keypress").on("keypress", function(e) {
It's common to want to stop the click event action from happening
// $('a').on('click', function(e) { // Commented out because of problem w/ syntax highlighting https://github.com/atom/language-gfm/issues/21#issuecomment-299510304
e.preventDefault();
The event will continue to travel up the DOM tree, and might travel on a path that looks similar to this
<a>
<li>
<ul #list>
<div #container>
<body>
<html>
document root
To stop the event from propogating further you can call e.stopPropogation()
If you wanted to prevent default and stop propogation, you could return false from the function. Here's an example of doing so in a one-liner.
$('a').on('click', false)
This method executes a click event on page load.
<ul>
<li><a class="drawing-method">Circle</a></li>
<li><a class="drawing-method">Square</a></li>
</ul>
$('a.drawing-method').on('click', function(e) {
e.preventDefault();
$(this).closest("ul").find('.active').removeClass('.active');
$(this).addClass('active')
}).eq(0).click();
on()
returns a jQuery object, which is a collection of a
tags in this case. The default event fires by selecting the first tag with eq
and manually triggering a click
event.
Takes a function and returns a new one that will always have a particular context.
Required
- url
success
method ORdone
chained function
Other:
- type (default is GET)
- data (appended as query param)
To get a better high level understanding of the code it's often helpful to organize code into objects.
Say you have a comments
object with a method getCommentsFor
that returns the comments for a specified object. If we want to take advantage of other methods defined on the comments
object inside the success callback we'll have to manually set the context.
var comments = {
getCommentsFor: function(id) {
// $.ajax({ // Commented out because of problem w/ syntax highlighting https://github.com/atom/language-gfm/issues/21#issuecomment-299510304
url: "/comments",
data: "photo_id=" + id,
context: this,
success: function(commentJSON) {
this.display(commentJSON);
this.bindEvent(); // Ensure that binding occurs after successful rendering of content
}
});
},
display: function () { ... },
By default the context will be a response object, and we need to set the context to the comments
object. Therefore we use the context
setting, set to this
. Now when comments.getCommentsFor
is called the context will comments
within the callback.
If an object method containing an Ajax call is bound to an event, there is some added complication
- the method's context must be explicitly bound to
this
- the ajax call must use
this
as thecontext
setting
var socialInfo = {
bindEvents: function() {
$('section > header').on('click', ".actions a", socialInfo.get.bind(this));
},
renderPhotoInformation: function(index) { ... },
get: function(e) {
e.preventDefault();
$.ajax({
url: $e.attr("href"),
type: "POST",
data: "photo_id=" + $e.attr("data-id"),
// comments methods and properties copied to the event,
// which will be the callback context
context: $.extend(e, this),
}).done(function(json) {
var $e = $(this.target);
var url = $e.attr("href");
// returns either favorites or likes
var socialTypePlural = url.substring(url.lastIndexOf('/') + 1) + "s";
photosJSON[currentIndex][socialTypePlural] = json.total;
this.renderPhotoInformation(currentIndex);
});
}
};
If you need to access the DOM element in the success callback as well, then you need to extend either the comments
object to include DOM event data or vice versa.
Prepend variables with $
to denote it's a jQuery object.
Add names to document ready functions
Helpful if you are using multiple large functions.
$(function calculateStudentInterests() ... });
If efficiency is important for you, then this can be a good thing to do. Also, more function calls means slower code execution and a more tangled debugging process.
last()
var $lis = $("li");
$lis.last().addClass("last");
last
is a convenience call to eq(-1)
- Using
eq
may be less function calls eq
may not be as semantically clear aslast()
- Toggle classes, not CSS, in your jQuery JS
- Make use of
toggleClass
(rather than add/remove class)
- Store selectors as variables - don't search again.
- Make use of
find
- If you do something once you don't need a variable, butfind
is faster than a new$()
DOM search.
Loading issues
- Check if jQuery is working in the browser console
$('body')
This should return something
- Always link any scripts files that use jQuery after you link the jQuery library itself.
<script src="js/jquery-1.12.4.js"></script>
<script src="js/scripts.js"></script>
jQuery
Use the .attr()
method. As a setter method, attr()
will change the HTML markup.
JS
attributes
- e.target.attributes['data-id'].value
dataset
- e.target.dataset.id
data()
vs. attr()
.data()
- As a setter method, data()
will store the value on the node that can be retrieved with the data()
method as a getter, but it will not update the HTML markup.
attr()
- As a setter method, it will modify the page's HTML.
<article data-block="gold">
<h1>Gold Sponsors</h1>
</article>
<article data-block="silver">
<h1>Silver Sponsors</h1>
</article>
<article data-block="bronze">
<h1>Bronze Sponsors</h1>
</article>
Note how attr()
require a bit more verbose syntax.
<a href="#" data-block="gold">Gold Sponsors</a>
var $a = $('a[data-block=gold]');
console.log($a.attr('data-block')); // gold
console.log($a.data('block')); // gold
$a.data('block', 'silver');
console.log($a.attr('data-block')); // gold
console.log($a.data('block')); // silver
Data properties with hyphens are formatted as camel-cased, and therefore should be accessed this way using vanilla JS.
<div id="div" data-date-of-birth="28-02-1981"></div>
var date = document.getElementById('div');
console.log(date.dataset.dateOfBirth);
However using jQuery this is not necessary.
console.log($(date).data('date-of-birth'));
console.log($(date).attr('data-date-of-birth'));
Basic use case
- Compile HTML from script tag into Handlebars function (stored as a variable)
- Render template with context JSON
<!-- /test.html -->
<body>
<script id="itemsTemplate" type="text/x-handlebars">
{{#each items}}
<li>{{.}}</li>
{{/each}}
</script>
<main>
<ul id="list">
</ul>
</main>
</body>
// /test.js
// Compile Handlebars function
var itemsTemplate = Handlebars.compile($('#itemsTemplate').html());
// Render template with context
var products = ['Apple', 'Banana'];
$('#list').html(itemsTemplate({ items: products }))
Partial
- Register partial
- Render template with JSON context
<!-- /test.html -->
<body>
<ul id="list"></ul>
<script id="itemsTemplate" type="text/x-handlebars">
{{#each items}}
{{> basicTemplate}}
{{/each}}
</script>
<script id="basicTemplate" type="text/x-handlebars">
<li>{{.}}</li>
</script>
...
// /test.js
...
// Handlebars.registerPartial('basicTemplate, $('#basicTemplate').html()); // Commented out because of problem w/ syntax highlighting https://github.com/atom/language-gfm/issues/21#issuecomment-299510304
$('#list').html(itemsTemplate({ items: products }));
Checking for a partial
Type Handlebars.partials
into the console to see registered partials
You could render an object item
using a partial named item
like so:
Handlebars.partials.item(item.toJSON()));
Handlebars keywords
>
- tells Handlebars to look for a partial with the name basicTemplate
@key
- current key of iteration
@index
- current index of iteration
{{.}}
- equivalent to {{this}}
(current object in iteration)
A template could also just be a string of Handlebars code that's then compiled to a function instead of a script tag.
var Handlebars = require('handlebars');
var template = '{{#each foo}}\n{{@key}}:{{.}}!\n{{/each}}';
var render = Handlebars.compile(template);
var data = {
"foo": {
"first": "Hello",
"second": "World"
}
};
console.log(render(data));
// first:Hello!
// second:World!
Compilation is the most expensive part of Handlebars, so we can do it ahead of time.
npm install handlebars -g
- Follow the rest of these instructions for how to precompile your templates here
Handy command:
handlebars templates/ -f fileNameOfCompiledTemplates.js
Localstorage
- No expiration - on desktop may remain indefinitely (mobile safari may delete)
- Up to 5MB
- Data as key-value pairs
Cookies
- Has expiration date
- Holds 4KB
- Data stored as a string
window.sessionStorage
- only lasts the current session (tab / window closed)
window.localStorage
- stays around after the browser window or tab closes
setItem()
localStorage.setItem(keyString, valueString)
getItem()
localStorage.setItem(keyString, valueString)
Dealing with Objects/Strings
Non-string values will have toString()
called on them. So how do you save objects? The solution is to use JSON.parse
and JSON.stringify
When setting a value, pass the JS object into JSON.stringify()
. When getting a value use JSON.parse
which takes a string of JSON and converts it back to a JavaScript object.
var person = {
name: 'Amanda Rose',
bgColor: '#ff0000'
};
function setPerson(personToSave) {
localStorage.setItem('person', JSON.stringify(personToSave));
}
function getPerson() {
return JSON.parse(localStorage.getItem('person'));
}
Save on page close
unload
event fires when the browser or tab closes.
let
instantiates block scoped variables
let a = 2;
{
let a = 3;
console.log( a ); // 3
}
console.log( a ); // 2
Here's a slightly more common example
let a = 2;
if (true) {
let b = 5;
for (let i = a; i <= b; i++) {
console.log(i); // 2, 3, 4, ...
}
// console.log(i) // ReferenceError
}
// console.log(b) // ReferenceError
The for
loop instantiating i
with let
reassigns on each iteration. var
has no such restriction to block scoping.
let a = 2;
if (true) {
var b = 5;
for (var i = a;i <= b; i++) {
console.log(i); // 2, 3, 4, ...
}
console.log(i, 'hey') // 6 'hey'
}
console.log(b) // 5
window.i // 6
for
and let
Here's a bit of unintuitive ES5 code:
var funcs = [];
for (var i = 0; i < 5; i++) {
funcs.push( function(){
console.log( i );
} );
}
funcs[3](); // 5
The problem is there is only one i
in the outer scope that was closed over.
If you use for (let i = 0; i < 5; i++) {
, let declares an i
not just for the for loop itself, but it redeclares a new i
for each iteration of the loop. That means that closures created inside the loop iteration close over those per-iteration variables the way you wouldd expect.1
The for
shorthand can obscure some of this, but here is an equivalent to a let
in a for
header.
var funcs = [];
for (var i = 0; i < 5; i++) {
let j = i;
funcs.push( function(){
console.log( j );
} );
}
funcs[3](); // 3
We are defining a let
within a block scope, and let
is a block scoped variable so it correctly closes over, making the correct i
accessible to the function.
That variable reference cannot be reassigned, but object internals can be.
const a = [1,2,3]
a.push(4) # [1,2,3,4]
const a = 42 # ReferrenceError
// ES5
var a = 5;
var objA = { a: a };
// ES6
let a = 5;
let objA = { a };
var pushValue = function(ele, array) { array.push(ele); }
const arr = []
const val = 1
const result = test(val, arr)
console.log(res); // undefined
There's no return
value, so this is expected. However in ES6 there's the concept of implicit returns.
(param1, param2, …, paramN) => { statements } (param1, param2, …, paramN) => expression // equivalent to: => { return expression; }
const pushValue = (ele, array) => array.push(ele)
// ...
console.log(res); // 1
I found this to be a bit surprising to be honest, and I can't explain why the result is an integer and not an array. Here's a link to more surprising uses, or omissions, of return keyword in arrow functions.
Destructured returns
elements.map(element => element.length); // [8, 6, 7, 9]
elements.map(({ length }) => length); // [8, 6, 7, 9]
- spread operator
Copy objects
// ES6
let objB = { b : 5 };
let objA = { a: 1 };
let c = { ...objA, ...objB }
console.log(c) // { a: 1, b: 5 }
The spread operator iterates over the properties in objA
and objB
and assigns them to the new object.
Copy + Overwrite
This is a useful tool in writing pure functions
const oldThread = state.threads[threadIndex];
const newThread = {
...oldThread,
messages: oldThread.messages.concat(newMessage),
};
...oldThread
copies all of the properties fromoldThread
tonewThread
:messages: oldThread.messages.concat(newMessage)
sets the messages property of newThread to the new messages array that contains newMessage:
Note that by having the property messages appear after oldThread, we’re effectively “overwriting” the messages property from oldThread.
Object.assign()
...
let c = Object.assign( {}, objA, objB);
Copy + Overwrite
We can do the same thing in the ES6 example with Object.assign
.
Object.assign({}, oldThread, {
messages: oldThread.messages.concat(newMessage),
});
function foo(...args) {
console.log( args );
}
foo( 1, 2, 3, 4, 5); // [1,2,3,4,5]
In ES5 apply
foo.apply( null, [1,2,3] );
Both arrays and object can assign values to named variables:
const car = { model: 'Ford' };
const { model } = car; // const model = car.model;
model; // 'Ford'
let tenses = ["I", "you", "me"]
let [ firstPerson, secondPerson ] = tenses;
firstPerson // "I"
If can't see how a function is defined, then code like this is down right confusing.
Albums.set(albums, true, false)
We can probably intuit what the albums
param is, but we have no idea what this darn false is.
ES6 brings some new features to the table that makes this situation a bit better.
Default params
You can do some type checking in ES5 to get the same affect, but it's not very clean looking. In ES6 it's very simple, just use the =
sign in the method signature.
export default {
set(albums, incrementId=true, explodeBomb=false) {...};
}
Default params / optional arguments are pretty cool, but they don't really help with making vague params like true
make sense when you see them invoked.
Destructured params
To be a bit clearer with what arguments you were passing in ES5, you could pass in an object.
var personInfo = { weight : 170, height: 72, max: 25 };
calcBmi(personInfo);
The problem was your method signature would then be less clear, or at least a bit reptitive.
function calcBmi(args) {
var weight = args.weight;
var height = args.height;
var max = args.max;
var callback = args.callback;
// the actual function ...
}
Now you can do something like this:
function calcBmi({ weight: w, height, callback, max }) {
console.log(max); // 25
console.log(w); // 170
}
Note that the number and order of the arguments are now flexible.
Options object can help provide flexible parameters to your function.
Albums.set(albums, { incrementId: true, explodeBomb: false });
However, as is you need to pass in this options object every time. ES6 gives us some additional flexibility here.
set(albums, { initializeFooToOne: true, explodeBomb: false } = {}) {...};
Now we can leave off the options object if we want.
Albums.set(albums, { explodeBomb: true });
// Albums.set(albums, { {initializeFooToOne: true }); // Commented out because of problem w/ syntax highlighting https://github.com/atom/language-gfm/issues/21#issuecomment-299510304
Albums.set(albums);
In the last three examples of set
invocations, the variable initializeFooToOne
is always true
.
Background on this technique and dealing with destructred params in ES6 here
Rest gathers individual elements together into an array
const aTail = (head, ...tail) => tail;
aTail(1, 2, 3); // [2, 3]
This is useful for removing elements
const myObject = {
a: 1,
b: 2,
c: 3
};
const { a, ...noA } = myObject;
console.log(noA); // => { b: 2, c: 3 }
https://codeburst.io/use-es2015-object-rest-operator-to-omit-properties-38a3ecffe90
Collecting components
Spread does the opposite of rest: it spreads the elements from an array to individual elements.
let num = [1]
let largerNums = [2,3]
[num, ...largerNums] // [1,2,3]
const shiftToLast = (head, ...tail) => [...tail, head];
shiftToLast(1, 2, 3); // [2, 3, 1]
Which works as long as the second argument is iterable
num = 1;
[num, ...largerNums] // [1,2,3]
let largerNum = 3;
let nums = [1,2]
[nums, ...largerNum] // largerNum is not iterable
Cloning
This technique is also useful for shallow cloning:
const moreNums = [...largerNums];
// [2,3]
Example of equivalent method calls
// do.something(function(a, b) { return a + b; } // ES5 - Commented out because of problem w/ syntax highlighting https://github.com/atom/language-gfm/issues/21#issuecomment-299510304
do.something((a,b) => { return a + b; }) // ES6 - no function
do.something((a,b) => (a + b)) // ES6 - implicit return
do.something((a,b) => a + b) // ES6 - minimum parens
More equivalent examples
[5,6].map(function(num, index) { return num + index }); // ES5
[5,6].map((num, index) => { return num + index }); // ES6 - no function
[5,6].map((num, index) => (num + index)); // ES6 - implicit return
[5,6].map((num, index) => num + index); // ES6 - minimum parens
Single argument functions can be made even terser.
[0,2,4].map((a) => { return ++a }); // ES6
[0,2,4].map(a => { return ++a }); // ES6 w/out extra parens
// [0,2,4].map((a) => (++a); // ES6 w/out return statment // Commented out because of problem w/ syntax highlighting https://github.com/atom/language-gfm/issues/21#issuecomment-299510304
[0,2,4].map(a => ++a) // ES6 single arg shorthand
Here's a nested loop example as well.
let arrays = [[1,2],[3,4]];
// ES5
// arrays.forEach(function(nums) { // Commented out because of problem w/ syntax highlighting https://github.com/atom/language-gfm/issues/21#issuecomment-299510304
// nums.forEach(function(num) {
console.log(num);
}
}
// ES6
arrays.forEach(nums => nums.forEach(num => console.log(num)) )
Function declarations still require the function
keyword, and can't make use of ES6 arrow function syntax.
function sum(a, b) => a + b;
sum(1,2) // SyntaxError
function sum(a, b) { return a + b; }
sum(1,2) // 3
Using a function expression to an anonymous function lets us take advantage of the new terser syntax.
const sum = (a, b) => a + b
sum(1,2) // 3
This code will now work without manual binding, which is convenient.
var person = {
age: 29,
intro: () => { console.log("I'm" + this.age) }
};
var intro = person.intro()
intro() // 'I'm 29'
When it is not helpful
Watch out when using the arrow syntax with something like jQuery - you may lose jQuery this
or the current element in an event handler.
Is this ES2017/ES7?
class Me {
constructor(name) {
this.name = name;
};
sayHello() {
console.log('Hi ' + this.name);
return 1;
}
}
const _Me = new Me('Dylan');
console.log(_Me.sayHello()); // 'Hi Dylan'
More examples
class Parent {
constructor() {}
bar() { console.log('bar'); }
static foo() { console.log('foo') }
}
var parent = new Parent()
parent.bar();
Parent.foo()
class Child extends Parent {
baz() { console.log('baz') }
}
var child = new Child();
child.baz()
class Bar extends Foo {
constructor(props) {
super(props)
// The rest of the implementation
}
}
Function Bar() {
Foo.apply(this);
// the rest
}
Simple example.
// myModule.js
export default {
module: 'a module',
}
// other file
import myModule from 'myModule';
Exporting a function
export function createTodo(text) {
...
}
import * as Actions from 'Actions';
A bit more complicated.
export const routeByWindowWidth = (props, nextProps, route, maxWidth) => {
const { router, windowWidth } = props;
if (nextProps.windowWidth !== windowWidth && nextProps.windowWidth < maxWidth) {
router.push(route);
}
};
import { routeByWindowWidth } from './utils/routeByWindowWidth';
Generator function which returns a promise which is thenable.
async function() {
var friends = await $.get("http://somesite.com/friends");
console.log(friends);
}
Based off an existing repo's package.json
, but it should work.
First install babel-cli
npm module, and then you should be able to execute babel through the node modules.
./node_modules/.bin/babel-node app.js
Setting up ESLint with Airbnb rules in Atom for ES5
Reference this comment to learn about ES5 linting:
Basically make sure you're:
- Installing
eslint-config-airbnb-base
- Extending
airbnb-base/legacy
in your.eslintrc
- Install package dependencies and the package itself like so with Node 5+:
npx install-peerdeps --dev eslint-config-airbnb
Other options and additional details at eslint-config-airbnb npm installation instructions.
- Install
linter-eslint
for Atom.
Notes:
Instead of using airbnb-base/legacy
you might be able to use this package, but I haven't tried it:
https://www.npmjs.com/package/eslint-config-airbnb-es5
Library syntax errors
Make sure your tests are specified in the env option. Same goes if you're using something like jQuery. tlvince/eslint-plugin-jasmine#56
CLI
Once you've done all of the above, you can also use ESLint as a CLI if you wish. Here's how you'd do that in the context of your local directory/project.
./node_modules/.bin/eslint yourfile.js
https://eslint.org/docs/user-guide/getting-started#local-installation-and-usage
You could also do a global install, but then you'd also have to do a global install of eslint-config-airbnb
, and following that pattern could get messy as you switched global ESLint configs over time and across projects.
Many ESLint setups flag param reassignment and mutation (modifying object internals). This issue even comes up when you using reduce, which IMO isn't a big issue. The problem is that ESLint has no way to recognize the reduce accumulator param as something safe to mutate.
Say you had an inputs
variable that looked like this:
[
0 : {name: "progress", value: "0.0"},
1 : {name: "level", value: "2"}
]
and you wanted to turn it into object whether the "name" value's key was "values" value. I.e
[ { "progress": "0.0" }, ... ]
This is how you'd normally do this with reduce, but ESLint doesn't like the param re-assignment that's happening (again, as an accumulator it's probably a pretty safe object to mutate).
return inputs.reduce((formObject, item) => {
formObject[item.name] = item.value; // eslint-disable-line no-param-reassign
return formObject;
}, {});
What you can do instead is return a new accumulator each time.
return inputs.reduce((formObject, item) => (
{ ...formObject, [item.name]: item.value }
), {});
Note the need for brackets when setting the key.
- Uninstall any previous node installations here
- Use one of the nvm install scripts
nvm install --lts
nvm alias default $NODE_VERSION
(preserves default between sessions, see here