详解JS的作用域和闭包

序言

尽管通常将JavaScript归类为“动态”或“解释执行”语言,但实际上它是一门编程语言。但与传统的编译语言不同,它不是提前编译的,编译结果也不能在分布式系统中进行移植。尽管如此,JavaScript引擎进行编译的步骤和传统的编译语言非常相似,在某些环节可能比预想的复杂。

传统的编译语言通常会在一段源代码执行之前经历三个步骤,统称为“编译”。这三个步骤分别是分词/词法分析(Tokenizing/Lexing)、解析/语法分析(Parsing)、代码生成。但对于JavaScript来说,大部分情况下编译发生在代码执行前的几微秒,所以JavaScript引擎不会有大量的时间用来进行优化,他只会在这几微秒内用尽各种办法(比如JIT,可以延迟编译甚至实施重编译)来保证性能最佳。

一、理解作用域

首先要知道以下三个概念:

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

以 var a = 2 为例,首先这段代码是分为两个步骤执行的,第一补是声明var = a,编译器会在当前作用域中查找是否存在一个名为 a 的变量,若存在则忽略该声明,若不存在,则会在当前作用域进行声明。第二步是赋值,在运行时引擎会在作用域中查找该变量,然后将 2 赋值给 a

如果查找的目的是对变量进行赋值,那么就会使用LHS查询,如果目的是获取变量的值,就会使用RHS查询。赋值操作符会导致LHS查询。 = 等号操作符或调用函数是传入参数的操作都会导致关联作用域的赋值操作。

思考一下下面一段代码有几处LHS和RHS查询:

function foo(a) {
	var b = a;
	return a + b;
}

var c = foo(2);

答案是LHS查询有三处,分别是(1) c = …;c被赋值。(2)a = 2;隐式变量分配,也就是foo(2)传入foo(a),相当于a = 2。(3)b = …;b被赋值。
LRH查询有四处,分别是(1)函数foo(a)获取a的值。(2)b = a 获取a的值。(3)(4)return a + b 获取一次a获取一次b

二、词法作用域

大部分标准语言编译器的第一个工作阶段叫做词法化。简单地说,词法作用域就是定义在词法阶段的作用域。换句话说,词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的,因此当词法分析器处理代码时会保持作用域不变(大部分情况下是这样的)。

三、函数作用域

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

1.匿名函数和具名函数

顾名思义,匿名函数是没有名称标识符的函数(比如回调函数),具名函数是有名称标识符的函数。

//匿名函数
setTimeout(function(){
	console.log("I wait 1000ms")
},1000)
//具名函数
function hello(){
	console.log("Hello World")
}

2. 立即执行函数(IIFE)

var a = 2
(funcrion IIFE(global){
	var a = 3;
	console.log(a); //3
	console.log(global.a); //2
})(window);

console.log(a); //2

上面这个函数就是立即执行函数,函数别包含在一对( )括号内部,因此成了一个表达式,通过在末尾加上另外一个( )可以立即执行这个函数。第一个( )将函数变成了表达式,第二个( )执行了这个函数。并且第二个( )可以对第一个( ) 里的函数进行传参。

四、块作用域

定义:块作用域是一个用来对之前最小授权原则进行拓展的工具,将代码从在函数中隐藏信息扩展为在块中隐藏信息。

对于大家常用的for循环来说,为什么要把一个只在for循环内部使用的变量 i,污染到整个函数作用域中呢?

除了for循环之外,ES3规范中规定 try/catch 的 catch 分句也会创建一个块作用域,其中声明的变量仅在 catch 内部有效。

1. let

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

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

2. 垃圾收集

考虑以下代码:

function process(data) {
	//do something
}
var someData = { .. }
process( someData )

var btn = document.getElementById("my_button")
btn.addEventListeer("click", function click(evt){
	console.log("button clicked")
}, false)

click 函数的点击回调并不需要 someData 变量。理论上这意味着当process()执行完后,在内存中占用大量空间的数据结构就可以被垃圾回收了。但是,由于click函数形成了一个覆盖整个作用域,JavaScript引擎极有可能依然保存着这个结构(取决于具体实现)。

块作用域可以打消这种顾虑,可以让引擎清楚地知道没有必要继续保存someData 了。

function process(data) {
	//do something
}
//在这个块中定义的内容完事可以销毁!
{
    let someData = { .. }
	process( someData )
}

var btn = document.getElementById("my_button")
btn.addEventListeer("click", function click(evt){
	console.log("button clicked")
}, false)

3. let循环

