要点
- 创建函数后得到的是函数引用(指向函数及其环境)
- 环境中保存了其创建时所处作用域内的变量的值(包括一切在函数中出现过的局部变量,全局变量除外),且环境中的自由变量只能通过该函数访问和修改(私有性)
i.e. 创建函数后也创建了与其相关联的环境 - 请将函数及其环境视为一个整体
调用函数时,实际上是在与其相关联的环境中执行这个函数
i.e. 无论何时调用函数(即使返回函数后在外面调用它,情况也是如此),函数都优先使用其环境中保存的变量,找不到再向外查找 - 词法作用域意味着通过阅读代码就能确定变量的作用域:(在嵌套函数中)为了确定未在本地定义的变量值,将在最近的外部函数中查找;如果没有找到,再在全局作用域中查找,仍未找到则认为undefined
函数引用,它指向的到底是什么?
“函数引用是指向函数的引用”,这里所谓的“指向函数”,应该有如下理解:
- 可以将函数视为经过简单包装的代码,其中不仅包含函数体中的代码,还有与函数相关联的环境,可随时调用
- 函数引用指向的是函数及其环境(这是一个整体)
函数环境(environment)
-
每个函数都有与之相关联的环境
-
函数的环境包含了在其作用域内出现的所有局部变量(包括在函数内定义的变量、包括没有在函数内定义的变量,即外围变量的值;包括函数内的嵌套函数、包括传入函数的形参,但必不包含全局变量)
i.e. 函数内出现的所有局部变量都存储在环境中(形参也可视为局部变量) -
JavaScript中,函数执行完后,函数和其环境(含有局部变量)会被保存;
而在C++和Java语言中,函数执行完后,其局部变量被销毁 -
函数及其环境创建后,无法直接访问(无法改变)环境中的变量值,必须通过那个函数来访问和改变(私有性)
-
调用函数时,实际上是在与其相关联的环境中执行这个函数
-
无论何时调用函数,函数都优先使用其环境中已保存的变量,若环境中找不到相应的变量,再向外、向全局作用域查找同名的变量(即使返回函数并在定义函数的作用域外面调用它,情况也是如此)
-
环境是实时更新的,要访问和改变环境中的自由变量(外围变量)值,要么直接改变环境中的自由变量值,要么改变(嵌套函数的)外层函数中的相应变量
问:在函数被嵌套很多层时,环境的工作原理是什么呢?
答:环境包含了在局部作用域内定义的所有变量(包括嵌套函数),嵌套函数同样是在定义它的环境中执行的。
另外,可认为每个嵌套函数都有自己的小环境,其中包含它自己的变量。这样将形成一个环境链,从内到外依次为各个嵌套函数的环境。
因此,在环境中查找变量时,你将从最近的环境着手,沿环境链不断往下查找,直到找到变量为止。如果在环境链中没有找到,再在全局环境中查找。
函数环境能为我们带来什么好处呢?
- 可以利用这种特性创建闭包,详见((((((((((((P7
词法作用域
根据函数环境的知识,可知:
- JavaScript函数都是在与其相关联的环境中执行的。
- 在函数中,要确定变量来自何方,可按从内到外的顺序依次在包含它的函数中搜索。
根据上面的理论,我们引入词法作用域,总是可以通过查看代码来确定变量是在哪里定义的(即:确定变量的值)
词法(lexical)意味着只需查看代码的结构就可确定变量是在哪里定义的,进而确定它的值(也确定了它是局部变量还是同名的全局变量),而不是等到代码执行时才明白。
- 按照代码编写时的样子,嵌套函数(内部函数)可以访问函数外面的变量
代码在执行时,按照词法作用域的规则,可以访问外围的变量 - JavaScript的作用域规则完全基于代码的结构,而非运行阶段的动态属性
- 函数不会因为运行时调用它的时机不同而不同(在词法作用域中,重要的是函数是在什么地方定义的,而非调用函数的时机)
- 这意味着只需查看代码的结构,就能确定变量是在什么地方定义的
- 在函数中,要确定引用的究竟是哪个变量(确定它是在哪里定义的),可从最里面(当前函数)开始依次向最外面进行查找,直到找到它为止。如果在这些函数中都找不到它,则它要么是全局的,要么未定义
Eg1.下面有全局变量justAVar和局部变量justAVar
var justAVar = "I'm GLOBAL";//全局变量justAVar
function whereAreYou() {//函数whereAreYou定义了一个新的词法作用域
var justAVar = "I'm LOCAL";//局部变量justAVar
function inner() {
return justAVar;
}
return inner();
}
var result = whereAreYou();
console.log(result);
/输出"I'm LOCAL"/
函数whereAreYou被调用时返回justAVar,不过是哪个justAVar呢?
- 使用词法作用域,因此在最近的函数作用域内查找justAVar;如果在这个作用域内没有找到,再在全局作用域内查找。
- ps. 事实上,这里与之前的认知相同:函数whereAreYou中有一个局部变量justAVar,它遮住了同名的全局变量
因此,调用whereAreYou()返回的结果是局部变量justAVar
Eg2.下面做一点调整,这次whereAreYou返回一个函数
var justAVar = "I'm GLOBAL";//全局变量justAVar
function whereAreYou() {
var justAVar = "I'm LOCAL";//局部变量justAVar
function inner() {
return justAVar;
}
return inner;//返回一个函数引用
}
var innerFunction = whereAreYou();//将返回的函数引用赋给innerFunction
var result = innerFunction();
console.log(result);
/输出"I'm LOCAL"/
此时调用whereAreYou(),返回了一个函数引用,赋给innerFunction
图:返回函数时,返回的不仅是函数,还有其环境
如果我们调用这个返回的函数(innerFunction),返回哪个justAVar?
- 首先注意,innerFunction的值为函数引用,因此innerFunction和inner指向同一个函数
- 我们使用的是词法作用域,那么每当inner被调用时,它都认为justAVar这个局部变量还存在,需要时可直接使用
因此结果是:innerFunction返回了局部变量justAVar的值,即“I’m LOCAL”
错误的看法
这里有一些错误的理解如下:
- 错误的看法:重要的是调用函数的时机。我们调用返回的函数inner时,全局变量justAVar在作用域内,因此结果为“I’m GLOBAL”
- 错误的看法:定义局部变量justAVar的函数whereAreYou已执行完毕,局部变量justAVar已不复存在
- 错误的看法:离开其作用域后,局部变量justAVar就不存在了,它消失(derezzed)了
在C++和Java语言中也许如上面所说
但在JavaScript中不是这样的,JavaScript使用的是函数环境和词法作用域
正确的看法
- 虽然定义函数inner时,局部变量justAVar的作用域在这个函数内。
- 函数和其环境会被保存:每当inner被调用时,它都认为这个局部变量还存在,需要时可直接使用。
i.e. 函数whereAreYou已执行完毕,但其作用域依然存在,可供inner使用
更进一步地理解环境
Eg2-2
我们知道,return函数时,实际上是return函数引用,它指向函数及其环境
函数及其环境将在return函数的瞬间创建,此后函数及其环境无法直接访问(但之后可以通过该函数访问和改变环境中变量的值,详见下面Eg3)
var justAVar = "I'm GLOBAL";//全局变量justAVar
function whereAreYou() {
var justAVar = "I'm LOCAL";//局部变量justAVar
function inner() {
return justAVar;
}
justAVar = "I'm";
return inner;//返回一个函数引用
justAVar = "LOCAL";
}
justAVar = "I'm GLB";
var innerFunction = whereAreYou();//将返回的函数引用赋给innerFunction
var result = innerFunction();
console.log(result);
//输出"I'm"/
Eg3
函数及其环境创建后,无法直接访问(无法改变)环境中的变量值,必须通过调用那个函数来访问和改变(私有性)
function makeCounter() {
var count = 0;
function counter() {
count = count + 1;
return count;
}
return counter;
}
var doCount = makeCounter();
console.log(doCount());//输出1
console.log(doCount());//输出2
console.log(doCount());//输出3
每当通过doCount调用counter时(它们实际上指向的是同一个函数),其环境中的count变量的值会改变,并且没有其他方法能改变count的值
Eg2-3
全局变量是个例外:环境中不会存储全局变量的值(因为环境保存的是函数在return的那一刻的状态及其周边的变量)
- 即使是全局变量在函数体内出现,函数环境也不保存全局变量的值
因此,如果使用了全局变量,当前调用该函数的结果,与全局变量当前的值有关
var justAVar = "I'm GLOBAL";//全局变量justAVar
function whereAreYou() {
function inner() {
return justAVar;
}
justAVar = "I'm LOCAL";//在函数内改变全局变量的值
return inner;//返回一个函数引用
}
justAVar = "I'm";
var innerFunction = whereAreYou();//将返回的函数引用赋给innerFunction
justAVar = "GLO";
var result = innerFunction();
justAVar = "BAL";
console.log(result);
//输出"GLO"/
Eg4
环境是实时更新的,访问和改变环境中的自由变量(外围变量)值,要么直接改变环境中的自由变量值,要么改变(嵌套函数的)外层函数中的相应变量
换句话说,要访问和改变环境中的自由变量,有两种方法:
- 直接改变环境中的自由变量值:函数及其环境创建后,无法直接访问环境中的变量值,必须通过调用那个函数来访问和改变(如上面的Eg3所述)
- 改变(嵌套函数的)外层函数中的相应变量:在外层函数中,修改相应的自由变量的值
为证明这一点,有如下的示例
function makeTimer(doneMessage, n) {//设置定时器,它在n毫秒后提示doneMessage
//创建一个函数(使用了自由变量doneMessage),并作为setTimeout的事件处理程序
setTimeout(function() {
alert(doneMessage);
}, n);
doneMessage = "OUCH!";
}
makeTimer("Cookies are done!", 1000);
输出"OUCH!"
下面是分析
function makeTimer(doneMessage, n) {//设置定时器,它在n毫秒后提示doneMessage
...
}
makeTimer("Cookies are done!", 1000);
首先调用了makeTimer,并传入参数
function makeTimer(doneMessage, n) {
//创建一个函数(使用了自由变量doneMessage),并作为setTimeout的事件处理程序
setTimeout(function() {
alert(doneMessage);
}, n);
...
}
在这里,向setTimeout传递事件处理程序时,使用了函数表达式创建闭包
在此刻,环境中doneMessage的值为"Cookies are done!"
function makeTimer(doneMessage, n) {
//创建一个函数(使用了自由变量doneMessage),并作为setTimeout的事件处理程序
setTimeout(function() {
alert(doneMessage);
}, n);
doneMessage = "OUCH!";
}
紧接着又在闭包外面修改doneMessage的值为"OUCH!"
最终,1000毫秒后调用事件处理程序时,由于闭包函数的环境中,doneMessage变量的值为"OUCH!",提示框中显示的是"OUCH!"