它是一套规则,这套规则用来管理引擎如何在当前作用域及嵌套的子作用域中根据标识符名称进行变量的查找。

1
2
3
4
if(true) {
var num = 10;
}
console.log(num);

思考:JavaScirpt怎样才会形成作用域?

函数作用域

就是说,在JavaScript中,在函数里面定义的变量,可以在函数里面被访问,但是在函数外无法访问。

1
2
3
4
var func = function() {
var num = 10;
};
console.log(num);
1
2
3
4
var func = function() {
var num = 10;
console.log(num);
};

前面说了,函数可以限定变量的作用域,那么在函数中的函数就成为该作用域的子域。在子域中的代码可以访问到父域中的变量。

1
2
3
4
5
6
7
8
var func = function() {
var num = 10;
var sub_func = function() {
console.log(num);
};
sub_func();
};
func();
1
2
3
4
5
6
7
8
9
var func = function() {
var num = 10;
var sub_func = function() {
var num = 20;
console.log(num);
};
sub_func();
};
func();

由此可见访问有一定规则可言。在JavaScript中使用变量,JavaScript解释器首先在当前作用域中搜索是否有该变量的定义,如果有,就是用这个变量;如果没有就到父域中寻找该变量。以此类推,直到最顶级作用域,仍然没有找到就抛出异常”变量未定义”。

1
2
3
4
5
6
7
8
9
(function() {
var num = 10;
(function() {
var num = 20;
(function(){
console.log(num);
})()
})();
})();

块级作用域

思考:什么叫块级作用域?

1
2
3
4
5
6
7
8
9
function outputNumbers(count){
for (var i=0;i<count;i++){
console.log(i);
}
console.log(i)
var i;
console.log(i);
}
outputNumbers(3);

很遗憾,在es6之前,javascript是没有块级作用域的。所以也会因此造成对一些变量值的忽视,从而引起程序运行结果不对。那前辈们遇到这个问题是怎么做的呢?让我们来考考古。

利用函数来实现块级作用域

  • 因为函数是js里唯一具有块级作用域特点的。
1
2
3
4
5
6
7
8
9
function outputNumbers(count){
(function(){
for (var i=0;i<count;i++){
console.log(i);
}
})();
console.log(i);
}
outputNumbers(3);

思考:这是不是一个闭包?

方法类(不建议使用)

with
  • 用with从对象创建出的作用域仅在with声明中而非外部作用域中有效。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    function foo(obj) {
    with (obj) {
    var a = 2;
    }
    }
    var o1 = {
    a: 3
    };
    var o2 = {
    b: 3
    };
    foo( o1 );
    console.log( o1.a );

    foo( o2 );
    console.log( o2.a );
    console.log( a );
try/catch
1
2
3
4
5
6
7
try{
undefined();//执行一个非法操作来强制制造一个异常
}
catch(err){
console.log(err)//能够正常执行
}
console.log(err);

ES6中的块级作用域

将代码在函数中隐藏的信息扩展为在块中隐藏起来。

let(不能重复声明)

let关键字可以将变量绑定到所在的任意作用域中。

  • 垃圾回收
    1
    2
    3
    4
    5
    6
    7
    8
    9
    function process(data){
    //在这里做点有趣的事情
    }

    {//在这个块中定义的内容完事可以销毁
    let someReallyBigData={...};
    process(someReallyBigData);
    }

  • let循环
    1
    2
    3
    4
    for(let i=0;i<10;i++){
    console.log(i);
    }
    console.log(i);
const(不能重复声明)
  • 定义一个该块的常量,不能修改值。
1
2
3
4
5
6
7
8
9
10
var foo = true;

if (foo) {
var a = 2;
const b = 3;
a = 3;
b = 4;
}
console.log(a);
console.log(b);

作用域链

因为作用域是一套用于确定在何处以及如何查找变量的规则,我对作用域链的理解就是查找变量所走的路。

建筑

作用域链建筑型

根据这个图来理解,就是查找变量所爬的楼。