for循环头部的let不仅将 i 绑定到了for循环的块中,事实上它将其重新绑定到了循环的每一个迭代中,确保使用上一个循环迭代结束时的值重新进行赋值。

每个迭代进行重新绑定会在后面讨论闭包时进行说明。

4. const

const也是ES6和let一块引进来用来创建作用域变量的,与其他两个不同的是它声明的值都是固定的,也就是只能用来声明常量。声明之后任何试图修改值的操作都会引起错误。

五、提升

思考下面两段代码

a = 2 
var a
console.log(a)
console.log(a)
var a = 2

我们先来说一下两段代码的输出结果,第一段输出 2,第二段输出的是undefined。

为什么会是这样的呢?正确的思考思路是,包括变量和函数在内的所有声明都会在任何代码被执行前首先被处理。也就是也就是引擎会在解释JavaScript代码之前首先对其进行编译编译阶段中的一部分工作就是找到所有的声明,并用合适的作用域将他们关联起来,也就是之前说的词法作用域的机制。

也就导致了var a永远是在前面的,所以第一段代码被成功赋值,第二段代码为赋值而抛出undefined而非未声明的referenceError。

函数的声明与函数表达式

函数的声明是以function foo () { … }的方式声明的函数

函数表达式是 var foo = function () { … }

foo() //函数的声明会被提升
function foo(){
	console.log(a) //undefined
	var a = 2
}
foo() // 不是referenceError,而是TypeError,函数表达式不会被提升
var foo = function(){ .. }

1. 函数优先

函数声明和变量声明都会被提升。但是函数会被首先提升,然后才是变量。重复的var声明会被忽略掉,出现在后面的函数声明会覆盖前面的。

foo() //3

function foo() {
	console.log(1)
}

var foo = function() {
	console.log(2)
}

function foo() {
	console.log(3)
}

无论作用域中的声明出现在什么地方,都将在代码本身被执行前首先进行处理。可以将这个过程想象成所有的声明(变量和函数)都会被“移动”到各自作用域的最顶端,这个过程被称为提升。

六、作用域闭包

1. 什么是闭包

函数在当前语法作用域之外执行,也可以函数记住并访问所在的词法作用域

我们来看一段代码,清晰地展示了闭包:

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() 显然可以被正常执行。但是在这个例子中,它在自己定义的词法作用域以外的地方执行。

在foo()执行后,通常会期待fo()的整个内部作用城都被销毁,因为我们知道引擎有垃圾回收器用来释放不再使用的内存空间。由于看上去foo()的内容不会再被使用,所以很自然地会考虑对其进行回收。

而闭包的“神奇”之处正是可以阻止这件事情的发生。事实上内部作用域依然存在,因此没有被回收。谁在使用这个内部作用域?原来是bar()本身在使用。

拜bar()所声明的位置所赐,它拥有涵盖foo() 内部作用域的闭包,使得该作用域能够一直存活,以供bar()在之后任何时间进行引用。

bar()依然持有对该作用域的引用,而这个引用就叫作闭包。

2. 循环和闭包

要说明闭包,for循环是最常见的例子。

for (var i = 0; i <= 5; i++) {
	setTimeout( function timer() {
		console.log( i )
	}, i * 1000)
}

相信大家的预期效果都是分别输出数字1-5,每秒一次,每次一个。

但实际上,这段代码在运行时会以每秒一次的频率输出五次6。

这是为什么呢?

很显然,当for循环5次后,i = 6,然后跳出循环。这时候延迟函数的回调才会开始执行,所以才会输出五个6来。

缺陷是我们试图假设循环中的每个迭代在运行时都会给自己“捕获”一个 i 的副本。但是根据作用域的工作原理,实际情况是尽管循环中的五个函数是在各个迭代中分别定义的,但是它们都被粉笔仔一个共享的全局作用域中,因此实际上只有一个 i。

那么我们要怎么解决呢?可以使用前面说到过的IIFE立即执行一个函数来创建作用域。我们来试一下

for (var i = 0; i <= 5; i++) {
	(function() {
		setTimeout( function timer() {
            console.log( i )
        }, i * 1000)
	})()
}

你以为这样就可以了吗?其实不然,显然我们拥有了更多的词法作用域,但是这个作用域没有起到对 i 的封闭作用,很显然 i 的赋值是在作用域外的。那么我们再改进一下

for (var i = 0; i <= 5; i++) {
	(function() {
		var j = i
		setTimeout( function timer() {
            console.log( j )
        }, j * 1000)
	})()
}

