JavaScript学习笔记——函数 Part6:函数环境与词法作用域-确定当前使用的是[局部变量]还是[同名的全局变量]

要点

  • 创建函数后得到的是函数引用(指向函数及其环境
  • 环境中保存了其创建时所处作用域内的变量的值(包括一切在函数中出现过的局部变量,全局变量除外),且环境中的自由变量只能通过该函数访问和修改(私有性)
    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使用的是函数环境和词法作用域

正确的看法
  1. 虽然定义函数inner时,局部变量justAVar的作用域在这个函数内。
  2. 函数和其环境会被保存:每当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
环境是实时更新的访问和改变环境中的自由变量(外围变量)值,要么直接改变环境中的自由变量值,要么改变(嵌套函数的)外层函数中的相应变量
换句话说,要访问和改变环境中的自由变量,有两种方法:

  1. 直接改变环境中的自由变量值:函数及其环境创建后,无法直接访问环境中的变量值,必须通过调用那个函数来访问和改变(如上面的Eg3所述)
  2. 改变(嵌套函数的)外层函数中的相应变量:在外层函数中,修改相应的自由变量的值

为证明这一点,有如下的示例

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!"

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值