进击的巨人第三篇,本篇就作用域、作用域链、闭包等知识点,一一击破。
作用域
作用域:负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符(变量)的访问权限
——《你不知道的JavaScript上卷》
作用域有点像圈地盘,大家划好区域,然后各自经营管理,井水不犯河水。
|
|
作用域的变量声明
不同作用域下命名相同的变量不会发生冲突,“就近原则”选取。
|
|
作用域的类型
执行上下文环境有:全局、函数、eval。那么作用域也有三种,ES6新增了块级作用域。
- 全局作用域
- 函数作用域
- eval作用域(不推荐使用eval,暂时忽略)
- 块级作用域(ES6新增)
全局作用域
JavaScript中全局环境只有一个,对应的全局作用域也只有一个。没有用var/let/const
声明的变量默认都会成为全局变量。
|
|
函数作用域
ES6之前,想要实现局部作用域的方式,都是是通过在函数中声明变量来实现的,所以也称函数作用域,支持嵌套多个。
|
|
函数中声明变量时,建议在函数起始部分声明所有变量,方便查看,切记要用var/let/const
声明,防止手抖将局部变量变成成全局变量。
|
|
块级作用域
我们先来理解什么是块?所谓块,其实就是被大括号{}
包裹的代码部分。
ES6前没有块级作用域的概念,所以{}
中并没有自己的作用域。如果我们想在ES5的环境下构建块级作用域,一般都是是通过立即执行函数来实现的。
|
|
ES5借助函数作用域来实现块级作用域的方式,会让我们的代码充斥大量的立即执行函数(IIFE),不便于代码的阅读。好的代码的就跟好的文章一样,让阅读的人读来舒畅明了。
为此,ES6新增块级作用域的概念,使用let/const
声明变量的方式,即可将其作用域指定在代码块中,跟函数作用域一样支持嵌套。
|
|
let/const
不允许变量提升,必须“先声明再使用”。这种限制,称为“暂时性死区”。这也能让我们在代码编写阶段变得更加规范化,执行跟书写顺序保持一致。
作用域链(变量查询规则)
变量被作用域所管理,那么变量在作用域中的查找规则,就是所谓的作用域链。
作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问
——《JavaScript高级程序涉及》
“在当前执行环境开始查找使用到的变量,如果找到,则返回其值。如果找不到,会逐层往上级(父作用域)查找,直到全局作用域”。
|
|
自由变量
变量我们见的不少,但”自由变量”听着是不是挺唬人的。其实对它,我们并不陌生。
“自由变量:当前执行环境使用到,但并未在当前执行环境声明的变量(函数参数arguments排除)”
函数调用时,进入执行上下文创建阶段,会对argument
进行隐式的变量声明。
|
|
“自由变量的作用域由词法环境决定,也就是它的作用域在代码书写阶段就已经确定了,而不是在代码编译执行阶段确定。”
“自由变量的值是在代码执行时确定的,变量变量变量,值肯定要变,所以自由变量的值只有在程序运行阶段才能确定。”
闭包
开篇第一文我们就执行环境,执行栈做出了详解,有所遗忘的可再温习。执行栈是我们理解闭包原理基础中的基础。
函数调用栈过程的图再晒出来,顺便温习下。
|
|
函数调用时入栈,调用结束出栈。执行函数时,会创建一个变量对象去存储函数中的变量,方法,参数arguments
等,结束调用时,该变量对象就会被销毁。(理想的情况下,不理想的情况就是出现“闭包”调用了)。
什么是闭包?
闭包是指有权访问另外一个函数作用域的变量的函数。
——《JavaScript高级程序设计》
闭包是指那些能够访问自由变量的函数。
——MDN
闭包的特点首先是函数,其次是它可以访问到父级作用域的变量对象,即使父级函数完成调用后“理应出栈销毁”。
判定闭包出现
- 函数作为参数传递
- 函数作为返回值传递
|
|
对函数中谁是闭包,各文档解释不一。在此我们遵照Chrome的方式,暂且称foo
是闭包。
因为作用域和作用域链规则的限定,子环境的自由变量只能逐层向上到父环境查找。
但是通过闭包,我们在外部环境也可以获取到变量fooVal
,虽然foo()
函数执行完成了,但它并没从函数调用栈中销毁,其变量对象存储仍然能被访问到。
实际执行过程请看图:
把上述代码改以下,接着看:
答案与结果不符的小伙伴要回头理解下自由变量了。“自由变量的作用域在代码书写时(函数创建时)就确定了”,所以函数中getValue()
使用的fooVal
在foo
的作用域下,而不是在全局作用域下。
答对的小伙伴们再来一道题,加深你的记忆
|
|
题目解析:max
作为函数bar
中的自由变量,它的作用域在函数bar
创建的时候就确定了,就是函数fn
中的max
,所以它的作用域链查找到fn
中已经结束并返回了,不会再向上找到全局作用域。
注意:栈中存储的不只是闭包中使用到的自由变量,而是父级函数的整个变量对象(父级函数作用域中声明的方法,变量,参数等)
闭包的应用场景
上文中已经阐述了闭包的特点,就是能够让我们跨作用域取值(不局限于父子作用域)。列举两个实际开放中常用的栗子:
封装回调保存作用域
12345678for(var i = 1; i < 5; i++) {setTimeout((function(i){return function() {console.log(i);}})(i), i * 1000)}// 原理:通过自执行函数传参i,然后返回一个函数(闭包)中使用i,使父函数的变量对象一直存在私有变量和方法实现模块化
1234567891011121314151617181920var makePeople = function () {var _name = '以乐之名';return {getName: function () {console.log(_name);},setName: function (name) {if (name != 'Hello world') {_name = name;}}}}var me = makePeople();me.getName(); // '以乐之名'me.setName('KenTsang');me.getName(); // 'KenTsang'// 原理:私有变量_name没有对外访问权限,但通过闭包使其一直保留在内存中,可以被外部调用
闭包的应用场景还有很多,具体实际情况还需具体分析。
闭包造成的内存泄露
闭包的使用,破坏了函数的出栈过程。解释执行栈的时候,讲到同个函数即使调用自身,创建的变量对象也并非同一个,其内存存储是各自独立的。
栈中只入不出,函数的变量对象没有被有效回收,就会造成浏览器内存占用逐步增加,内存占用过高的情况下,就会导致页面卡顿,甚至浏览器崩溃。这就是我们常说的闭包造成的“内存泄露”。
所以,一名合格的前端,除了会用闭包,还要正确的解除闭包引用。
垃圾回收机制讲解时,通过设置变量值为null
时可已解除变量的引用,以便下一次垃圾回收销毁它。
|
|
写在结尾
闭包算是前端初学者的一个难点,能解释清楚并不容易,涉及到作用域,执行上下文环境、变量对象等等。
零散知识的内聚汇总,正是是系列更文的初衷所在。
知识不是小段子,听完笑过就忘,唯有形成体系,达成闭环,才能深植入记忆中。
参考文档:
本文首发Github,期待Star!
https://github.com/ZengLingYong/blog
作者:以乐之名
本文原创,有不当的地方欢迎指出。转载请指明出处。