Re-Introduction To JS
Re-Introduction To JS
Number
String
Boolean
Function
Object
Symbol (new in ES2015)
... oh, and undefined and null, which are ... slightly odd. And Array,
which is a special kind of object. And Date and RegExp, which are objects
that you get for free. And to be technically accurate, functions are just a
special type of object. So the type diagram looks more like this:
Number
String
Boolean
Symbol (new in ES2015)
Object
Function
Array
Date
RegExp
null
undefined
And there are some built-in Error types as well. Things are a lot easier if
we stick with the first diagram, however, so we'll discuss the types listed
there for now.
Numbers
Numbers in JavaScript are "double-precision 64-bit format IEEE 754
values", according to the spec. This has some interesting consequences.
There's no such thing as an integer in JavaScript, so you have to be a little
careful with your arithmetic if you're used to math in C or Java.
parseInt('010'); // 8
parseInt('0x10'); // 16
Here, we see the parseInt() function treat the first string as octal due to
the leading 0, and the second string as hexadecimal due to the leading
"0x". The hexadecimal notation is still in place; only octal has been
removed.
If you want to convert a binary number to an integer, just change the
base:
parseInt('11', 2); // 3
Similarly, you can parse floating point numbers using the built-
in parseFloat() function. Unlike
its parseInt() cousin, parseFloat() always uses base 10.
You can also use the unary + operator to convert values to numbers:
+ '42'; // 42
+ '010'; // 10
+ '0x10'; // 16
A special value called NaN (short for "Not a Number") is returned if the
string is non-numeric:
parseInt('hello', 10); // NaN
NaN is toxic: if you provide it as an input to any mathematical operation
the result will also be NaN:
NaN + 5; // NaN
You can test for NaN using the built-in isNaN() function:
isNaN(NaN); // true
JavaScript also has the special values Infinity and -Infinity:
1 / 0; // Infinity
-1 / 0; // -Infinity
You can test for Infinity, -Infinity and NaN values using the built-
in isFinite()function:
isFinite(1 / 0); // false
isFinite(-Infinity); // false
isFinite(NaN); // false
The parseInt() and parseFloat() functions parse a string until they reach a
character that isn't valid for the specified number format, then return the
number parsed up to that point. However the "+" operator simply converts the
string to NaN if there is an invalid character contained within it. Just try parsing
the string "10.2abc" with each method by yourself in the console and you'll
understand the differences better.
Strings
Strings in JavaScript are sequences of Unicode characters. This should be
welcome news to anyone who has had to deal with internationalization.
More accurately, they are sequences of UTF-16 code units; each code unit
is represented by a 16-bit number. Each Unicode character is represented
by either 1 or 2 code units.
If you want to represent a single character, you just use a string consisting
of that single character.
To find the length of a string (in code units), access its length property:
'hello'.length; // 5
There's our first brush with JavaScript objects! Did we mention that you
can use strings like objects too? They have methods as well that allow you
to manipulate the string and access information about the string:
'hello'.charAt(0); // "h"
'hello, world'.replace('hello', 'goodbye'); // "goodbye,
world"
'hello'.toUpperCase(); // "HELLO"
Other types
JavaScript distinguishes between null, which is a value that indicates a
deliberate non-value (and is only accessible through the null keyword),
and undefined, which is a value of type undefined that indicates an
uninitialized value that is, a value hasn't even been assigned yet. We'll
talk about variables later, but in JavaScript it is possible to declare a
variable without assigning a value to it. If you do this, the variable's type
is undefined. undefined is actually a constant.
JavaScript has a boolean type, with possible values true and false (both
of which are keywords.) Any value can be converted to a boolean
according to the following rules:
1.false, 0, empty strings (""), NaN, null, and undefined all
become false.
2.All other values become true.
You can perform this conversion explicitly using the Boolean() function:
Boolean(''); // false
Boolean(234); // true
However, this is rarely necessary, as JavaScript will silently perform this
conversion when it expects a boolean, such as in an if statement (see
below). For this reason, we sometimes speak simply of "true values" and
"false values," meaning values that become true and false, respectively,
when converted to booleans. Alternatively, such values can be called
"truthy" and "falsy", respectively.
Boolean operations such as && (logical and), || (logical or),
and ! (logical not) are supported; see below.
Variables
New variables in JavaScript are declared using one of three
keywords: let, const, or var.
var is the most common declarative keyword. It does not have the
restrictions that the other two keywords have. This is because it was
traditionally the only way to declare a variable in JavaScript. A variable
declared with the var keyword is available from the function it is declared
in.
var a;
var name = 'Simon';
An example of scope with a variable declared with var:
// myVarVariable *is* visible out here
'3' + 4 + 5; // "345"
3 + 4 + '5'; // "75"
Adding an empty string to something is a useful way of converting it to a
string itself.
Comparisons in JavaScript can be made using <, >, <= and >=. These work
for both strings and numbers. Equality is a little less straightforward. The
double-equals operator performs type coercion if you give it different
types, with sometimes interesting results:
123 == '123'; // true
1 == true; // true
To avoid type coercion, use the triple-equals operator:
Dictionaries in Python.
Hashes in Perl and Ruby.
Hash tables in C and C++.
HashMaps in Java.
Associative arrays in PHP.
The fact that this data structure is so widely used is a testament to its
versatility. Since everything (bar core types) in JavaScript is an object, any
JavaScript program naturally involves a great deal of hash table lookups.
It's a good thing they're so fast!
The "name" part is a JavaScript string, while the value can be any
JavaScript value including more objects. This allows you to build data
structures of arbitrary complexity.
var obj = {
name: 'Carrot',
for: 'Max', // 'for' is a reserved word, use '_for' instead.
details: {
color: 'orange',
size: 12
}
};
Attribute access can be chained together:
obj.details.color; // orange
obj['details']['size']; // 12
The following example creates an object prototype, Person and an
instance of that prototype, You.
function Person(name, age) {
this.name = name;
this.age = age;
}
// Define an object
var you = new Person('You', 24);
// We are creating a new person named "You" aged 24.
Once created, an object's properties can again be accessed in one of two
ways:
//dot notation
obj.name = 'Simon';
var name = obj.name;
And...
// bracket notation
obj['name'] = 'Simon';
var name = obj['name'];
// can use a variable to define a key
var user = prompt('what is your key?')
obj[user] = prompt('what is its value?')
These are also semantically equivalent. The second method has the
advantage that the name of the property is provided as a string, which
means it can be calculated at run-time. However, using this method
prevents some JavaScript engine and minifier optimizations being
applied. It can also be used to set and get properties with names that
are reserved words:
obj.for = 'Simon'; // Syntax error, because 'for' is a
reserved word
obj['for'] = 'Simon'; // works fine
Starting in ECMAScript 5, reserved words may be used as object property
names "in the buff". This means that they don't need to be "clothed" in quotes
when defining object literals. See the ES5 Spec.
For more on objects and prototypes see: Object.prototype. For an
explanation of object prototypes and object prototype chains
see: Inheritance and the prototype chain.
Starting in ECMAScript 2015, object keys can be defined by variable using
bracket notation upon being created. {[phoneType]: 12345} is possible instead
of just var userPhone = {}; userPhone[phoneType] = 12345.
Arrays
Arrays in JavaScript are actually a special type of object. They work very
much like regular objects (numerical properties can naturally be accessed
only using [] syntax) but they have one magic property called 'length'.
This is always one more than the highest index in the array.
One way of creating arrays is as follows:
a.push(item);
Arrays come with a number of methods. See also the full documentation
for array methods.
Method name Description
Returns a string with the toString() of each element
a.toString()
separated by commas.
Returns a string with the toLocaleString() of each
a.toLocaleString()
element separated by commas.
a.concat(item1[, item2[, ...
Returns a new array with the items added on to it.
[, itemN]]])
Converts the array to a string with values delimited
a.join(sep)
by the sep param
a.pop() Removes and returns the last item.
a.push(item1, ..., itemN) Appends items to the end of the array.
a.reverse() Reverses the array.
a.shift() Removes and returns the first item.
a.slice(start[, end]) Returns a sub-array.
a.sort([cmpfn]) Takes an optional comparison function.
a.splice(start, delcount[, Lets you modify an array by deleting a section and
item1[, ...[, itemN]]]) replacing it with more items.
a.unshift(item1[, item2[, ...
Prepends items to the start of the array.
[, itemN]]])
Functions
Along with objects, functions are the core component in understanding
JavaScript. The most basic function couldn't be much simpler:
function add(x, y) {
var total = x + y;
return total;
}
This demonstrates a basic function. A JavaScript function can take 0 or
more named parameters. The function body can contain as many
statements as you like, and can declare its own variables which are local
to that function. The return statement can be used to return a value at
any time, terminating the function. If no return statement is used (or an
empty return with no value), JavaScript returns undefined.
The named parameters turn out to be more like guidelines than anything
else. You can call a function without passing the parameters it expects, in
which case they will be set to undefined.
add(); // NaN
// You can't perform addition on undefined
You can also pass in more arguments than the function is expecting:
add(2, 3, 4); // 5
// added the first two; 4 was ignored
That may seem a little silly, but functions have access to an additional
variable inside their body called arguments, which is an array-like object
holding all of the values passed to the function. Let's re-write the add
function to take as many values as we want:
function add() {
var sum = 0;
for (var i = 0, j = arguments.length; i < j; i++) {
sum += arguments[i];
}
return sum;
}
add(2, 3, 4, 5); // 14
That's really not any more useful than writing 2 + 3 + 4 + 5 though.
Let's create an averaging function:
function avg() {
var sum = 0;
for (var i = 0, j = arguments.length; i < j; i++) {
sum += arguments[i];
}
return sum / arguments.length;
}
(function() {
var b = 3;
a += b;
})();
a; // 4
b; // 2
JavaScript allows you to call functions recursively. This is particularly
useful for dealing with tree structures, such as those found in the
browser DOM.
function countChars(elm) {
if (elm.nodeType == 3) { // TEXT_NODE
return elm.nodeValue.length;
}
var count = 0;
for (var i = 0, child; child = elm.childNodes[i]; i++) {
count += countChars(child);
}
return count;
}
This highlights a potential problem with anonymous functions: how do
you call them recursively if they don't have a name? JavaScript lets you
name function expressions for this. You can use named IIFEs
(Immediately Invoked Function Expressions) as shown below:
Custom objects
For a more detailed discussion of object-oriented programming in JavaScript,
see Introduction to Object Oriented JavaScript.
s = makePerson('Simon', 'Willison');
personFullName(s); // "Simon Willison"
personFullNameReversed(s); // "Willison, Simon"
This works, but it's pretty ugly. You end up with dozens of functions in
your global namespace. What we really need is a way to attach a function
to an object. Since functions are objects, this is easy:
function personFullName() {
return this.first + ' ' + this.last;
}
function personFullNameReversed() {
return this.last + ', ' + this.first;
}
function Person(first, last) {
this.first = first;
this.last = last;
this.fullName = personFullName;
this.fullNameReversed = personFullNameReversed;
}
That's better: we are creating the method functions only once, and
assigning references to them inside the constructor. Can we do any better
than that? The answer is yes:
function Person(first, last) {
this.first = first;
this.last = last;
}
Person.prototype.fullName = function() {
return this.first + ' ' + this.last;
};
Person.prototype.fullNameReversed = function() {
return this.last + ', ' + this.first;
};
Person.prototype is an object shared by all instances of Person. It forms
part of a lookup chain (that has a special name, "prototype chain"): any
time you attempt to access a property of Person that isn't set, JavaScript
will check Person.prototype to see if that property exists there instead.
As a result, anything assigned to Person.prototypebecomes available to
all instances of that constructor via the this object.
This is an incredibly powerful tool. JavaScript lets you modify something's
prototype at any time in your program, which means you can add extra
methods to existing objects at runtime:
s.reversed(); // nomiS
Our new method even works on string literals!
Person.prototype.toString = function() {
return '<Person: ' + this.fullName() + '>';
}
function nestedFunc() {
var b = 4; // parentFunc can't use this
return a + b;
}
return nestedFunc(); // 5
}
This provides a great deal of utility in writing more maintainable code. If a
function relies on one or two other functions that are not useful to any
other part of your code, you can nest those utility functions inside the
function that will be called from elsewhere. This keeps the number of
functions that are in the global scope down, which is always a good thing.
This is also a great counter to the lure of global variables. When writing
complex code it is often tempting to use global variables to share values
between multiple functions which leads to code that is hard to
maintain. Nested functions can share variables in their parent, so you can
use that mechanism to couple functions together when it makes sense
without polluting your global namespace "local globals" if you like. This
technique should be used with caution, but it's a useful ability to have.
Closures
This leads us to one of the most powerful abstractions that JavaScript has
to offer but also the most potentially confusing. What does this do?
function makeAdder(a) {
return function(b) {
return a + b;
};
}
var x = makeAdder(5);
var y = makeAdder(20);
x(6); // ?
y(7); // ?
The name of the makeAdder() function should give it away: it creates new
'adder' functions, each of which, when called with one argument, adds it
to the argument that it was created with.
What's happening here is pretty much the same as was happening with
the inner functions earlier on: a function defined inside another function
has access to the outer function's variables. The only difference here is
that the outer function has returned, and hence common sense would
seem to dictate that its local variables no longer exist. But they dostill
exist otherwise the adder functions would be unable to work. What's
more, there are two different "copies" of makeAdder()'s local variables
one in which a is 5 and one in which a is 20. So the result of those
function calls is as follows:
x(6); // returns 11
y(7); // returns 27
Here's what's actually happening. Whenever JavaScript executes a
function, a 'scope' object is created to hold the local variables created
within that function. It is initialized with any variables passed in as
function parameters. This is similar to the global object that all global
variables and functions live in, but with a couple of important differences:
firstly, a brand new scope object is created every time a function starts
executing, and secondly, unlike the global object (which is accessible
as this and in browsers as window) these scope objects cannot be directly
accessed from your JavaScript code. There is no mechanism for iterating
over the properties of the current scope object, for example.
So when makeAdder() is called, a scope object is created with one
property: a, which is the argument passed to
the makeAdder() function. makeAdder() then returns a newly created
function. Normally JavaScript's garbage collector would clean up the
scope object created for makeAdder() at this point, but the returned
function maintains a reference back to that scope object. As a result, the
scope object will not be garbage-collected until there are no more
references to the function object that makeAdder() returned.
Scope objects form a chain called the scope chain, similar to the
prototype chain used by JavaScript's object system.