先编译后执行

JavaScript虽然是动态语言,但是在执行的时候,也仍然是按照先编译后执行的顺序来解析代码的。codeExample_1就能证明这种结果。

1
2
3
4
// codeExample_1
var greeting = 'hello';
console.log(greeting);
greeting = .'Hi'; // 错误代码,会报错

用node去执行段代码,会发现,不会打印hello再报错,而是直接报错。因为编译器在遇到错误代码的时候就报错了,从而导致整个程序都无法执行。

词法作用域

在编译时,变量的作用域就已经确定,这样的作用域称为词法作用域。而从一个作用域拿到另一个作用域中的变量的方法,则称为闭包。在代码执行的时候,每当遇到一个函数或者包含有let、const声明的变量的{...}就会生成一个块级作用域。所以,作用域最后会形成一个作用域里面嵌套多个子作用域。子作用域可以使用父作用域中的变量,但是父作用域不能使用子作用域中的变量。

在作用域中查找变量有以下几个规则:

  1. look up

    当一个作用域中使用了一个变量的时候,会优先在当前作用域去查找这个变量的值,如果没有找到这个变量,就会一直往父作用域中查找,知道找到为止或者到顶级作用域为止。当然,这个规则其实是一种抽象,其实当前作用域能否拿到这个作用域中的变量的值在编译时期就已经知道了,不会在运行的时候再去look up。

  2. Shadowing

    当一个作用域声明了一个和父作用域中相同的变量名时候,由于look up规则,会优先使用当前作用域中的变量声明的值,从而造成了shadow的效果。

变量提升

函数声明和变量声明在编译器编译的时候的行为是不一样的。编译器在遇到函数声明时,会连带着变量声明和变量赋值,所以函数声明可以后声明先使用。

1
2
3
const a = getNumber(1, 2);

function getNumber(a, b) { return a + b; }

但是变量声明,会将变量的声明语句和赋值语句分开,声明语句会先执行,赋值语句后执行,而var声明的变量会默认给一个初始值undefined,letconst声明的语句则不会。所以var声明的变量,可以先使用声明,但是在声明语句之前的值是undefined,而letconst在声明之前使用则会报错,因为他们没有给默认的初始值。

1
2
3
4
5
6
7
console.log(a); // undefined
console.log(b); // Error,会导致程序崩溃
var a = 1;
let b = 2;

console.log(a); // 1
console.log(b); // 2

let、var、const的区别

  1. let和const会生成新的作用域,var不会

  2. var、let声明的变量可以改变,const声明的变量必须一开始就赋初始值,且不会改变

  3. var不适合用于for…loop中,因为没有生成新的作用域,会导致所有迭代变最后都是一个值

  4. const不适用于有变量for(;;i++)的循环

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    for (const i = 0; i < 5; i++) { .... }

    // 相当于
    {
    const $i = 0;
    for (; $i < 5; $i++) {
    const i = $i;
    ...
    $i++; // const不能重新赋值
    }
    }

闭包

Closure is when a function is able to remember and access its lexical scope even when that function is executing outside its lexical scope.

比如:

1
2
3
4
5
6
7
8
9
10
11
function foo() {
var a = 2;
function bar() {
console.log(a);
}

return bar;
}

var baz = foo();
baz(); // 2

上面这段代码很好的展示了什么是闭包。按照作用域的规则,正常情况下,我们是拿不到foo作用域中的a的。当foo()执行完之后,垃圾回收机制会将foo作用域回收,那么其内部声明的变量以及函数都会被回收,但是由于bar被return了,导致仍然有baz指向了这段函数,所以bar和它的词法作用域才没有被垃圾回收,因此baz才能正常执行,也能拿到a

1
2
3
4
5
6
7
8
9
10
11
12
13
function foo () {
var a = 2;

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

bar(baz);
}

function bar(fn) {
fn();
}

按照作用域的规则,bar是无法拿到foo中的变量a的。但是,当baz作为函数传递给bar时,由于baz能拿到foo中的a。此时bar也可取到a了,这就是闭包。闭包在JS中随处可见,但是闭包也有一个问题就是,无法及时释放内存,如果存在大量闭包,会导致大量内存无法释放,占用资源,甚至导致内存泄漏。所以,在使用闭包结束之后,应该将闭包清理掉,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
function foo() {
var a = 2;
function bar() {
console.log(a);
}

return bar;
}

var baz = foo();
baz(); // 2

baz = null; // baz指向了null,那bar指向的那段函数就没有再被引用,将会被垃圾回收机制清理掉,从而释放内存