爬楼太累了,我们再看看用画树的方法再来看看。

  • 绘制规则
  • 作用域链就是对象的数组
  • 全局作用域是0级链,每个对象占一个位置
  • 凡是看到函数延伸一个链出来,一级级展开
  • 访问首先看当前函数,如果没有定义往上一级链检查
  • 如此往复,直到0级链
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var num = 10;
var func1 = function() {
var num = 20;
var func2 = function() {
var num = 30;
console.log(num);
};
func2();
};
var func2 = function() {
var num = 20;
var func3 = function() {
console.log(num);
};
func3();
};
func1();
func2();

现场绘制一波

预解析

形成变量提升现象

1
2
3
4
5
6
7
var num = 10;
var func = function() {
console.log(num);
var num = 20;
console.log(num);
};
func();

函数提升

1
2
3
4
5
6
7
8
9
10
var func = function() {
console.log("调用外面的函数");
};
var foo = function() {
func();
var func = function() {
console.log("调用内部的函数");
};
func();
};

预解析:在当前作用域下,js运行之前,会把带有var和function关键字的事先声明,并在内存中安排好,然后再从上到下执行js语句。预解析只会发生在通过var定义的变量和function上。

var

  • 只要是通过var定义的,不管是变量,还是函数,都是先赋值undefined,如果是变量,也不管变量有没有赋值,在预解析阶段,都是会被赋值为undefined。

function

  • function进行预解析的时候,不仅是声明而且还定义了,但是它存储的数据的那个空间里面存储的是代码是字符串,没有任何意义。
1
2
3
function fun() {
//代码区
}();
  • 定义一个函数想要立即执行,写成上面的形式是不可行的,在预解释的时候,它把它分解成两部分来对待,第一部分是fun函数,而第二部分是(),一个匿名函数,执行时会报错。如果小括号带参数,如(2),虽然不会报错,会打印出来2,但并不能把fn执行,也不能当成参数传递给fn函数。

  • 如果你想实现立即执行的函数,可以把要执行的函数放到一对括号里面,对于JavaScript 来说,括弧()里面不能包含语句,所以在这一点上,解析器在解析function关键字的时候,会将相应的代码解析成function表达式,而不是function声明所以,只要将大括号将代码(包括函数部分和在后面加上一对大括号)全部括起来就可以了。 如下:

1
2
3
(function fun() {
//代码区
}());
  • 预解析是发生在当前作用域下的,刚开始的时候,我们预解析的是全局作用域,在js中我们的global就是我们的window。
  • 我们运行函数的时候会生成一个新的私有作用域(每次执行都是新的,执行完成就销毁)这个作用域下我们可以理解为开辟了一个新的内存空间。在这个内存中我们也要执行预解析。当我们的函数执行完成后,这个内存或者作用域就会销毁。
  • 如果在当前作用域下的一个变量没有预解析,就会向它的上一级去找,直到找到window,如果window下也没有定义,就会报错。所以,在函数内通过var定义的变量是局部变量,没有通过var定义的变量是全局变量。
  • 预解析不会在同一个变量上重复的发生,也就是一个变量如果已经在当前作用域下预解析了,不会再重复解析。
  • 等号右边的function不会进行预解析。
  • 预解释是不受其它if或者其它判断条件影响的,也就是说,即使条件不成立,我们里面只要有var或者function也会被预解释。
  • 后面定义的会覆盖前面定义的。

一些例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// 例子一
if(!("a" in window)){
var a = "李玉华";
}
console.log(a);

// 例子二
function fn(){
console.log("我们是全局的fn");
}
function fn2(){
console.log(fn);
fn = 3;
return ;
function fn(){
console.log("我是fn2里面的");
}
}
fn2();

// 例子三
var n = 0;
function a(){
var n = 10;
function b(){
n++;
console.log(n);
}
b();
return b;
}
var c = a();
c();
console.log(n);

// 例子四
var n = 99;
function outer(){
var n = 0;
return function inner(){
return n++;
}
}
var c = outer();
var num1 = c();
var num2 = c();
var d = outer();
var num3 = d();
console.log(c);
console.log(num1);
console.log(num2);
console.log(d);
console.log(num3);

总结

  • 作用域就是根据标识符查找变量的一套规则。(路标)
  • 作用域链就是找变量所走的路径。
  • 预解析就是万恶的“VIP”。

注: