知识列表作用域/作用域链 闭包(涉及JS垃圾回收机制 )https://zhuanlan.zhihu.com/p/27110726 【 js 基础 】【读书笔记】作用域和闭包https://juejin.im/post/5afb0ae56fb9a07aa2138425 深入理解闭包之前置知识→作用域与词法作用域
(一)前置知识
一、作用域和作用域链
前言: 在记录作用域和作用域链知识,我一度陷入了错误理解的边缘 ,它也是之后理解闭包知识非常重要的前置知识。查了资料,做了习题,问了老师验证了自己的理解,终于输出了一些我自己的理解。部分知识的表述有引用如有不妥之处,请老司机拍
参考资料:lce_shou《深入理解闭包之前置知识→作用域与词法作用域》
1、什么是作用域
先看这段代码:
function fn() {var a = 'miya';console.log(a); // 输出"miya"
}
fn();
//miya
在fn 执行的时候,输出一个 a 变量 ,那么这个a变量是哪里来?有看到函数第一行有 定义 a变量的代码var a = 'miya'
继续看另外一段代码:
var b = 'programmer';
function fn() {console.log(b); // 输出"programmer"
}
fn();
//programmer
同样,在输出 b 时,自己函数内部没有找到变量 b ,那么就 在外层的全局中查找 ,找到了就停止查找并输出结果。
究竟是啥回事呢?慢慢道来: 那么,可以注意到以上两段代码都有查找变量。第一段代码是在函数中找到a变量,第二段代码是在全局中找到b变量。 当然也要注意:JavaScript 中 { } 并没有带来块级作用域,如:
{var a=1
}
console.log(a)
//1 即使这样的定义同样能输出结果 1
JavaScript 的作用域是 通过 函数 来形成,也就是说一个函数内定义的变量,函数外是不可以访问。
例子如下:
function fn(){var a =1;
} //js编译器从此函数作用域出来之后,外界的变量函数或者声明变量均与之无关 fn();
console.log(a); //"ReferenceError: a is not defined" 说明在全局作用域中变量未被声明
接下来,我们在函数、全局,两个概念名分别都加上作用域三个字,是不是又打开一个新世界了? 关注细节的就知道,作用域,本质是一套规则,用于 确定在何处 以及 如何查找变量(标识符)的规则。关键点在于:查找变量(或标识符)。接下来让我们继续探索 作用域链 吧。
2、作用域链
还是看刚才这段代码:
var b = 'programmer';
function fn() {console.log(b); // 输出"programmer"
}
fn();
一般来说,我们在查找 b 变量时,先在函数作用域中 查找,没有找到,再去 全局作用域中 查找。你会注意到,这是一个往外层查找的过程,即顺着一条链条 从下往上查找变量 。这条链条,我们就称之为作用域链。
1、全局作用域 从 JS 页面中同时存在 函数fn 和 变量a 所处的位置来看。在页面里所写的代码 都是出于 一个全局作用域下。
全局作用域,相当于页面上有一个含有声明 变量a 或者 函数fn 的window对象。所声明的全局变量都是window对象下对应的一个属性。
还没有接触到 ES6 的 let、const 之前,只有函数作用域和全局作用域。函数作用域肯定是在全局作用域里面的,而函数作用域中又可以继续嵌套函数作用域,如:
那么代码则是:
二、从面试题解析作用域和作用域链
1、解密原理
- 每当执行完一块 作用域里的函数后,它就进入一个新的作用域下(一般从下往上找)
- 当你使用一个变量 或者 给一个变量赋值时,变量是从当前的作用域先找,再从上层作用域找
2、具体运用
var name = 'iceman'
这段小小的js代码有其编译过程,经历了下面的步骤: A、编译器在当前作用域中 声明一个变量name B、运行时引擎在作用域中查找该变量,找到了name变量并为其赋值 编译器工作,在代码执行前从上到下进行编译,当遇到某个用 var 声明变量时,先检查在当前作用域下是否存在该变量。如存在,则忽略这个声明;如果不存在,则在当前作用域中声明该变量。 以下例子则证明以上说法:
console.log(name); // 输出undefined
var name = 'miya';
在var name = 'miya'的上一行已输出 name变量,并没有报错,输出undefined,说明输出时该变量已存在,只是没有赋值而已。【这段很重要】
查找的规则:是从当前作用域开始找,如果没找到再到父级作用域中找,一层层往外找,如果在全局作用域如果还没找到的话,就会报错了:ReferenceError: 某变量 is not defined
(1)第一题:
var a = 1
function fn1(){ function fn2(){console.log(a)}function fn3(){var a = 4fn2()}var a = 2 return fn3
}
var fn = fn1()
fn() //输出多少//输出a=2
//执行fn2函数,fn2找不到变量a,接着往上在找到创建当前fn2所在的作用域fn1中找到a=2
(2)第二题:
var a = 1
function fn1(){function fn3(){ var a = 4fn2() }var a = 2return fn3
}function fn2(){console.log(a)
}
var fn = fn1()
fn() //输出多少//输出a=1
//最后执行fn2函数,fn2找不到变量a,接着往上在找到创建当前fn2所在的全局作用域中找到a=1
(3)【重点】第三题:
var a = 1
function fn1(){function fn3(){function fn2(){console.log(a)}var afn2()a = 4} var a = 2return fn3
}
var fn = fn1()
fn() //输出多少//输出undefined
//函数fn2在执行的过程中,先从自己内部找变量找不到,再从创建当前函数所在的作用域fn去找,注意此时变量声明前置,a已声明但未初始化为undefined
事实上,我发现上一节函数中 立刻执行的函数表达式 本质上也可以感受一下局部作用域和全局作用域的区别。
(4)再深入看几道网上的经典题,感受一下已经模糊的智商~ (其实也还好啦,认真点还是并没有对智商多大的撞击) 在作用域链中查找过程的伪代码
第1道题
var x = 10
bar()
function foo() {console.log(x)
}
function bar(){var x = 30foo()
}/*
第2行,bar()调用bar函数
第6行,bar函数里面调用foo函数
第3行,foo函数从自己的局部环境里找x,结果没找到
第1行,foo函数从上一级环境里找x,即从全局环境里找x,找到了var x=10。
foo()的输出结果为10。
*/
第2道题
var x = 10;
bar() //30
function bar(){var x = 30;function foo(){console.log(x) }foo();
}
/*
第2行,bar()调用bar函数
第3行,bar函数里面是foo函数
第4行,foo函数在自己的局部环境里寻找x,没找到。
foo函数到自己的上一级环境,即bar函数的局部环境里找x,找到var x = 30
所以第2行的bar()输出为30
*/
第3道题
var x = 10;
bar()
function bar(){var x = 30;(function (){console.log(x)})()
}
/*
第2行,bar()调用bar函数
第三行,bar函数里的function()在自己的局部环境里寻找x,但没找到
function()在上级环境即bar的局部环境里寻找x,找到var x =30,于是显示结果为30
*/
二、补充作用域下标识符的查找规则
- 函数在执行的过程中,先从自己内部中找声明过的变量。如果找不到,下一步
- 从创建当前函数所在的作用域(词法作用域)去找, 以此往外。注意,如果找不到则为undefined或报错
- 注意找的是变量的当前的状态
看完了作用域和作用域链知识,我们有必要了解一下JS编译过程,JavaScript是有编译过程。
先从这段简单的代码开始:var name = 'iceman',它的编译过程其实有两个动作:
- 编译器在当前作用域中声明一个变量name
- 运行时引擎在作用域中查找该变量,找到了name变量并为其赋值
证明以上的说法:
console.log(name); // 输出undefined
var name = 'iceman';
在var name = 'iceman'的上一行输出name变量,并没有报错,输出undefined,说明输出的时候该变量已经存在了,只是没有赋值而已。
其实编译器是这样工作的,在代码执行之前从上到下的进行编译,当遇到某个用var声明的变量的时候,先检查在当前作用域下是否存在了该变量。如果存在,则忽略这个声明;如果不存在,则在当前作用域中声明该变量。
上面的这段简单的代码包含两种查找类型:
- 输出变量的值时查找类型是:RHS,即变量出现在右侧时进行RHS查询。(作用域中查找变量都是RHS)。RHS就是取到它的源值。
- 找到变量为其赋值的查找类型是:LHS,即变量出现在赋值操作的左侧时进行LHS查询。(所有的赋值操作中查找变量都是LHS) 注:“赋值操作的左侧和右侧”,并不意味着只是“=”,实际上赋值操作还有好几种形式。
作用域中查找变量都是RHS。查找规则是从当前作用域开始找,如果没找到再到父级作用域中找,一层层往外找,如果在全局作用域如果还没找到的话,就会报错了:ReferenceError: 某变量 is not defined
所有的赋值操作中查找变量都是LHS。其中a=4这类赋值操作,也是会从当前作用域中查找,如果没有找到再到外层作用域中找,如果到全局变量啊这个变量,在非严格模式下会创建一个全局变量a。
注意:不过,非常不建议这么做,因为轻则污染全局变量,重则造成内存泄漏(比如:a = 一个非常大的数组,a在全局变量中,一直用有引用,程序不会自动将其销毁)。
三、词法作用域是什么?
熟悉作用域后,通常我们将其定义为一套规则,这套规则来管理浏览器引擎如何在当前作用域以及嵌套的作用域中根据 变量(标识符)进行变量查找。
我们在前面有抛出一个概念:“词法作用域是作用域的一种工作模型”,作用域有两种工作模型:一种主流的是,JavaScript的静态作用域——词法作用域,另一种则是动态作用域(比较少的语言在用)。
先看一下这个代码:
function fn1(x) {var y = x + 4;function fn2(z) {console.log(x, y, z);}fn2(y * 5);
}
fn1(6); // 6 10 50
例子中有三个嵌套的作用域:A、B、C,
- A 为全局作用域,有一个标识符:fn1
- B 为fn1所创建的作用域,有三个标识符:x、y、fn2
- C为fn2所创建的作用域,有一个标识符:z
如图:
作用域,是由其代码写在哪里决定的,并且是从外向内逐级包含的。
词法作用域,即在你写代码时将变量和块作用域写在哪里来决定,编译阶段就能够知道全部标识符在哪里以及是如何声明的,词法作用域就是静态的作用域,能够预测在执行代码的过程中如何查找变量(标识符),它在你书写代码时就确定。
(二)闭包是什么?
一、对闭包的各种解释
MDN的解释:
A closure is the combination of a function and the lexical environment within which that function was declared.闭包,是一个变量所声明的函数+它的词法作用域的结合。
JavaScriptKit的解释
A closure is the local variables for a function - kept alive after the function has returned 闭包对于函数来说是个本地变量,这个变量是当这个函数返回的时候,变量还存在。闭包,能访问当前函数外的变量。
《JavaScript高级程序设计》的解释:
闭包是一个函数,指有权访问另一个函数作用域中的变量的函数。
《JavaScript权威指南》的解释:
从技术的角度讲,所有的JavaScript函数都是闭包。它们都是对象,它们都关联到作用域链。
【较认可】《你不知道的JavaScript》的解释:
当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。
闭包,是基于词法作用域书写代码时产生的一种现象。通过下面的实践你会知道,闭包在代码中随处可见,不用特意为其创建而创建,随着深入做项目后,打代码的不经意间就已经用了闭包。
二、闭包的作用
- 封装数据
- 暂存数据
三、从实例解析闭包
实例1:
function fn1() {var name = 'iceman';function fn2() {console.log(name);}fn2();
}
fn1();
根据《JavaScript高级程序设计》中可知,这个函数出现了闭包。fn2访问到了fn1的变量,满足了书中对闭包的定义“有权访问另一个函数作用域中的变量的函数”,fn2本身是个函数,所以满足《JavaScript权威指南》所说的“所有的JavaScript函数都是闭包”
不过,看得出这是多个函数嵌套,特别是fn2本身是函数,也是一个返回值,也是fn1的赋值变量,对于基础不牢的小白来说还是很容易混淆的。
实例2:
function car(){var speed = 0function fn(){speed++console.log(speed)}return fn
}var speedUp = car()
speedUp() //1
speedUp() //2
分析如下:
- fn的词法作用域能访问car的作用域。根据条件执行fn函数内的代码,fn当做值返回
- car执行后,将fn的引用赋值给speedUp,赋值过程即speedUp=car=fn
- 执行speedUp,分别输出1,2
通过引用的关系,speedUp就是fn函数本身(speedUp=fn)。执行speedUp能正常输出变量speed的值,这不就是“fn能记住并访问它所在的词法作用域”,而fn(被speedUp调用)的运行是在当前词法作用域之外。
当car函数执行完毕之后,其作用域是会被销毁的,然后 垃圾回收器 会释放闭包那段内存空间,但是闭包就这样神奇地将car的作用域存活了下来,fn依然持有该作用域的引用,
这块引用为以下:
var speed = 0function fn(){speed++console.log(speed)}
以上引用就是闭包
总结:某个函数在定义时的词法作用域之外的地方被调用,闭包可以使该函数访问定义时的词法作用域。
实例3:用for循环输出函数值的问题
var fnArr = [];
for (var i = 0; i < 10; i++) {fnArr[i] = function(){return i};
}
console.log( fnArr[3]() ) // 10
通过for循环,预期的结果我们是会输出0-9,但最后执行的结果,在控制台上显示则是全局作用域下的10个10。
这是因为当我们执行fnArr[3]时,先从它当前作用域中找 i 的变量,没找到 i 变量,从全局作用域下找。开始了从上到下的代码执行,要执行匿名函数function时,for循环已经结束(for循环结束的条件是当i大于或等于10时,就结束循环),然后执行函数function,此时当 i 等于[0,1,2,3,4,5,6,7,8,9]时,此时i 再执行函数代码,输出值都是 i 循环结束时的最终值为:10,所以是输出10次10。
由此可知:i 是声明在全局作用域中,function匿名函数也是执行在全局作用域中,那当然是每次都输出10了。
延伸:
那么,让 i 在每次迭代的时候都产生一个私有作用域,在这个私有的作用域中保存当前 i 的值
var fnArr = [];
for (var i = 0; i < 10; i++) {fnArr[i] = (function(){var j = ireturn function(){return j} })()
}
console.log(fnArr[3]()) //3
用一种更简洁、优雅的方式改造:
将每次迭代的 i 作为实参传递给自执行函数,自执行函数用变量去接收输出值
var fnArr = []
for (var i = 0; i < 10; i ++) {fnArr[i] = (function(j){return function(){return j} })(i)
}
console.log( fnArr[3]() ) // 3
实例:用做一个计时器来实际理解闭包的作用
理解JS闭包——以计数器为例