这样!他就能正常地工作了!也许还能再改进一下

for (var i = 0; i <= 5; i++) {
	(function(j) {
		setTimeout( function timer() {
            console.log( j )
        }, j * 1000)
	})(i)
}

还记得我们前面说过的块作用域吗?除了创建一个新的作用域外,我们还可以通过let劫持块作用域,并且在这个块作用域中声明一个变量。本质上是将一个块转换成一个可以被关闭的作用域。

代码如下:

for (var i = 0; i <= 5; i++) {
	let j = i	//闭包的块作用域!
	setTimeout( function timer() {
		console.log( j )
	}, j * 1000)
}

或许我们还能再进行优化

for (let i = 0; i <= 5; i++) {
	setTimeout( function timer() {
		console.log( i )
	}, i * 1000)
}

这样我们就大功告成了!

3. 模块

相信写过一点项目的同学都会知道吧,就是我们的js文件不会都写在一块,而是在一个模块对其暴露再在另一个需要的模块进行导入,最后将主要模块集合在一个js文件进行管理,这样会使我们的代码脉络更加清晰以及更好地分工合作。

4. 未来的模块机制

ES6中为模块增加了一级语法支持。在通过模块系统进行加载时,ES6会将文件当作独立的模块来处理。每个模块都可以导入其他模块或特定的API成员,同样也可以导出自己的API成员。

ES6的模块没有“行内”格式,必须被定义在独立的文件中(一个文件一个模块)。浏览器或者引擎有一个默认的“模块加载器”(可以被重载,但这远超出我们的讨论范围)可以在导入模块时同步地加载模块文件。

考虑以下代码:

bar.js
	function hello(who) {
		return "Let me intriduce:" + who
	}
	
	export hello
	
foo.js
	//仅从"bar"模块导入hello()
	import hello from "bar"
	
	var hungry = "hippo"
	
	function awesome() {
		console.log(
			hello( hungry ).toUpperCase()
		)
	}
	
	export awesome
	
baz.js
	//导入完整的"foo"和"bar"模块
	module foo from "foo"
	module bar from "bar"
	
	console.log(
		bar.hello("rhino")
	)	//Let me intriduce: rhino
	
	foo.awecome() //Let me intriduce: hippo

import 可以讲一个模块中的一个或多个API导入到当前作用域中,并分别绑定在一个变量上(在我们的例子里是hello)。module会将整个模块的API导入并绑定到一个变量上(在我们的例子里是foo 和 bar)。export会将当前模块的一个标识符(变量,函数)导出为公共API。这些操作可以在模块定义中根据需要使用任意多次。

本文所有用例,部分话语引用自 Scope and Closures, Kyle Simpson 著(O’Reilly,2014)。版权所有,978-1-491-33558-8。

