JS的的作用域和闭包

JS的作用域在何时确定

编程语言中,作用域一般来说有两种,词法作用域和动态作用域。词法作用域就是依赖编程时所写的代码结构确定的作用域,一般来说在编译结束后,作用域就已经确定,代码运行过程中不再改变。而动态作用域听名字就知道是在代码运行过程中作用域会动态改变。一般认为我们的javascript的作用域是词法作用域(说一般,是因为javascript提供了一些动态改变作用域的方法,后文会有介绍)。

对标识符来说有以下两种操作

- 赋值操作(LHS);常见的是函数定义,函数传参,变量赋值等等
* 取值操作(RHS);常见包括函数调用,

作用域其实就是定义了我们的呈现在运行期,进行标识符操作的范围,对应到实际问题来说,就是我们熟悉的函数或者变量可以在什么地方调用。

作用域也可以看做是一套依据名称查找变量的规则。那我们再细看一下这个规则,在当前作用域中无法找到某个变量时,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量, 或抵达最外层的作用域(也就是全局作用域)为止。

JS中的作用域类型
函数作用域

函数作用域是js中最常见的作用域了,函数作用域给我们最直观的体会就是,内部函数可以调用外部函数中的变量。一层层的函数,很直观的就形成了嵌套的作用域。还记得我们常常听到的“如果在函数内部我们给一个未定义的变量赋值,这个变量会转变为一个全局变量”

闭包:闭包是基于词法作用域书写代码时所产生的自然结果,你甚至不需要为了利用它们而有意 识地创建闭包。闭包的创建和使用在你的代码中随处可见。你缺少的是根据你自己的意愿 来识别、拥抱和影响闭包的思维环境。

块作用域

除了函数作用域,JS也提供块作用域。我们应该明确,作用域是针对标识符来说的,块作用域把标识符限制在{}中。

改变函数作用域的方法

一般说来词法作用域在代码编译阶段就已经确定,这种确定性其实是很有好处的,代码在执行过程中,能够预测在执行过程中如何对它们进行查找。能够提高代码运行阶段的执行效率。不过JS也提供动态改变作用域的方法。eval()函数和with关键字.

eval()方法:
这个方法接受一个字符串为参数,并将其中的内容视为好像在书写时就存在于程序中这个位置的代码。换句话说,可以在你写的代码中用程序生成代码并运行,就好像代码是写在那个位置的一样。

function foo(str,a){    
    eval(str);
    //欺骗作用域,词法阶段阶段foo()函数中并没有定义标识符,
      但是在函数运行阶段却临时定义了一个b;
   console.log(a,b); } var b = 2; foo("var b =3;",1);
