JavaScript中关于预编译、作用域链和闭包的理解


Posted in Javascript onMarch 31, 2021

一.预编译

首先是对预编译

1概念

在JavaScript中存在一种预编译的机制,这也是Java等一些语言中没有的特性,也就正是因为这个预编译的机制,导致了js中变量提升的一些问题。js引擎在运行代码时的基本步骤为:语法分析(通篇扫描)—> 预编译 —> 解释执行(解释一行,执行一行)

1.1什么是预编译

首先,我们知道,JavaScript是解释性语言,即逐行解析,逐行执行。那么,什么是预编译呢?

在JavaScript真正被解析前,js解析引擎会首先把整个文件进行预处理,从而来消除一些歧义,这个预处理的过程就被称为预编译

console.log(a)
var a = 123
console.log(a)
function a(){
	console.log(a)
}
a()

比如上面这个看似简单的代码,输出的结果可能跟我们说想的不太一样
输出结果:
JavaScript中关于预编译、作用域链和闭包的理解

可以看出,第一次输出的是一个函数,第二次输出的是123,而第三次输出就报错提示a不是一个函数。如果要完全理解,就要深入理解js引擎到底是怎么工作的,下面我会为大家逐一讲解

1.2全局对象(Global Object)

  • 在浏览器中,js引擎会整合<script>标签中的内容,产生window对象,这个window对象就是全局对象(整合表示js引擎会将代码中的全部js代码整合在一起,不管是引入的还是分别写在几个<script>中的js代码,浏览器加载后都会变成一个,在一个标签中的定义全局变量在其他标签也能使用
  • 在node环境中,会产生global对象

  全局变量与全局函数

  • 在<script>标签中声明的变量就为全局变量,全局变量会作为window对象的属性存在
  • 在<script>标签中声明的函数就为全局函数,全局函数会作为window对象的方法存在。

  例:

var obj = 123 
console.log(a)	//100
console.log(window.a)	//100

打印a和window对象的a是一样的,其实这两个表示的就是一个值,全局变量保存在window对象中

总结

当js引擎在执行js代码的时候,会先生成一个global空间,里面存储的是全局变量与全局函数,这也能够解释js代码的整合,此global空间一旦生成,就会一直存在,不会被系统回收,其中的全局变量也会一直存在。(global对象可在代码执行时,通过浏览器中的sources来进行查看。)

1.3活动对象(Activation Object)

  • 活动对象也叫激活对象(在函数被调用才会被激活)

  • 在函数被调用时产生,用来保存当前函数内部的执行环境(Execute Context),也叫执行期上下文

  • 在函数调用结束时销毁

局部变量与局部函数

例:

function a(){
	var i = 0
	console.log(i)
}
a()

在函数a的外部不能访问变量i,变量i只在函数a的范围内才能使用,这就是作用域的由来

  • 在函数内部声明的变量叫做局部变量,局部变量作为AO对象的属性存在
  • 如果不执行函数,不会产生AO对象,就不会存在i属性
  • 如果执行函数,就会产生AO对象,并将变量i作为AO对象的属性
  • 函数执行完过后,AO对象也被销毁,也就意味着不能使用i属性

在函数内部声明的函数叫局部函数,局部函数作为AO对象的方法存在

例:

function a(){
	function b(){
		console.log("hello world")
	}
	b() 
}
a()

当函数被调用时,系统就会为此函数分配一个空间,用于存储此函数中的变量,如例我们称a()为外部函数,b()为内部函数,当内部函数被调用时,会在外部函数已建立的空间基础上,再分配一个空间,用于执行内部函数,当内部函数执行玩后,空间销毁,外部函数执行完后,也会被销毁。值得注意的是,如果a()方法被同时调用两次,此时会开辟两个不同的空间,相互独立,互不影响,理解这个概念有注意后续理解闭包的概念。

2全局预编译

2.1流程

  1. 查找变量声明,作为GO对象的属性名,值为undefined
  2. 查找函数声明,作为GO对象的属性名,值为function

变量声明

var a  // 变量声明
var b  //变量声明 + 变量赋值

通过function关键字声明函数

function a() {}   // 函数声明
var a = function() {}  // 函数表达式,不是函数声明

我们再回到刚开始的例子:

console.log(a)  //function a(){}
	var a = 123		
	console.log(a)	//123
	function a(){
		console.log(a) // 没有执行
	}
	a()  // a is not a function
  1. 首先产生window对象
  2. 查找变量声明,预编译首先找到a,将a作为window对象的属性名,属性值为undefined
  3. 查找函数声明,找到a的函数声明,将a作为window对象的属性名,属性值为function()
  4. 全局预编译结束后,代码从上到下依次执行

所以在函数预编译结束后,代码从上到下依次执行,第一行打印出的就为function() {},再通过第二行将123赋值给a,所以第三行打印出来的就是123,此时a为一个number类型,故在最后调用的时候会显示a is not a function

2.2结论

在函数预编译的时候,如果存在同名的变量和函数,函数的优先级高

3函数预编译

3.1流程

  1. 在函数被调用时,为当前函数产生AO对象
  2. 查找形参和变量声明作为AO对象的属性名,值为undefined
  3. 使用实参的值改变形参的值
  4. 查找函数声明,作为AO对象的属性名,值为function

例:

function a(test){
	var i = 0
	function b(){
		console.log("hello world")
	}
	b()
}
a(1)

预编译过程:

  1. 产生window对象
  2. GO:查找变量声明
  3. 查找函数声明,将函数a作为window对象的属性名,值为function
  4. 全局预编译结束,执行代码
  5. 执行到a(1)时,此时开始对函数预编译,产生AO对象
  6. 查找函数中的形参和变量,作为AO对象的属性值,此时变量test和i的值都为undefined
  7. 使用实参的值改变形参的值,即变量test的值改变为1
  8. 查找函数声明b,作为AO对象的属性名,值为function
  9. 预编译结束,逐行执行调用的函数
  10. 函数执行结束,系统收回为其开辟的AO空间

3.2结论

  1. 只要声明的局部函数,函数的优先级最高
  2. 没有声明局部变量,实参的优先级高
  3. 整体来说:局部函数 > 实参 > 形参和局部变量

二.作用域域作用域链

1.概念

1.1域

在js中,作用域分为全局作用域局部作用域

  • 全局作用域:由<script>标签产生的区域,从计算机的角度可以理解为window对象
  • 局部作用域:由函数产生的区域,从计算机的角度可以理解为该函数的AO对象

1.2作用域链

在js中,函数存在一个隐式属性[[scopes]],这个属性用来保存当前函数在执行时的环境,由于在数据结构上是链式的,也被称作域链,我们可以把它理解为一个数组

function a(){}
console.dir(a) // 打印内部结构

JavaScript中关于预编译、作用域链和闭包的理解
例:

function a(){
	console.dir(a)
	function b(){
		console.dir(b)
		function c(){
		console.dir(c)
		}
		c()
	}
	b()
}
a()

产生过程:
(前面的数字表示下标,如上文解释能将其理解为数组,aAO表示a的AO对象,并且在函数预编译时添加了b这个属性,其值为function)

  1. 产生a函数的AO对象,aAO
        函数a的scopes:
            0:aAO={b:function}   
            1:GO
  2. 产生函数的AO对象,bAO:
        函数b的scopes
            0:bAO={c:function}
            1:aAO={b:function}
            2:GO
  3. 产生c函数的AO对象,cAO
        函数c的scopes
            0:cAO
            1:bAO={c:function}
            2:aAO={b:function}
            3:GO

2.作用

作用域链的作用是什么?
在访问变量或者函数时,会在作用域链上依次查找,最直观的表现就是:

  • 内部函数能够使用外部函数声明的变量
function a(){
	var aa = 111
	function b(){
		console.log(aa)
	}
	b()
}
a()
  • 在函数a中定义了变量aa
  • 在函数b中没有声明,却可以使用

如果在函数b中,也定义同名变量aa会怎样

例:

function a(){
	var aa = 111
	function b(){
		var aa = 222
		console.log(aa)
	}
	b()
}
a()

第一个问题:函数a和函数b中的变量aa是不是同一个变量?
第二个问题:函数b中打印的aa是用的是谁的?

结论
内部函数能使用外部函数的变量
外部函数不能使用内部函数的变量

三.闭包

如果在内部函数使用了外部函数的变量,就会形成闭包,闭包保留了外部环境的引用
如果内部函数不返回到了外部函数的外卖,在外部函数执行完后,依然可以使用闭包里的值

1.闭包的形成

在内部函数使用外部函数的变量,就会形成闭包,闭包是当前作用域的延伸

例:

function a(){
	var aa = 100
	function b(){
		console.log(aa)
	}
	b()
}
a()

演示
JavaScript中关于预编译、作用域链和闭包的理解
此时在我认为已经是形成了闭包,但是这个闭包并不能保持住,即函数执行完过后AO对象都会被回收,有一些书籍对闭包的定义是当形成了能够持续的闭包,才算是一个真正的闭包,在此我的个人观点是此时已经形成了闭包,但是没有保持。

例:

function a(){
	var aa = 100
	function b(){
		var b = 100
		console.log(b)
	}
	b()
}
a()

会不会形成闭包呢?

答案

JavaScript中关于预编译、作用域链和闭包的理解
不会形成闭包,由于在b函数内定义了变量b,打印时使用的是内部函数里的变量b,不会形成闭包

2.闭包的保持

如果希望在函数调用后,闭包依然保持,就需要将内部函数返回到外部函数的外部

function a(){
	var num = 0
	function b(){
		console.log(num++) // 形成闭包
	}
	return b
}
var demo = a()  // 调用a函数,将内部函数b返回,保存在函数a的外部
console.dir(demo) // 调用demo函数,实质上是调用内部函数,在函数b的[[scopes]]属性中可以找到闭包对象,从而访问到里面的值
demo() // 0
demo() // 1

JavaScript中关于预编译、作用域链和闭包的理解

3.总结

使用闭包要满足的两个条件

  1. 闭包要形成:在内部函数使用外部函数的变量
  2. 闭包要保持:内部函数返回到外部函数的外面

四.闭包的应用

1.闭包的两面性

好处:一般来说,在函数外部是没有办法访问函数内部的变量的,设计闭包的作用就是为了解决这个问题
坏处:使用了闭包,可能会导致出现意想不到的结果

2.闭包的应用

  1. 在函数外部访问私有变量
  2. 实现封装
  3. 防止污染全局变量

function Person(){
 var name
 function setName(){
	this.name = name
}
function getName(){
	return name
}
return{
	setName:setName,
	getName:getName,
}
var lisi = Person()
lisi.setName("lisi")
var name = lisi.getName()
onsole.log(name)

}
Javascript 相关文章推荐
ExtJS 设置级联菜单的默认值
Jun 13 Javascript
jquery的键盘事件修改代码
Feb 24 Javascript
使用Jquery Aajx访问WCF服务(GET、POST、PUT、DELETE)
Mar 16 Javascript
js Select下拉列表框进行多选、移除、交换内容的具体实现方法
Aug 13 Javascript
window resize和scroll事件的基本优化思路
Apr 29 Javascript
详解JavaScript语法对{}处理的坑爹之处
Jun 05 Javascript
jquery实现的3D旋转木马特效代码分享
Aug 25 Javascript
javascript伸缩型菜单实现代码
Nov 16 Javascript
jQuery 实现双击编辑表格功能
Jun 19 jQuery
ES6正则表达式扩展笔记
Jul 25 Javascript
如何通过vscode运行调试javascript代码
Jul 24 Javascript
vue3+typeScript穿梭框的实现示例
Dec 29 Vue.js
JavaScript 去重和重复次数统计
Mar 31 #Javascript
vue中三级导航的菜单权限控制
Mar 31 #Vue.js
jQuery class属性操作addClass()与removeClass()、hasClass()、toggleClass()
vue3中的组件间通信
vue前端工程的搭建
JS数组的常用方法整理
Mar 31 #Javascript
分享15个Webpack实用的插件!!!
You might like
PHP 的异常处理、错误的抛出及回调函数等面向对象的错误处理方法
2012/12/07 PHP
php操作mongoDB实例分析
2014/12/29 PHP
新浪SAE搭建PHP项目教程
2015/01/28 PHP
Ubuntu server 11.04安装memcache及php使用memcache来存储session的方法
2016/05/31 PHP
Yii2中事务的使用实例代码详解
2016/09/07 PHP
获取客户端电脑日期时间js代码(jquery)
2012/09/12 Javascript
jq选项卡鼠标延迟的插件实例
2013/05/13 Javascript
Dojo Javascript 编程规范 规范自己的JavaScript书写
2014/10/26 Javascript
JavaScript数据结构与算法之链表
2016/01/29 Javascript
简单谈谈javascript中this的隐式绑定
2016/02/22 Javascript
JQuery 设置checkbox值二次无效的解决方法
2016/07/22 Javascript
浅谈Vue.js
2017/03/02 Javascript
vue中路由验证和相应拦截的使用详解
2017/12/13 Javascript
详解Vue基于 Nuxt.js 实现服务端渲染(SSR)
2018/04/05 Javascript
JQuery扩展对象方法操作示例
2018/08/21 jQuery
vue通过video.js解决m3u8视频播放格式的方法
2019/07/30 Javascript
详解webpack打包vue项目之后生成的dist文件该怎么启动运行
2019/09/06 Javascript
JS实现电脑虚拟键盘的操作
2020/06/24 Javascript
一个基于flask的web应用诞生 使用模板引擎和表单插件(2)
2017/04/11 Python
详解python项目实战:模拟登陆CSDN
2019/04/04 Python
PyCharm2018 安装及破解方法实现步骤
2019/09/09 Python
使用pytorch和torchtext进行文本分类的实例
2020/01/08 Python
python 简单的调用有道翻译
2020/11/25 Python
如何用python开发Zeroc Ice应用
2021/01/29 Python
使用Html5中的cavas画一面国旗
2019/09/25 HTML / CSS
Fossil加拿大官网:化石手表、手袋、首饰及配饰
2019/04/23 全球购物
金融管理专业毕业生求职信
2014/03/12 职场文书
学校地质灾害防治方案
2014/06/10 职场文书
干部对照检查材料范文
2014/08/26 职场文书
大型主题婚礼活动策划方案
2014/09/15 职场文书
居委会个人对照检查材料思想汇报
2014/09/29 职场文书
考试作弊检讨书
2014/10/21 职场文书
2014年党的群众路线活动个人整改措施
2014/10/28 职场文书
兵马俑导游词
2015/02/02 职场文书
亲戚关系证明
2015/06/24 职场文书
初二英语教学反思
2016/02/15 职场文书