1.作用域
作用域是根据名称找变量的一套规则。
变量的赋值操作会执行两个动作,首先编译器会在当前作用域中声明一个变量(如果之前没有声明过),然后在运行时引擎会在作用域中查找该变量,如果能够找到就会对它赋值。
引擎在查找变量时执行怎样的查找,会影响最终的查找结果。
当变量出现在赋值操作的左侧时进行LHS查询,出现在右侧时进行RHS查询:
console.log(a)
对a的引用是一个RHS引用,这里a并没有赋予任何值。相应地,需要查找并取得a的值,这样才能将值传递给console.log(..)
。a = 2
对a的引用是LHS引用,因为实际上我们并不关心当前的值是什么,只是 为=2这个赋值操作找到一个目标。
当一个块或函数嵌套在另一块或函数中时,就发生了作用域的嵌套。在当前作用域中无法找到某个变量时,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量,或抵达最外层的作用域为止。
2.函数作用域
2.1 函数中的作用域
function foo(a){
var b = 2;
function bar(){
//...
}
//更多代码
var c = 3;
}
foo(...)
的作用域气泡中包含了标识符a、b、c和bar。bar(...)
拥有自己的作用域气泡。全局作用域也有自己的作用域气泡,它只包含了一个标识符foo。
由于a、b、c和bar都附属于foo(...)
的作用域气泡,因此无法从foo(...)
的外部对它们进行访问。
函数作用域的含义是指,属于这个函数的全部变量都可以在整个函数的范围内使用及复用。
2.2 隐藏内部实现
“隐藏”作用域中的变量和函数好处是可以避免标识符之间的冲突。
当程序中加载了多个第三方库时,如果它们没有妥善地将内部私有的函数或变量隐藏起来,就会很容易引发变量冲突。
这些库通常会在全局作用域中声明一个名字足够独特的变量,通常是一个对象。这个对象被用作库的命名空间,所有需要暴露给外界的功能都会成为这个对象的属性。
var MyReallyCoolLibrary = {
awesome:"stuff",
doSomething:function(){
//...
},
doAnotherThing:function(){
//...
}
};
2.3 函数作用域
函数声明和函数表达式之间最重要的区别是它们的名称标识符将会绑定在何处。
var a = 2;
//函数声明
function foo(){
var a = 3;
console.log(a);//3
}
foo();
console.log(a);//2
var a = 2;
//函数表达式
(function foo(){
var a = 3;
console.log(a);//3
})();
console.log(a);//2
(function foo(){...})
作为函数表达式意味着foo只能在...
所代表的位置中被访问,外部作用域则不行。
匿名函数表达式:
setTimeout(function(){
console.log("I waited 1 second!");
},1000);
函数表达式可以是匿名的,而函数声明则不可以省略函数名。
匿名函数在栈追踪中不会显示出有意义的函数名,使得调试很困难。
始终给函数表达式命名是一个最佳实践。
setTimeout(function() timeoutHandler{
console.log("I waited 1 second!");
},1000);
IIFE(Immediately-invoked function expression),即立即执行函数表达式:
var a = 2;
(function foo(){
var a = 3;
console.log(a);//3
})();
console.log(a);//2
由于函数被包含在一对()
括号内部,因此成为了一个表达式,通过在末尾加上另外一个()
可以立即执行这个函数。
IIFE还有一种变化的用途是倒置代码的运行顺序,将需要运行的函数放在第二位,在IIFE执行之后当作参数传递进去。
var a = 2;
(function IIFE(def){
def(window);
})(function def(global){
var a = 3;
console.log(a);//3
console.log(global.a);//2
});
2.4 提升
引擎会在解释JavaScript代码之前先对其进行编译,编译阶段中的一部分工作就是找到所有的声明,并用合适的作用域将它们关联起来。
包括变量和函数在内的所有声明都会在任何代码被执行前首先被处理。
a = 2;
var a;
console.log(a);//输出结果为2
/*
等价于
var a;
a = 2;
console.log(a);//输出结果为2
*/
console.log(a);//输出结果为undefined
var a = 2;
/*
等价于
var a;
console.log(a);//输出结果为undefined
a = 2;
*/
上面的处理过程好像变量和函数声明从它们在代码中出现的位置被“移动”到了最上面,这个过程就叫作提升。
每个作用域都会进行提升操作。
函数声明和变量都会被提升。但是一个值得注意的细节是函数会首先被提升,然后才是变量。
foo();//1
var foo;
function foo(){
console.log(1);
}
foo = function(){
console.log(2);
};
/*
等价于
function foo(){
console.log(1);
}
foo();//1
foo = function(){
console.log(2);
};
*/
注意:var foo
尽管出现在function foo()...
的声明之前,但它是重复的声明(因此被忽略了),因为函数声明会被提升到普通变量之前。
尽管重复的var
声明会被忽略掉,但出现在后面的函数声明还是可以覆盖前面的。
foo();//3
function foo(){
console.log(1);
}
var foo = function(){
console.log(2);
};
function foo(){
console.log(3);
}
在同一个作用域中进行重复定义是非常糟糕的,而且经常会导致各种奇怪的问题。
3.作用域闭包
JavaScript中闭包无处不在,你只需要能够识别并拥抱它。
闭包是基于词法作用域书写代码时所产生的自然结果,你甚至不需要为了利用它们而有意识地创建闭包。
function foo(){
var a = 2;
function bar(){
console.log(a);
}
return bar;
}
var baz = foo();
baz();//2--这就是闭包的效果
函数bar()
的词法作用域能够访问foo()
的内部作用域,在foo()
执行后,其返回值(也就是内部的bar()
函数)赋值给变量baz
并调用baz()
。
在foo()
执行后,通常会期待foo()
的整个内部作用域都被销毁,因为我们知道引擎有垃圾回收器用来释放不再使用的内存空间。由于看上去foo()
的内容不会再被使用,所以很自然地会考虑对其进行回收。bar()
拥有涵盖foo()
内部作用域的闭包,使得该作用域能够一直存活,以供bar()
在之后任何时间进行引用。bar()
依然持有对该作用域的引用,而这个引用就叫作闭包。
function foo(){
var a = 2;
function baz(){
console.log(a);
}
bar(baz);
}
function bar(fn){
fn();//这就是闭包!
}
var fn;
function foo(){
var a = 2;
function baz(){
console.log(a);
}
fn = baz;//将baz分配给全局变量
}
function bar(){
fn();//这就是闭包!
}
foo();
bar();//2