//1,3 // 严格模式下,`eval()`会产生自己的作用域,无
法修改所在的作用域
function foo(str){    
    'use strict'
;
      eval(str);    
      console.log(a);
      //ReferenceError: a is not de ned }  foo('var a =2');
JavaScript 引擎会在编译阶段进行数项的性能优化。其中有些优化依赖于能够根据代码的词法进行静态分析,并预先确定所有变量和函数的定义位置,才能在执行过程中快速找到标识符。


声明提升

作用域关系到的是标识符的作用范围,而标识符的作用范围和它的声明位置是密切相关的。

关于声明也许大家都听说过声明提升一词。我们来分析一下造成声明提升的原因。

我们已经知道引擎会在解释 JavaScript 代码之前首先对其进行编译。编译阶段中的一部分工作就是找到所有的声明,并用合适的作用域将它们关联起来(词法作用域的核心)。
这样的话,声明好像被提到了前面。
值得注意的是每个作用域都会进行提升操作。声明会被提升到所在作用域的顶部。

不过并非所有的声明都会被提升,不同声明提升的权重也不同,具体来说函数声明会被提升,函数表达式不会被提升(就算是有名称的函数表达式也不会提升)。

函数声明和变量声明都会被提升。但是一个值得注意的细节也就是函数会首先被提升,然后才是变量,也就是说如果一个变量声明和一个函数声明同名,那么就算在语句顺序上变量声明在前,该标识符还是会指向相关函数。

如果变量或函数有重复声明以会第一次声明为主。

最后一点需要注意的是:
声明本身会被提升,而包括函数表达式的赋值在内的赋值操作并不会提升。

作用域的一些应用
最小特权原则

也叫最小授权或最小暴露原则。这个原则是指在软件设计中,应该最小限度地暴露必要内容,而将其他内容都“隐藏”起来,比如某个模块或对象的 API 设计。也就是尽可能多的把部分代码私有化。

函数可以产生自己的作用域,因此我们可以采用函数封装(函数表达式和函数声明都可以)的方法来实现这一原则。

这里顺便说明一下如何区分函数表达式和函数声明

如果 function 是声明中 的第一个词,那么就是一个函数声明,否则就是一个函数表达式。 函数声明和函数表达式之间最重要的区别是它们的名称标识符将会绑定在何处。函数表达式可以是匿名的,而函数声明则不可以省略函数名——在 JavaScript 的语法中这是非法的。

闭包

一般大家都会这么形容闭包。

当一个函数的返回值是另外一个函数,而返回的那个函数如果调用了其父函数内部的其它变量,如果返回的这个函数在外部被执行,就产生了闭包。

function foo() {        
   var a = 2;       function bar() {            
       console.log(a);   }        
   return bar;}    
var baz = foo();baz();
// 2 —— 这就是闭包的效果。在函数外访问了函数内的标识符// bar()函数持有对其父作用域的引用,
  而使得父作用域没有被销毁,这就是闭包

一般来说,由于垃圾回收机制的存在,函数在执行完以后会被销毁,不再使用的内存空间。上例中由于看上去 foo()的内容不会再被使用,所以很自然地会考虑对其进行回收。而闭包的“神奇”之处正是可以阻止这件事情的发生(以前总有人说要减少使用闭包,害怕内存泄漏什么的,其实这个也不大比担心)。

本质上无论何时何地,如果将函数(访问它们各自的词法作用域)当作第一 级的值类型并到处传递,你就会看到闭包在这些函数中的应用。在定时器、事件监听器、 Ajax请求、跨窗口通信、Web Workers或者任何其他的异步(或者同步)任务中,只要使 用了回调函数,实际上就是在使用闭包!

这里说一个大家可能都遇到过的坑,一个没有正确理解作用域和闭包造成的坑。

for (var i = 1; i <= 5; i++) {    setTimeout(function timer() {            
       console.log(i);   }, i * 1000);}
// 其实我们想得到的结果是1,2,3,4,5,结果却是五个6

我们分析一下造成这个结果的原因:我们试图假设循环中的每个迭代在运行时都会给自己“捕获”一个 i 的副本。但是根据作用域的工作原理,实际情况是尽管循环中的五个函数是在各个迭代中分别定义的(前面说过以第一次定义为主,后面的会被忽略), 但是它们都被封闭在一个共享的全局作用域中,因为在时间到了执行timer函数时,全局里面的这个i就是6,因此无法达到预期。

理解了是作用域的问题,这里我们有两种解决办法:

// 办法1for (var i = 1; i <= 5; i++) {    (function(j) {       setTimeout(function timer() {                
           console.log(j);      }, j * 1000);   })(i);    
   //通过一个立即执行函数,为每次循环创建一个单独的作用域。}    // 办法2for (var i = 1; i <= 5; i++) {        
   let j = i;
   // 是的,闭包的块作用域!   setTimeout( function timer() {        
       console.log(j);   }, j * 1000);}    
// let 每次循环都会创建一个块作用域

现在的开发都离不开模块化,下面说说模块是如何利用闭包的。

模块是如何利用闭包的:
最常见的实现模块模式的方法通常被称为模块暴露

我们来看看如何定义一个模块

function CoolModule() {        
   var something = "cool";        
   var another = [1, 2, 3];       function doSomething() {            
       console.log(something);   }       function doAnother() {            
       console.log(another.join(" ! "));   }       // 返回的是一个对象,对象中可能包含各种函数   return {       doSomething: doSomething,      doAnother: doAnother   };}    
var foo = CoolModule();
// 在外面调用返回对象中的方法就形成了闭包foo.doSomething();
// coolfoo.doAnother();
// 1 ! 2 ! 3

模块的两个必要条件:

  • 必须有外部的封闭函数,该函数必须至少被调用一次

  • 封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态。

文章写到这里也差不多该结束了,谢谢你的阅读,希望你有所收获。

转载自:https://segmentfault.com/a/1190000007650548



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
提供的源码资源涵盖了安卓应用、小程序、Python应用和Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值