【JavaScript】作用域和词法作用域

本文详细介绍了JavaScript中的作用域和闭包。首先,讲解了作用域的产生、理解及其嵌套规则,强调了词法作用域的重要性,即变量在声明时就决定了其作用域。接着,对比了词法作用域与动态作用域的区别,并通过实例解释了函数作用域和块作用域,特别是如何通过函数作用域来隐藏变量和避免命名冲突。最后,提到了ES6引入的`let`关键字,使得JavaScript拥有了块作用域。
摘要由CSDN通过智能技术生成

前言

《你不知道的Javascript》上卷中对于作用域和闭包的介绍写得真的太好了!形象生动,通俗易懂,读之前,我脑子中对这块知识只有一团模糊的身影,知道大概是什么东西,也明白它们的工作流程,但是却一直困顿于它们为什么会这样。在阅读《你不知道的Javascript》上卷以及参考各路文章后,终于触及到了它们的真面目。

本文参考资料

《你不知道的Javascript》上卷
《JavaScript高级程序设计》第四版
深入理解javascript原型和闭包


一、作用域

在直接给作用域下定义之前,我们先来看看“作用域”到底怎么来的,又是到底怎么用的。

1.1 编译原理

我们先明确一个概念,JavaScript是一门编译语言。但是它跟传统的编译语言不同,它不是提前编译的,编译的结构也不可以在分布式系统中进行移植。但是,JavaScript引擎进行编译的步骤和传统的编译语言非常相似。

传统编译语言的流程中,一段源代码在执行之前,一般会经历这仨步骤:词法分析 → 语法分析 → 代码生成;举例说 var a = 2 ,词法分析会把它分解成词法单元:var、a、=、2、;。语法分析是将词法单元流转换成语法树,代码生成就是将语法树转换成可执行代码(机器指令)

1.2 理解作用域

在JavaScript中,代码片段在执行前的编译离不开以下仨朋友:

引擎:从头到尾负责整个JavaScript程序的编译及执行过程
编译器:负责语法分析代码生成
作用域负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则确定当前执行的代码对这些标识符的访问权限

var a = 2为例,在var a = 2这段代码,可别以为它只是一句声明:引擎认为,这里有两个完全不同的声明,一个由编译器在编译时处理,另一个则由引擎在运行时处理。

在这里插入图片描述

1.3 作用域嵌套

作用域是根据名称查找变量的一套规则。但实际情况中,通常需要同时顾及好几个作用域。

当一个块或函数嵌套在另一个块或函数中时,就发生了作用域的嵌套。在当前作用域中无法找到某个变量时引擎就会在外层嵌套的作用域中继续查找,直到找到该变量,或抵达最外层的作用域(也就是全局作用域)为止。

遍历嵌套作用域链的规则:引擎从当前的执行作用域开始查找变量,如果找不到,就向上一级继续查找。当抵达最外层的全局作用域时,无论找到还是没找到,查找过程都会停止。


二、词法作用域

作用域有两种工作模型:词法作用域(大多数编程语言采用)和动态作用域(Bash脚本、Perl中的一些模式)

为什么叫词法作用域?因为大部分标准语言编译器的第一个工作阶段叫作词法化,词法化的过程会对源代码中的字符进行检查,如果是有状态的解析过程,还会赋予单词语义。
简单来说,词法作用域就是定义在词法阶段的作用域。是由这个变量和块在哪里写决定,而不是由这个变量和块在哪里调用被决定。词法作用域是一生下来就决定了,并且一般不会再改变。

让我们来看看下面代码:

function foo(a) {
	var b = a * 2;
	function bar(c) {
		console.log( a, b, c );
	}
	bar( b * 3;
}
foo( 2; // 2, 4, 12

查找

当在嵌套的作用域查找变量时,作用域查找会在找到第一个匹配的标识符时停止。在多层的嵌套作用域中可以定义同名的标识符,这叫作“遮蔽效应”——内部的标识符“遮蔽”了外部的标识符。
全局变量会自动成为全局对象(比如浏览器中的window对象)的属性,因此可以不直接通过全局对象的词法名称,而是间接地通过对全局对象属性的引用来对其进行访问。例如window.a。这样我们就可以访问到那些被同名变量所遮蔽的全局变量。但非全局的变量如果被遮蔽了,无论如何都无法被访问到。


三、函数作用域和块作用域

3.1 函数作用域

对函数的传统认知就是先声明一个函数,然后再向里面添加代码。但反过来想也可以带来一些启示:从所写的代码中挑选出一个任意的片段,然后用函数声明对它进行包装,实际上就是把这些代码“隐藏”起来了

3.2 隐藏内部实现

隐藏变量和函数是一个有用的技术,它们都是从最小特权原则中引申出来的,也叫最小授权或最小暴露原则。这个原则是指在软件设计中,应该最小限度地暴露必要内容,而将其他内容都“隐藏”起来,比如某个模块或对象的API设计。

除此之外,“隐藏”作用域中的变量和函数所带来的另一个好处,是可以避免同名标识符之间的冲突。两个标识符可能具有相同的名字但用途却不一样,无意间可能造成命名冲突。冲突会导致变量的值被意外覆盖。作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。

3.3 函数作用域

函数作用域的含义是指,属于这个函数的全部变量都可以在整个函数的范围内使用及复用(事实上在嵌套的作用域中也可以使用)。

我们已经知道,在任意代码片段外部添加包装函数,可以将内部的变量和函数定义“隐藏”起来外部作用域无法访问包装函数内部的任何内容。比如说:

var a = 2;
function foo() { // <—— 添加这一行
	var a = 3;
	console.log( a ); // 3
} // <—— 以及这一行
foo(); // <—— 以及这一行
console.log( a ); // 2

但是这段代码中并不理想,因为它导致了一些额外的问题 :

  1. 声明了一个具名函数foo(),foo这个名称本身“污染”了所在作用域,这里是全局作用域
  2. 必须显式地通过函数名(foo())调用这个函数才能运行其中的代码

针对这个问题,我们其实有解决办法:匿名函数——不需要函数ing,并且能够自动运行

var a = 2;function foo(){ // <—— 添加这一行
	var a = 3;
	console.log( a ); // 3
})(); // <—— 以及这一行
console.log( a ); // 2

!!!包装函数的声明以(function……而不仅是以function……开始。函数会被当作函数表达式而不是一个标准的函数声明来处理。

区分函数声明和表达式最简单的方法是看function关键字出现在声明中的位置(不仅仅是一行代码,而是整个声明中的位置)。如果function是声明中的第一个词那么就是一个函数声明,否则就是一个函数表达式

3.4 块作用域

ES6引入了新的let关键字,提供了除var以外的另一种变量声明方式。

let关键字可以将变量绑定到所在的任意作用域中(通常是{ ..}内部)。换句话说,let为其声明的变量隐式地了所在的块作用域




闭包的内容下一篇再写吧ORZ,先去学点别的缓缓呜呜呜

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值