重拾JavaScript(三)-作用域和作用域链

作用域

在 JavaScript 中有两种作用域

  • 全局作用域
  • 局部作用域

当变量定义在一个函数中时,变量就在局部作用域中,而定义在函数之外的变量则从属于全局作用域。每个函数在调用的时候会创建一个新的作用域。


全局作用域

最外层函数定义的变量拥有全局作用域,全局作用域里的变量能够在其他作用域中被访问和修改。

1
2
3
4
5
6
7
var name = "Tennant";
console.log(name);//Tennant

function logName(){
console.log(name);
}
logName();//Tennant

局部作用域

和全局作用域相反,局部作用域一般只在固定的代码片段内可访问到,而对于函数外部是无法访问的,最常见的例如函数内部

1
2
3
4
5
function fn(){
var name = "Tennant";
}
fn();
console.log(name);//undefined

需要注意的是,函数内部声明变量的时候,一定要使用var命令。如果不用的话,实际上声明了一个全局变量:

1
2
3
4
5
function fn(){
name = "Tennant";
}
fn();
console.log(name);//Tennant

再如以下代码,只要函数内定义了一个局部变量,函数在解析的时候都会将这个变量“提前声明” :

1
2
3
4
5
6
7
var scope = "global";
function fn(){
console.log(scope);//undefined
var scope = "local";
console.log(scope);//local
}
fn();
1
2
3
4
5
6
7
8
var scope = "global";
function fn(){
var scope;//提前声明了局部变量
console.log(scope);//undefined
scope = "local";
console.log(scope);//local
}
fn();

没有块级作用域

Javascript没有块级作用域,在其他类C的语言中,由花括号封闭的代码块都有自己的作用域,因此支持条件来定义变量。例如,以下代并不会得到想要的结果:

1
2
3
4
if(true){
var name = "Tennant";
}
console.log(name);//Tennant

这里是在一个if语句中定义了变量name。如果是在C、C++或者Java中,name会在if语句执行完毕后被销毁。但在JavaScript中,if语句中的变量声明会将变量添加到当前的执行环境(在这里是全局环境)中。在使用for语句时要特别注意,例如:

1
2
3
4
for (var i = 0; i < 10; i++){
doSomething(i);
}
console.log(i);//10

对于有块级作用域的语言来说,for语句初始化变量的表达式所定义的变量,只会存在于循环的环境之中。而对于JavaScript来说,由for语句创建的变量i即使在for循环执行结束后,也依旧会存在于循环外部的执行环境中。


作用域链(Scope chain)

当代码在一个环境执行时,会创建变量对象的一个作用域链。作用域链的作用是保证对执行环境有权访问的所有变量和函数的有序访问


执行环境

执行环境定义了变量或函数有权访问的其他数据,决定了它们各自的行为。每个执行环境都有一个与之关联的变量对象(variable object)环境中定义的所有变量和函数都保存在这个对象中

全局执行环境是最外围的执行环境,全局执行环境被认为是window对象,因此所有的全局变量和函数都作为window对象的属性和方法创建的。

JavaScript的执行顺序是根据函数的调用来决定的,当一个函数被调用时,该函数环境的变量对象就被压入一个环境栈中。而在函数执行之后,栈将该函数的变量对象弹出,把控制权交给之前的执行环境变量对象。

举个例子:

1
2
3
4
5
6
7
8
9
10
var color = "blue";
function changeColor(){
if(color === "blue"){
color = "red";
}else{
color = "blue";
}
}
changeColor();
alert(color);//red

在以上例子中,函数changeColor()的作用域链包含两个对象:

它自己的变量对象(其中定义着arguments对象)和全局环境的变量对象。可以在函数内部访问变量color,就是因为可以在这个作用域链中找到它。

看如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var a = 1
function fn1(){
function fn2(){
console.log(a)
}
function fn3(){
var a = 4
fn2()
}
var a = 2
return fn3
}
var fn = fn1()
fn() //输出多少