已标记关键词 清除标记
相关推荐
<p> <span style="color:#337FE5;"><strong>【为什么还需要学习C++?】</strong></span> </p> <p style="margin-left:0cm;"> 你是否接触很多语言,但从来没有了解过编程语言的本质? </p> <p style="margin-left:0cm;text-align:start;"> 你是否想成为一名资深开发人员,想开发别人做不了的高性能程序? </p> <p style="margin-left:0cm;text-align:start;"> 你是否经常想要窥探大型企业级开发工程的思路,但苦于没有基础只能望洋兴叹? </p> <p style="margin-left:0cm;text-align:start;">   </p> <p style="margin-left:0cm;text-align:start;"> 那么C++就是你个人能力提升,职业之路进阶的不二之选。 </p> <p style="margin-left:0cm;text-align:start;"> <br /> </p> <p style="margin-left:0cm;text-align:start;"> <br /> </p> <p style="margin-left:0cm;"> <strong><span style="color:#337FE5;">【课程特色】</span></strong> </p> <p style="margin-left:0cm;text-align:start;"> 1.课程共19大章节,239课时内容,涵盖数据结构、函数、类、指针、标准库全部知识体系。 </p> <p style="margin-left:0cm;text-align:start;"> 2.带你从知识与思想的层面从0构建C++知识框架,分析大型项目实践思路,为你打下坚实的基础。 </p> <p style="margin-left:0cm;text-align:start;"> 3.李宁老师结合4大国外顶级C++著作的精华为大家推出的《征服C++11》课程。 </p> <p style="margin-left:0cm;text-align:start;"> <br /> </p> <p class="ql-long-24357476"> <span style="color:#337FE5;"><strong>【学完后我将达到什么水平?】</strong></span> </p> <p class="ql-long-24357476"> 1.对C++的各个知识能够熟练配置、开发、部署; </p> <p class="ql-long-24357476"> 2.吊打一切关于C++的笔试面试题; </p> <p class="ql-long-24357476"> 3.面向物联网的“嵌入式”和面向大型化的“分布式”开发,掌握职业钥匙,把握行业先机。 </p> <p class="MsoNoSpacing" style="margin-left:18pt;"> <br /> </p> <div> <br /> </div> <p> <br /> </p> <p style="margin-left:0cm;text-align:start;"> <span style="color:#337FE5;"><strong>【面向人群】</strong></span> </p> <p style="margin-left:0cm;text-align:start;"> <span style="color:#222226;font-family:PingFangSC-Regular, "font-size:14px;background-color:#FFFFFF;">1.希望一站式快速入门的C++初学者;</span> </p> <p style="margin-left:0cm;text-align:start;"> <span style="color:#222226;font-family:PingFangSC-Regular, "font-size:14px;background-color:#FFFFFF;">2.希望快速学习 C++、掌握编程要义、修炼内功的开发者;</span> </p> <p style="margin-left:0cm;text-align:start;"> <span style="color:#222226;font-family:PingFangSC-Regular, "font-size:14px;background-color:#FFFFFF;">3.有志于挑战更高级的开发项目,成为资深开发的工程师。</span> </p> <p style="margin-left:0cm;text-align:start;"> <br /> </p> <p> <br /> </p> <p> <span style="color:#337FE5;"><strong>【课程设计】</strong></span> </p> <p> 本课程包含3大模块 </p> <p> <strong>基础篇</strong><br /> 本篇主要讲解c++的基础概念,包含数据类型、运算符等基本语法,数组、指针、字符串等基本词法,循环、函数、类等基本句法等。 </p> <p> <br /> <strong>进阶篇</strong><br /> 本篇主要讲解编程中常用的一些技能,包含类的高级技术、类的继承、编译链接和命名空间等。 </p> <p> <br /> <strong>提升篇:</strong><br /> 本篇可以帮助学员更加高效的进行c++开发,其中包含类型转换、文件操作、异常处理、代码重用等内容。 </p> <p> <img src="https://img-bss.csdnimg.cn/202007091130239667.png" alt="" /> </p>
韦东山老师为啥要录升级版嵌入式视频?<br /><br /> 200x年左右,嵌入式Linux在全世界、在中国刚刚兴起。<br /> 我记得我2005年进入中兴时,全部门的人正在努力学习Linux。<br /> 在2008年,我写了一本书《嵌入式Linux应用开发完全手册》。<br /> 它的大概内容是:裸机、U-boot、Linux内核、Linux设备驱动。<br /> 那时还没有这样讲解整个系统的书,<br /> 芯片厂家Linux开发包也还不完善,从bootloader到内核,再到设备驱动都不完善。<br /> 有全系统开发能力的人也很少。<br /> 于是这书也就恰逢其时,变成了畅销书。<br /> 我也根据这个思路录制了视频:裸机、U-boot、Linux内核、Linux设备驱动。<br /> 收获些许名声,带领很多人进入Linux世界。<br /><br /><strong>11年过去了,嵌入式Linux世界发生了翻天覆地的变化</strong><br /><br /> ① 基本系统能用<br /><br /> 芯片厂家都会提供完整的U-boot、Linux内核、芯片上硬件资源的驱动。<br /> 方案厂家会做一些定制,比如加上某个WIFI模块,会添加这个WIFI模块的驱动。<br /> 你可以使用厂家的原始方案,或是使用/借鉴方案商的方案,做出一个“能用”的产品。<br /><br /> ② 基础驱动弱化;高级驱动专业化<br /><br /> 基础的驱动,比如GPIO、UART、SPI、I2C、LCD、MMC等,有了太多的书籍、视频、示例代码,修修改改总是可以用的。<br /> 很多所谓的驱动工程师,实际上就是“调参工程师”。<br /> 我们群里有名的火哥,提出了一个概念:这些驱动就起一个“hardware enable”的作用。<br /> 高级的驱动,比如USB、PCIE、HDMI、MIPI、GPU、WIFI、蓝牙、摄像头、声卡。<br /><br /> 体系非常复杂,很少有人能讲清楚,很多时候只是一笔带过。<br /> 配置一下应用层工具就了事,能用就成。<br /> 这些高级驱动,工作中需要专门的人来负责,非常专业。<br /> 他们是某一块的专家,比如摄像头专家、音频专家。<br /><br /> ③ 项目为王<br /> 你到一个公司,目的是把产品做出来,会涉及APP到内核到驱动全流程。<br /> 中小公司玩不起华为中兴的配置,需要的是全面手。<br /> 大公司里,只负责很小很小一块的镙丝钉,位置也不太稳固啊。<br /> 所以,如果你不是立志成为某方面的专家,那就做一个全栈工程师吧。<br /><br /> ④ 调试很重要<br /> 都说代码是3分写7分调,各种调试调优技术,可以为你的升职加薪加一把火。<br /> 基于上述4点,我录制的全新视频将有这些特点:<br /> 1. 快速入门,<br /> 2. 实战项目,<br /> 3. 驱动大全,<br /> 4. 专题,<br /> 5. 授人以渔,<br /> 6. 要做任务<br /> 另外,我们会使用多款芯片同时录制,先讲通用的原理,再单独讲各个板子的操作。<br /> 这些芯片涵盖主流芯片公司的主流芯片,让你学习工作无缝对接。<br /><img src="https://img-bss.csdn.net/201911180753564269.jpg" alt="" /><br /><br /><br /><br /> 1.快速入门<br /> 入门讲究的是快速,入门之后再慢慢深入,<br /> 特别是对于急着找工作的学生,对于业余时间挑灯夜读的工作了的人,一定要快!<br /> 再从裸机、U-boot、内核、驱动这样的路线学习就不适合了,时间就拉得太长了。<br /> 搞不好学了后面忘了前面。<br /> 并且实际工作中并不需要你去弄懂U-boot,会用就行:U-boot比驱动还复杂。<br /><br /> 讲哪些内容?<br /><img src="https://img-bss.csdn.net/201911180754297078.png" alt="" /><br /><br /> 怎么讲呢?<br /><br /> 混着讲<br /> 比如先讲LED APP,知道APP怎么调用驱动,再讲LED硬件原理和裸机,最后讲驱动的编写。<br /> 这样可以快速掌握嵌入式Linux的整套开发流程,<br /> 不必像以前那样光学习裸机就花上1、2个月。<br /> 而里面的裸机课程,也会让你在掌握硬件操作的同时,把单片机也学会了。<br /><br /> 讲基础技能<br /><br /> 中断、休眠-唤醒、异步通知、阻塞、内存映射等等机制,会配合驱动和APP来讲解。<br /> 这些技能是嵌入式Linux开发的基础。<br /> 而这些驱动,只会涉及LED、按制、LCD等几个驱动。<br /> 掌握了这些输入、输出的驱动和对应的APP后,你已经具备基本的开发能力了。<br /><br /> 讲配置<br /> 我们从厂家、从方案公司基本上都可以拿到一套完整的开发环境,怎么去配置它?<br /> 需要懂shell和python等配置脚本。<br /><br /><br /> 效果效率优先<br /> 以前我都是现场写代码、现场写文档,字写得慢,降低了学习效率。<br /> 这次,效果与效率统一考虑,不再追求所有东西都现场写。<br /> 容易的地方可先写好代码文档,难的地方现场写。<br /><br /> 2.实战项目<br /> 会讲解这样的涉及linux网关/服务器相关项目(不限于,请多提建议):<br />  <img src="https://img-bss.csdn.net/201911180754541383.jpg" alt="" />            <br />       <br /> 定位为:快速掌握项目开发经验,丰满简历。<br /> 涉及的每一部分都会讲,比如如果涉及蓝牙,在这里只会讲怎么使用,让你能写出程序;如果要深入,可以看后面的蓝牙专题。<br /><br /> 3. 驱动大全<br /> 包括基础驱动、高级驱动。<br /> 这些驱动都是独立成章,深入讲解。<br /> 虽然基础驱动弱化了,但是作为Linux系统开发人员,这是必备技能,并且从驱动去理解内核是一个好方法。<br /> 在讲解这些驱动时,会把驱动的运行环境,比如内核调度,进程线程等概念也讲出来,这样就可以搭建一个知识体系。<br /> 没有这些知识体系的话,对驱动的理解就太肤浅了,等于在Linux框架下写裸机,一叶障目,不见泰山。<br /> 定位为:工具、字典,用到再学习。<br /><br /> 4. 专题<br /> 想深入学习的任何内容,都可独立为专题。<br /> 比如U-boot专题、内核内存管理专题、systemtap调试专题。<br />
©️2020 CSDN 皮肤主题: 数字20 设计师:CSDN官方博客 返回首页