作用域和闭包----你不知道的JavaScript(上)
-
作用域
- js引擎:从头到尾负责js程序的编译及执行过程
- 编译器,负责语法分析和代码生成(ast树转换为可执行代码)
- 作用域
eg:var a = 2;
编译器进行词法分析时,会问作用域是否有该变量,若无则声明一个新变量a。
代码运行时,通过引擎来进行赋值操作,引擎先问作用域是不是有一个变量a,是就会直接使用变量进行赋值,否则就向上一级作用域寻找。
- LHS对赋值操作进行变量查询
- RHS对引用,找变量源头进行查询。会出现ReferencError,作用域判别失败。
-
词法作用域
函数的词法作用域由函数被声明时所处的位置决定。查找变量时从当层开始找,当层找不到就向上一层
-
函数作用域
属于这个函数的全部变量都可以在整个函数范围内使用及复用。外部无法直接访问函数内部的变量。因此可以对于代码片段进行封装,使得“隐藏”起来。
方式一:
function foo1() {
var a = 3;
console.log(a);
}
方式二:
(function foo1() {
var a = 3;
console.log(a)
});
方式一和二的区别在于,方式一中function是声明中第一个词,是一个函数声明,函数标识符foo会被绑定在全局作用域中;而方式二开头是“(”,会被当作函数表达式来看,函数标识符foo绑定在函数表达式自身。会立即执行,而不需要通过调用foo1.
同时,方式二表明函数只能在()中访问,外部无法被访问到,不会污染全局作用域。
函数表达式可匿名,为function(){},无名称标识符“foo”.例如:
setTimeout(function(){
console.log('aaa')
},1000)
- 函数表达式可以匿名,函数声明不可以匿名。
IIFE:立即执行函数
方式一:
(function foo1() {
var a = 3;
console.log(a)
})();
方式二:
(function foo1() {
var a = 3;
console.log(a)
}());
IIFE进阶,将所在作用域的参数传递到立即执行函数里面
(function foo1() {
var a = 3;
console.log(a)
})(window);
-
块作用域
let
let以{ }内部为作用域,无变量提升(在声明之前不能引用)
应用:
- 垃圾回收:
当函数在运行一次之后,之后不会被用到该变量,就可以对该变量进行let声明。会自动在没有引用之后对块作用域进行垃圾回收,不占用大量内存。
- let 的for循环
声明时的范围若在函数内,则在函数作用域内可访问变量。若在if判断语句中,变量可在全局访问。
var foo = true,baz = 10;
if (foo) {
var bar = 3;
if (baz > bar) {
console.log(baz);//10,可访问全局的baz
}
}
function aaa() {
var a = 1;
}
console.log(bar);//可访问if语句中的变量bar;当bar用let声明时,块级作用域会报错。
console.log(a);//不可访问函数里面的变量a
const
无变量提升,值也不可更改,为常量。当定义对象时,对象的属性可更改
-
提升
先编译,声明变量;运行逻辑会留在原地。赋值和console.log都在执行阶段进行。
a=2;
var a;
console.log(a)
//打印2
console.log(a)
var a=2
//undefined
//以上等效为下述顺序
var a//编译阶段
a=2 //执行阶段
console.log(a)//执行
var a//编译阶段
console.log(a)//执行阶段
a=2//执行阶段
只有声明会被提升。
函数声明function foo( ){ }会被提升,函数表达式var foo = function bar( ){ }不会。
函数会优先变量进行提升。
foo();//TypeError.变量标识符foo()被提升,但没有赋值,foo()对undefined值进行函数调用
bar();//ReferenceError
var foo = function bar(){
//....
}
//上述等效于以下形式
var foo;
foo();
bar();
foo = function(){
var bar = ...
}
普通块内部的函数声明会提升到作用域顶部。
-
闭包
定义:当函数可以记住并访问所在的词法作用域时,即使函数在当前词法作用域之外执行,就产生了闭包。
可访问函数内部之外的作用域即产生了闭包。
function foo(){
var a = 2;
function bar(){
console.log(a);
}
return bar();
}
var baz = foo();
baz()//2
上述即为闭包,全局作用域底下调用 baz:即使在函数外部,依然可以访问到内部bar的词法作用域。bar词法作用域可以访问foo内部作用域。
原本foo()执行后,其内部作用域会被销毁进行垃圾回收。但闭包使得内存不被回收。bar()本身在使用该内部作用域。
理解:var baz = foo()时,将foo()的返回值bar()赋值给baz。之后调用baz,实际上是调用了内部函数bar().
闭包的应用:
定时器:
function wait(message){
setTimeout(function timer(){
console.log(message);
},1000);
}
wait('hello')
将内部函数timer传递setTimeout()。timer具有涵盖wait()作用域的闭包,因此保有对变量message的引用。
setTimeout持有对参数的引用,引擎会调用这个函数timer,词法作用域在此过程中保持完整。
总结:在定时器、事件监听器等异步(或同步)任务中,只要使用了回调函数,实际上就是在使用闭包。
应用一:IIFE
那么,IIFE立即执行函数是闭包吗?
var a = 2;
(function foo(){
console.log(a)
})();
严格来讲,不是闭包。因为函数foo并不是在它本身的词法作用域之外进行的。它在定义所在的作用域中执行(而外部作用域,也就是全局作用域也持有a)。a是通过普通的词法作用域查找而非闭包被发现的
应用二:for循环
for循环是闭包最常见的例子.
for(var i=1;i<=5;i++){
setTimeout(function timer(){
console.log(i);
},i*1000)
}
//6,6,6,6,6间隔一秒出现
预期结果是,按照1-5顺序依次间隔一秒输出。实际运行结果为以每秒一次的频率输出5次6.
原因:循环终止时,不满足i<=5,因此为6. 延迟函数timer的回调会在循环结束后才执行。
深入探讨是啥缺陷导致结果与预期不一致的呢?尽管5个函数是在各自迭代中分别定义的,但是都在一个全局作用域中,实际上引用的只有一个i.我们需要每个迭代时都能有一个i的副本。所以需要更多的闭包作用域。每个迭代都有一个闭包作用域。
解决:每个迭代都要一个闭包作用域。
for(var i=1;i<=5;i++){
(function(){
setTimeout(function timer(){
console.log(i);
},i*1000);
})();
}
//6,6,6,6,6
有独立作用域,但是作用域为空,i没有传递进去。并不会有5个i的副本。
for(var i=1;i<=5;i++){
(function(j){
setTimeout(function timer(){
console.log(j);
},i*1000);
})(i);//<-----将i传递进函数,为区别,函数内部参数定为j
}
//6,6,6,6,6
第二种方案,let块作用域。
for(let i=1;i<=5;i++){
setTimeout(function timer(){
console.log(i);
},i*1000)
}
//6,6,6,6,6
应用三:模块化
var foo = (function coolModule(id){
function change(){
publicAPI.identify = identify2;
};
function identify1(){
console.log(id)
};
function identify2(){
console.log(id.toUppercase())
};
var publicAPI = {
change:change,
identify:identify1
};
return publicAPI;
})('hello')
foo.identify();//hello
foo.change();
foo.identify();//HELLO
import 可以将一个模块中的一个或者多个API导入当前作用域中;export将当前模块标识符导出为公共API。
附录:
静态作用域和动态作用域:
静态:在声明时就定义
动态:在调用时定义。
JavaScript没有动态作用域,只有词法作用域,但this机制很像动态作用域。