//输出a=2
//执行fn2函数,fn2找不到变量a,接着往上在找到创建当前fn2所在的作用域fn1中找到a=2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var a = 1
function fn1(){
function fn3(){
function fn2(){
console.log(a)
}
var a
fn2()
a = 4
}
var a = 2
return fn3
}
var fn = fn1()
fn() //输出多少

//输出undefined
//函数fn2在执行的过程中,先从自己内部找变量找不到,再从创建当前函数所在的作用域fn去找,注意此时变量声明前置,a已声明但未初始化为undefined

以上查找方向为:

  1. 函数在执行的过程中,先从自己内部找变量(注意找的是变量的当前的状态)
  2. 如果找不到,再从创建当前函数所在的作用域去找, 以此往上

立即执行函数表达式是什么?有什么作用?

立即执行函数的作用是隔离作用域,使被执行的函数内部的变量不会污染到外部,即外部不能访问函数内部的变量。
写法:

1
2
3
4
5
6
7
8
9
10
11
(function(){ / code / }());

(function(){ / code / })();

!function () { / code / }();

~function () { / code / }();

-function () { / code / }();

+function () { / code / }();

立即执行函数表达式的作用:
1.不必为函数命名,避免了污染全局变量;
2.立即执行函数表达式内部形成了一个单独的作用域,可以封装一些外部无法读取的私有变量,隔离作用域。


写一个函数,返回参数的平方和

1
2
3
4
5
6
7
8
9
10
11
function sumOfSquares(){
var sumOfSquares = 0;
for (var i = 0; i < arguments.length; i++) {
sumOfSquares= sumOfSquares+arguments[i]*arguments[i];
}
return console.log(sumOfSquares);
}
var result = sumOfSquares(2,3,4);
var result2 = sumOfSquares(1,3);
console.log(result);//29
console.log(result2);//10

如下代码的输出?为什么?

1
2
3
console.log(a);
var a = 1;
console.log(b);
1
2
3
4
5
6
7
输出:undefined 报错("error" "ReferenceError: b is not defined ...");

运算过程
var a;
console.log(a);//输出undefined,因为变量提升,会把变量a的声明提升到console.log(a);之前,当console.log(a);执行的时候,变量a还没有被赋值,所以console.log(a);输出undefined。
a = 1;
console.log(b);//报错,因为没有变量b。

1
2
3
4
5
6
7
8
sayName('world');
sayAge(10);
function sayName(name){
console.log('hello ', name);
}
var sayAge = function(age){
console.log(age);
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
输出: hello world
Uncaught TypeError: sayAge is not a function

运算过程
在执行上面代码的时候会将函数的声明前置,而函数表达式只会将变量的声明前置,函数不会前置。实际上述代码在执行的时候会变成这样:
var sayAge;
function sayName(name){
console.log('hello ', name);
}
sayName('world');
sayAge(10);//sayAge(10);的时候,sayAge只是个变量不是函数,所以会报错。最终会输出hello world和报错。
sayAge = function(age){
console.log(age);
};

1
2
3
4
5
6
7
8
9
var x = 10
bar()
function foo() {
console.log(x)
}
function bar(){
var x = 30
foo()
}
1
2
3
4
5
6
输出: 10
第2行,bar()调用bar函数
第6行,bar函数里面调用foo函数
第3行,foo函数从自己的局部环境里找x,结果没找到
第1行,foo函数从上一级环境里找x,即从全局环境里找x,找到了var x=10。
foo()的输出结果为10。

1
2
3
4
5
6
7
8
9
var x = 10;
bar()
function bar(){
var x = 30;
function foo(){
console.log(x)
}
foo();
}
1
2
3
4
5
6
输出: 30
第2行,bar()调用bar函数
第3行,bar函数里面是foo函数
foo函数在自己的局部环境里寻找x,没找到
foo函数到自己的上一级环境,即bar函数的局部环境里找x,找到var x=30
所以第2行的bar()输出为30