使用JavaScript也有几年时间了,一直对作用域和闭包一只半解,最近有幸拜读了《你不知道的JavaScript》一书,反复阅读了几遍,有了些许收获,甚是欣喜故写下此文以作梳理,欢迎各位大神前来喷。
文章开头先来看一段代码:
alert(age);
var age = 10;
function fn(){
alert(age);
var age = 20;
alert(age)
}
fn()
alert (age)
很简单的几句大家先来猜想下,结果是不是和自己的预期一样。
一 什么是作用域?
首先我们先聊下他的编译原理:例如var a = 10;这是一个我们经常使用的变量,通常我们会认为他就是一个声明。其实在浏览器中会有三个家伙会对他们进行处理:
1.编辑器:负责语法分析以及代码的生产;
2.引擎:从头到尾负责JavaScript的编译以及执行过程;
3.作用域:也就是我们今天主角,负责收集并维护变量的查找和生成,它有自己的一套严格的规范,负责管理标识符(变量)的访问权限。
在接收到 var a = 10;首先编译器会对他进行分词/词法分析(Tokenizing/Lexing)把他分成var、a、=、10、;首先他会询问作用域是否有一个变量(var a);如果没有则在作用域中创建一个变量,如果有则忽略。
接下来编辑器会为引擎生成运行时所依赖的代码,= 10;引擎则会去作用域中询问是否有 var a;在当前的作用域集合中是否存在一个叫作a的变量。如果否,引擎就会使用这个变量;如果不是,引擎会继续查找该变量。如果引擎最终找到了a变量,就会将10赋值给它。否则引擎就会举手示意并抛出一个异常!
那么引擎是如何查询的呢?引擎有两种查询方式 LHS 和 RHS,从字面意思理解来看就是左边和右边,再来看一段代码:
console.log( a );
其中a是一个RHS引用,因为这里并没有赋值,他会向作用域询问你见过a吗?如果找到则返回给console.log(...),否则抛出异常。再看一段代码:
age = 10;
这是一个LHS引用,他首先会询问作用域,你见过age吗?若是没见过则创建并进行赋值,否则直接赋值。
好了在了解了到了工作原理让我们再回过头来看一下文章开头那个问题他的结果是:
alert(age);//undefined
var age = 10;
function fn(){
alert(age);//undefined
var age = 20;
alert(age)//20
}
fn()
alert (age) //10
是不是和我们想的不一样,为什么会这样呢?这又为我们引出来了下一个话题
作用域提升:直觉认为JavaScript代码会从上到下一条一条往下执行但实际上这并不完全正确,有一种特殊情况会导致这个假设是错误的。
a = 2; var a;
console.log( a );
很多人会认为会返回undefined,其实不然会打印出2;
再看下这段代码:
consle.log(a) var a = 10;
鉴于第一段代码并非自上而下的执行,可能很多人会认为会打印出10;也许有一部分人认为会报错。不幸的是生成的结果是undefined。
到底是声明在前,还是赋值在前?不应该是:
var a;
a = 10;
console.log(10)
其实不然,上面提到过编译器会进行两步的操作,第一步是编译,第二部是执行也就是赋值,正确的结果也就是:
var a ;
console.log(a);
a = 10
所以会返回undefined,这也就很好的解释了文章文章开头的问题。
二、什么是全局作用域?什么是局部作用域
JavaScript作用域是一个链式调用的,是不可逆的通俗来讲子作用域可以访问父作用域,反之则会报错,两者之间的关系就像一个个气泡一样绝对包含的,是不可逆转的。
function foo(a) { var b = a * 2; function bar(c) {
console.log( a, b, c ); }
bar( b * 3 ); }
foo( 2 ); // 2, 4, 12
里面有foo()、bar()两个气泡。window全局对象中只包含foo()一个气泡,而foo中又包含bar()。作用域气泡由其对应的作用域块代码写在哪里决定,它们是逐级包含的。那么我们如何在foo()或者全局中使用bar()的元素呢,于是我们引出了下一个话题也就是今天的主角 -- 闭包。
三、什么是闭包,闭包能用来做什么?
闭包一直以来是JavaScript这门语言里的一个难点,但却是非常重要,闭包在我们写的代码中是无处不在的,只是我们不是罢了。有可能很多人认为只有return返回的就是一个闭包,这也是很多论坛里给人的一个误导,简言之在a函数里包裹着b函数,而b函数的作用域又可以在a或者全局上访问到,这就是一个闭包。引用文中的概念:当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。看下面这段代码:
function foo() {
var a = 2;
function bar() {
console.log( a ); // 2
}
bar();
}
foo();
这是闭包吗?
因为bar()完全被foo()函数包裹没有暴露出去,从字面意思来理解这是一个闭包,但其实这个严格意义上来说并不算一个闭包,因为他无法在当前词法作用域之外访问到。
那么什么样才算一个闭包呢?
function foo() {
var a = 2;
function bar() {
console.log( a );
}
return bar; }
var baz = foo();
baz(); //2 这就是一个闭包
函数bar()的作用域能够访问foo()的内部作用域。然后我们将bar()函数本身当作一个值类型进行传递。在这个例子中,我们将bar所引用的函数对象本身当作返回值。在foo()执行后,其返回值(也就是内部的bar()函数)赋值给变量baz并调用baz(),实际上只是通过不同的标识符引用调用了内部的函数bar()。bar()可以执行,而且是再作用域外执行的,所以他可以被称为闭包。
再看下面一段代码:
function fnc(){
var a = 10
function bar(){
console.log(a)
}
baz(bar)
}
function baz(fn){
fn()
}
fnc()//10
把内部函数bar传递给baz,当调用这个内部函数时(现在叫作fn),它涵盖的fnc()内部作用域的闭包就可以观察到了,因为它能够访问a。外部函数调用了fnc里的bar(),执行fnc时也可以获得值,这也可以被称为闭包。
无论通过何种手段将内部函数传递到所在的作用域以外,它都会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。
注意:闭包由于外部的引用会导致一直存在内存中,不利于垃圾回收机制,滥用闭包会导致内存泄漏问题。
注:一开始想的很宏伟,想写的也很多可是一开始写发现不知道从哪写起,最后导致有些内容太乱,只当学习笔记谢谢