本系列的第一本書层释,總結完結婆瓜。下周開始本系列第二本...
Values & Types
JavaScript has typed values, not typed variables. The following built-in types are available:
string
number
boolean
null and undefined
object
-
symbol (new to ES6)
The return value from thetypeof
operator is always one of six (seven as of ES6! - the "symbol" type) string values. That is,typeof "abc"
returns"string"
, notstring
.
a = null;
typeof a; // "object" -- weird, bug
typeof null
is an interesting case, because it errantly returns "object"
, when you'd expect it to return "null"
.
Warning: This is a long-standing bug in JS, but one that is likely never going to be fixed. Too much code on the Web relies on the bug and thus fixing it would cause a lot more bugs!
Objects
Theobject
type refers to a compound value where you can set properties (named locations) that each hold their own values of any type.
var obj = {
a: "hello world",
b: 42,
c: true
};
// dot notation
obj.a; // "hello world"
obj.b; // 42
obj.c; // true
// bracket notation
obj["a"]; // "hello world"
obj["b"]; // 42
obj["c"]; // true
Bracket notation is useful if you have a property name that has special characters in it, like obj["hello world!"]
-- such properties are often referred to as keys when accessed via bracket notation. The [ ]
notation requires either a variable (explained next) or a string
literal (which needs to be wrapped in" .. "
or' .. '
).
var obj = {
a: "hello world",
b: 42
};
var b = "a";
obj[b]; // "hello world"
obj["b"]; // 42
Arrays
An array is an object
that holds values (of any type) not particularly in named properties/keys, but rather in numerically indexed positions.
The best and most natural approach is to use arrays for numerically positioned values and use objects for named properties.
Functions
functions are a subtype of objects
-- typeof
returns "function"
, which implies that a function is a main type -- and can thus have properties, but you typically will only use function object properties (like foo.bar
) in limited cases.
function foo() {
return 42;
}
foo.bar = "hello world";
typeof foo; // "function"
typeof foo(); // "number"
typeof foo.bar; // "string"
Built-In Type Methods
var a = "hello world";
var b = 3.14159;
a.length; // 11
a.toUpperCase(); // "HELLO WORLD"
b.toFixed(4); // "3.1416"
The "how" behind being able to calla.toUpperCase()
is more complicated than just that method existing on the value.
Briefly, there is a String
(capitalS
) object wrapper form, typically called a "native," that pairs with the primitive string
type; it's this object wrapper that defines the toUpperCase()
method on its prototype.
When you use a primitive value like"hello world"
as anobject
by referencing a property or method (e.g., a.toUpperCase()
in the previous snippet), JS automatically "boxes" the value to its object wrapper counterpart (hidden under the covers).
Comparing Values
There are two main types of value comparison that you will need to make in your JS programs: equality and inequality. The result of any comparison is a strictly boolean
value (true
or false
), regardless of what value types are compared.
Coercion
Coercion comes in two forms in JavaScript: explicit and implicit.
Truthy & Falsy
The specific list of "falsy" values in JavaScript is as follows:
-
""
(empty string) -
0
,-0
,NaN
(invalidnumber
) -
null
,undefined
false
Any value that's not on this "falsy" list is "truthy."
Equality
There are four equality operators: ==, ===, !=, and !==.
The proper way to characterize them is that ==
checks for value equality with coercion allowed, and===
checks for value equality without allowing coercion; ===
is often called "strict equality" for this reason.
there's two possible waysa == b
could give true
via coercion. Either the comparison could end up as 42 == 42
or it could be "42" == "42"
. So which is it?
The answer: "42"
becomes42
, to make the comparison 42 == 42
.
You should take special note of the==
and ===
comparison rules if you're comparing two non-primitive values, like object
s (including function
and array
). Because those values are actually held by reference, both ==
and ===
comparisons will simply check whether the references match, not anything about the underlying values.
For example, array
s are by default coerced to strings by simply joining all the values with commas (,
) in between. You might think that two array
s with the same contents would be ==
equal, but they're not:
var a = [1,2,3];
var b = [1,2,3];
var c = "1,2,3";
a == c; // true
b == c; // true
a == b; // false
Inequality
The <
, >
, <=
, and >=
operators are used for inequality, referred to in the specification as "relational comparison."
Notably, there are no "strict inequality" operators that would disallow coercion the same way ===
"strict equality" does.
In section 11.8.5 of the ES5 specification, it says that if both values in the < comparison are strings, as it is with b < c, the comparison is made lexicographically (aka alphabetically like a dictionary). But if one or both is not a string, as it is with a < b, then both values are coerced to be numbers, and a typical numeric comparison occurs.
var a = 42;
var b = "foo";
a < b; // false
a > b; // false
a == b; // false
Wait, how can all three of those comparisons be false
? Because the b value is being coerced to the "invalid number value" NaN
in the <
and >
comparisons, and the specification says that NaN
is neither greater-than nor less-than any other value.
Variables
In JavaScript, variable names (including function names) must be valid identifiers.
An identifier must start with a
-z
, A
-Z
, $
, or_
. It can then contain any of those characters plus the numerals0
-9
.
However, certain words cannot be used as variables, but are OK as property names. These words are called "reserved words," and include the JS keywords (for
, in
, if
, etc.) as well as null
, true
, and false
.
Function Scopes
Hoisting
Metaphorically, this behavior is called hoisting, when a var declaration is conceptually "moved" to the top of its enclosing scope.
var a = 2;
foo(); // works because `foo()`
// declaration is "hoisted"
function foo() {
a = 3;
console.log( a ); // 3
var a; // declaration is "hoisted"
// to the top of `foo()`
}
console.log( a ); // 2
Nested Scopes
When you declare a variable, it is available anywhere in that scope, as well as any lower/inner scopes.
bad one:
function foo() {
a = 1; // `a` not formally declared
}
foo();
a; // 1 -- oops, auto global variable :(
This is a very bad practice. Don't do it! Always formally declare your variables.
In addition to creating declarations for variables at the function level, ES6 lets you declare variables to belong to individual blocks (pairs of { .. }), using the let keyword.
function foo() {
var a = 1;
if (a >= 1) {
let b = 2;
while (b < 5) {
let c = b * 2;
b++;
console.log( a + c );
}
}
}
foo();
// 5 7 9
Conditionals
Sometimes you may find yourself writing a series of if..else..if
statements like this:
if (a == 2) {
// do something
}
else if (a == 10) {
// do another thing
}
else if (a == 42) {
// do yet another thing
}
else {
// fallback to here
}
This structure works, but it's a little verbose because you need to specify the a
test for each case. Here's another option, the switch
statement:
switch (a) {
case 2:
case 10:
// some cool stuff
//Here, if a is either 2 or 10, it will execute the "some cool stuff" code statements.
break;
case 42:
// other stuff
break;
default:
// fallback
}
The break
is important if you want only the statement(s) in one case
to run. If you omit break
from a case
, and that case
matches or runs, execution will continue with the next case
's statements regardless of that case
matching. This so called "fall through" is sometimes useful/desired.
Another form of conditional in JavaScript is the "conditional operator," often called the "ternary operator." It's like a more concise form of a single if..else
statement, such as:
var a = 42;
var b = (a > 41) ? "hello" : "world";
// similar to:
// if (a > 41) {
// b = "hello";
// }
// else {
// b = "world";
// }
Strict Mode
ES5 added a "strict mode" to the language, which tightens the rules for certain behaviors.
Not only will strict mode keep your code to a safer path, and not only will it make your code more optimizable, but it also represents the future direction of the language. It'd be easier on you to get used to strict mode now than to keep putting it off -- it'll only get harder to convert later!
Functions As Values
Not only can you pass a value (argument) to a function, but a function itself can be a value that's assigned to variables, or passed to or returned from other functions.
Immediately Invoked Function Expressions (IIFEs)
There's another way to execute a function expression, which is typically referred to as an immediately invoked function expression (IIFE):
(function IIFE(){
console.log( "Hello!" );
})();
// "Hello!"
Because an IIFE is just a function, and functions create variable scope, using an IIFE in this fashion is often used to declare variables that won't affect the surrounding code outside the IIFE:
var a = 42;
(function IIFE(){
var a = 10;
console.log( a ); // 10
})();
console.log( a ); // 42
Closure
You can think of closure as a way to "remember" and continue to access a function's scope (its variables) even once the function has finished running.
function makeAdder(x) {
// parameter `x` is an inner variable
// inner function `add()` uses `x`, so
// it has a "closure" over it
function add(y) {
return y + x;
};
return add;
}
// `plusOne` gets a reference to the inner `add(..)`
// function with closure over the `x` parameter of
// the outer `makeAdder(..)`
var plusOne = makeAdder( 1 );
// `plusTen` gets a reference to the inner `add(..)`
// function with closure over the `x` parameter of
// the outer `makeAdder(..)`
var plusTen = makeAdder( 10 );
plusOne( 3 ); // 4 <-- 1 + 3
plusOne( 41 ); // 42 <-- 1 + 41
plusTen( 13 ); // 23 <-- 10 + 13
More on how this code works:
- When we call makeAdder(1), we get back a reference to its inner
add(..)
that remembersx
as1
. We call this function referenceplusOne(..)
. - When we call
makeAdder(10)
, we get back another reference to its inneradd(..)
that remembersx
as10
. We call this function referenceplusTen(..)
. - When we call
plusOne(3)
, it adds3
(its innery
) to the1
(remembered byx
), and we get4
as the result. - When we call
plusTen(13)
, it adds13
(its innery
) to the10
(remembered byx
), and we get23
as the result.
Modules
The most common usage of closure in JavaScript is the module pattern. Modules let you define private implementation details (variables, functions) that are hidden from the outside world, as well as a public API that is accessible from the outside.
function User(){
var username, password;
function doLogin(user,pw) {
username = user;
password = pw;
// do the rest of the login work
}
var publicAPI = {
login: doLogin
};
return publicAPI;
}
// create a `User` module instance
var fred = User();
fred.login( "fred", "12Battery34!" );
Warning: We are not calling new User()
here, on purpose, despite the fact that probably seems more common to most readers. User()
is just a function, not a class to be instantiated, so it's just called normally. Using new
would be inappropriate and actually waste resources.
Executing User()
creates an instance of the User
module -- a whole new scope is created, and thus a whole new copy of each of these inner variables/functions. We assign this instance tofred
. If we run User()
again, we'd get a new instance entirely separate from fred
.
The inner doLogin()
function has a closure over username
and password
, meaning it will retain its access to them even after the User()
function finishes running.
publicAPI
is an object with one property/method on it, login
, which is a reference to the innerdoLogin()
function. When we return publicAPI
from User()
, it becomes the instance we callfred
.
At this point, the outer User()
function has finished executing. Normally, you'd think the inner variables like username
and password
have gone away. But here they have not, because there's a closure in the login()
function keeping them alive.
That's why we can call fred.login(..)
-- the same as calling the innerdoLogin(..)
-- and it can still access username
and password
inner variables.
There's a good chance that with just this brief glimpse at closure and the module pattern, some of it is still a bit confusing. That's OK! It takes some work to wrap your brain around it.
this Identifier
While it may often seem that this
is related to "object-oriented patterns," in JS this
is a different mechanism.
If a function has a this
reference inside it, that this
reference usually points to an object
. But which object
it points to depends on how the function was called.
function foo() {
console.log( this.bar );
}
var bar = "global";
var obj1 = {
bar: "obj1",
foo: foo
};
var obj2 = {
bar: "obj2"
};
// --------
foo(); // "global"
obj1.foo(); // "obj1"
foo.call( obj2 ); // "obj2"
new foo(); // undefined
There are four rules for how this
gets set, and they're shown in those last four lines of that snippet.
-
foo() ends up setting
thisto the global object in non-strict mode -- in strict mode,
thiswould be
undefinedand you'd get an error in accessing the
barproperty -- so "
global" is the value found for
this.bar`. -
obj1.foo()
setsthis
to theobj1
object. -
foo.call(obj2)
setsthis
to theobj2
object. -
new foo()
setsthis
to a brand new empty object.
Prototypes
When you reference a property on an object, if that property doesn't exist, JavaScript will automatically use that object's internal prototype reference to find another object to look for the property on. You could think of this almost as a fallback if the property is missing.
The internal prototype reference linkage from one object to its fallback happens at the time the object is created. The simplest way to illustrate it is with a built-in utility called Object.create(..)
.
var foo = {
a: 42
};
// create `bar` and link it to `foo`
var bar = Object.create( foo );
bar.b = "hello world";
bar.b; // "hello world"
bar.a; // 42 <-- delegated to `foo`
It may help to visualize the foo and bar objects and their relationship:
Old & New
Some of the JS features we've already covered, and certainly many of the features covered in the rest of this series, are newer additions and will not necessarily be available in older browsers. In fact, some of the newest features in the specification aren't even implemented in any stable browsers yet.
There are two main techniques you can use to "bring" the newer JavaScript stuff to the older browsers: polyfilling and transpiling.
Polyfilling
if (!Number.isNaN) {
Number.isNaN = function isNaN(x) {
return x !== x;
};
}
Transpiling
The better option is to use a tool that converts your newer code into older code equivalents. This process is commonly called "transpiling," a term for transforming + compiling.
- Babel (https://babeljs.io) (formerly 6to5): Transpiles ES6+ into ES5
- Traceur (https://github.com/google/traceur-compiler): Transpiles ES6, ES7, and beyond into ES5
Non-JavaScript
So far, the only things we've covered are in the JS language itself. The reality is that most JS is written to run in and interact with environments like browsers. A good chunk of the stuff that you write in your code is, strictly speaking, not directly controlled by JavaScript. That probably sounds a little strange.
The most common non-JavaScript JavaScript you'll encounter is the DOM API. For example:
var el = document.getElementById( "foo" );
This book, and this whole series, focuses on JavaScript the language. That's why you don't see any substantial coverage of these non-JavaScript JavaScript mechanisms. Nevertheless, you need to be aware of them, as they'll be in every JS program you write!