跳过正文
  1. 技术杂项/

You don't know JS

·1390 字·7 分钟
目录

Non-JavaScript
#

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.

The most common non-JavaScript JavaScript you’ll encounter is the DOM API. For example:

var el = document.getElementById( "foo" );

The document variable exists as a global variable when your code is running in a browser. It’s not provided by the JS engine, nor is it particularly controlled by the JavaScript specification. It takes the form of something that looks an awful lot like a normal JS object, but it’s not really exactly that. It’s a special object, often called a “host object.”

Moreover, the getElementById(..) method on document looks like a normal JS function, but it’s just a thinly exposed interface to a built-in method provided by the DOM from your browser. In some (newer-generation) browsers, this layer may also be in JS, but traditionally the DOM and its behavior is implemented in something more like C/C++.

So does alert(..) and console.log(..).

Compiler Theory
#

Despite the fact that JavaScript falls under the general category of “dynamic” or “interpreted” languages, it is in fact a compiled language. But not as other languages, JavaScript compilation doesn’t happen in a build step ahead of time. For JavaScript, the compilation that occurs happens, in many cases, mere microseconds (or less!) before the code is executed.

  • Engine: responsible for start-to-finish compilation and execution of our JavaScript program.
  • Compiler: one of Engine’s friends; handles all the dirty work of parsing and code-generation (see previous section).
  • Scope: another friend of Engine; collects and maintains a look-up list of all the declared identifiers (variables), and enforces a strict set of rules as to how these are accessible to currently executing code.

Statement var a = 2; will be proceed as:

  1. Encountering var aCompiler asks Scope to see if a variable a already exists for that particular scope collection. If so, Compiler ignores this declaration and moves on. Otherwise, Compiler asks Scope to declare a new variable called a for that scope collection.
  2. Compiler then produces code for Engine to later execute, to handle the a = 2 assignment. The code Engine runs will first ask Scope if there is a variable called a accessible in the current scope collection. If so, Engine uses that variable. If not, Engine looks elsewhere (see nested Scope section below).

If Engine eventually finds a variable, it assigns the value 2 to it. If not, Engine will raise its hand and yell out an error!

Scope
#

  • RHS and LHS

    Right-Hand-Search, for retrieve. Left-Hand-Search, for assignment.

    RHS and LHS behave differently in the circumstance where the variable has not yet been declared (is not found in any consulted Scope).

    If an RHS look-up fails to ever find a variable, anywhere in the nested Scopes, this results in a ReferenceError being thrown by the Engine. It’s important to note that the error is of the type ReferenceError.

    If an RHS look-up fails to ever find a variable, and if the program is not running in “Strict Modek, then the global Scope will create a new variable of that name in the global scope, and hand it back to Engine.

Lexical scope
#

There are two predominant models for how scope works. The first of these is by far the most common, used by the vast majority of programming languages. It’s called Lexical Scope, and we will examine it in-depth. The other model, which is still used by some languages (such as Bash scripting, some modes in Perl, etc.) is called Dynamic Scope.

Lexical scope is scope that is defined at lexing time. In other words, lexical scope is based on where variables and blocks of scope are authored, by you, at write time, and thus is (mostly) set in stone by the time the lexer processes your code.

No matter where a function is invoked from, or even how it is invoked, its lexical scope is only defined by where the function was declared.

Function Scope
#

The traditional way of thinking about functions is that you declare a function, and then add code inside it. But the inverse thinking is equally powerful and useful: take any arbitrary section of code you’ve written, and wrap a function declaration around it, which in effect “hides” the code.

In other words, you can “hide” variables and functions by enclosing them in the scope of a function.

Anonymous vs. Named Function
#

Anonymous function expressions are quick and easy to type, and many libraries and tools tend to encourage this idiomatic style of code. However, they have several draw-backs to consider:

  1. Anonymous functions have no useful name to display in stack traces, which can make debugging more difficult.
  2. Without a name, if the function needs to refer to itself, for recursion, etc., the deprecated arguments.callee reference is unfortunately required. Another example of needing to self-reference is when an event handler function wants to unbind itself after it fires.
  3. Anonymous functions omit a name that is often helpful in providing more readable/understandable code. A descriptive name helps self-document the code in question.

The best practice is to always name your function expressions.

Hoisting
#

Both function declarations and variable declarations are hoisted. But a subtle detail is that functions are hoisted first, and then variables.

Closure Scope
#

function foo() {
	var a = 2;

	function bar() {
		console.log( a );
	}

	return bar;
}

var baz = foo();

baz(); // 2 -- Whoa, closure was just observed, man.

bar() is executed outside of its declared lexical scope.

After foo() executed, normally we would expect that the entirety of the inner scope of foo() would go away, because we know that the Engine employs a Garbage Collector that comes along and frees up memory once it’s no longer in use. Since it would appear that the contents of foo() are no longer in use, it would seem natural that they should be considered gone.

But the “magic” of closures does not let this happen. That inner scope is in fact still “in use”, and thus does not go away. Who’s using it? The function bar() itself.

bar() still has a reference to that scope, and that reference is called closure.

function foo() {
	var a = 2;

	function baz() {
		console.log( a ); // 2
	}

	bar( baz );
}

function bar(fn) {
	fn(); // look ma, I saw closure!
}

We pass the inner function baz over to bar, and call that inner function (labeled fn now), and when we do, its closure over the inner scope of foo() is observed, by accessing a.

Whatever facility we use to transport an inner function outside of its lexical scope, it will maintain a scope reference to where it was originally declared, and wherever we execute it, that closure will be exercised.

This
#

The key contrast: lexical scope is write-time, whereas dynamic scope (and this!) are runtime. Lexical scope cares where a function was declared, but dynamic scope cares where a function was called from.

To be clear, JavaScript does not, in fact, have dynamic scope. It has lexical scope. Plain and simple. But the this mechanism is kind of like dynamic scope.

When a function is invoked, an activation record, otherwise known as an execution context, is created. This record contains information about where the function was called from (the call-stack), how the function was invoked, what parameters were passed, etc. One of the properties of this record is the this reference which will be used for the duration of that function’s execution.

To understand this binding, we have to understand the call-site: the location in code where a function is called (not where it’s declared). We must inspect the call-site to answer the question: what’s this this a reference to?

function baz() {
    // call-stack is: `baz`
    // so, our call-site is in the global scope

    console.log( "baz" );
    bar(); // <-- call-site for `bar`
}

function bar() {
    // call-stack is: `baz` -> `bar`
    // so, our call-site is in `baz`

    console.log( "bar" );
    foo(); // <-- call-site for `foo`
}

function foo() {
    // call-stack is: `baz` -> `bar` -> `foo`
    // so, our call-site is in `bar`

    console.log( "foo" );
}

baz(); // <-- call-site for `baz`