首页 web前端 js教程 浅谈JS函数及闭包

浅谈JS函数及闭包

Nov 30, 2019 pm 04:58 PM
js 函数 闭包

每声明一个函数就会产生一个作用域。而外面的作用域访问不了里面的作用域(把里面的变量和函数隐藏起来),而里面的可以访问到外面的。对于隐藏变量和函数是一个非常有用的技术。

浅谈JS函数及闭包

基于作用域隐藏的方法叫做最小授权最小暴露原则

这个原则是指在软件设计中,应该最小限度的暴露必要内容,而将其内容都隐藏起来,比如某个模块或对象得API设计。隐藏变量和函数可以解决同名标识符的之间的冲突,冲突会导致变量的意外覆盖。

例如:

var a = 2;
function foo(){
  var a = 3;
  console.log(a);
}
foo();
console.log(a);
登录后复制

虽然这种技术可以解决一些问题,但是他并不理想,会导致一些额外的问题,首先必须声明一个具名函数foo(),意味着foo这个名称本身“污染”了所在的作用域,其次必须显式的通过函数名foo()调用这个函数才能运行其中的代码。

如果函数不需要函数名,并且能够自动运行,这会更加理想。幸好js提供了同时解决这两个问题的方案 -- (IIFE) Immediately Invoked Function Expression  --  立即执行函数

var a = 2;
(function foo(){
    var a = 3;
    console.log(a);
})()
console.log(a);
登录后复制

首先立即执行函数不会当做函数声明处理而当做函数表达式处理。 

区分函数声明还是函数表达式:看function在声明中是不是第一个词,如果是第一个词就是函数声明否则就是函数表达式。而立即执行函数" (function ",不是"     function ",所以是函数表达式。

函数声明函数表达式之间重要的区别是他们的名称标识符将会绑定在何处

函数声明的函名称数会绑定在当前作用域内。假如在全局作用域创建一个函数声明,就可以在全局作用域访问这个函数名称并执行。而函数表达式的函数名称会绑定在自身的函数中,而不是当前说在作用域中。例如你全局创建一个函数表达式,如果你直接执行这个你创建的函数表达式的函数名就会报错,因为当前作用域下没有这个标识符,而你在函数表达式里面的作用域里访问这个函数名就会返回这个函数的引用。

 

作用域闭包,嗯,闭包这儿两个字就有点让人难以理解,(可以想象成一个包是关上的,里面隐藏了一些神秘的东西)而对于闭包的定义是这样说的:当函数可以记住并访问所在的作用域时,就产生了闭包,即使函数是在当前作用域之外执行。

for instance(拽个英文,哈哈)。

function foo() {
    var a = 2;
    function bar() {
        console.log(a);
    }
    bar();
}
foo();
登录后复制

上面的 代码bar()可以访问外部作用域中的变量。根据上面的定义这是闭包吗?从技术来讲也许是,但我们理解的是作用域在当前作用域查找变量如果没找到会继续向上面查找,找到返回,找不到继续找,直到全局作用域。-- 而这些正是闭包的一部分。函数bar()具有一个涵盖foo()作用域的闭包。

function foo(){    var a  = 2;    function bar (){
        console.log(a);
    }    return bar;
}var baz = foo();
baz();
登录后复制

在上面的代码更好的展示了闭包。

bar()函数在定义时作用域以外的地方执行(此时在全局作用域执行)。在foo()函数执行后,通常会期待foo()整个内部作用域都被销毁,因为我们知道引擎有垃圾回收器用来释放不在使用的内存空间,由于foo()已经执行完,看上去内容不会再被使用,所以很自然的会考虑对齐进行回收,回收后意味着里面的函数和变量访问不到了。foo()执行完,baz变量存着bar函数的引用。当执行baz也就是bar函数时。console.log(a)。不理解闭包的人可能认为会报错,事实上,打印的是2;???what?

foo()函数作用域不是执行完销毁了吗?怎么还能访问到a变量?-- 这就是闭包。

当foo()执行后,bar函数被返回全局作用域下,但是bar函数还保留着当时的词法作用域(当时写代码是的顺序就已经定义了作用域,这个作用域叫词法作用域--外面函数套着里面的函数的那种)甚至直到全局作用域。所以bar还留有foo()函数的引用。使得foo()函数没有被回收。

闭包可以说不出不在,只是你没有发现认出他。在定时器,事件监听器,ajax请求,跨窗口通信或者任何其他的异步(或者同步)任务中,只要使用了回调函数,实际上就是使用闭包。

for instance

function wait(message) {
    setTimeout(function timer() {
        console.log(message);
    }, 1000);
}
wait("hello");
登录后复制

在上面的代码中将一个内部函数(名为timer)传递给setTimerout(...).timer具有涵盖wait(...)的作用域的闭包。因此还保有对变量message的引用。wait()执行1000毫秒后,它的内部作用域不会消失,timer函数依然保有wait()作用域的闭包。

而闭包和立即执行函数息息相关。

循环和闭包

for(var i = 1; i <= 5; i++){
    setTimeout(function timer(){
        console.log(i);
    },i*1000);
}
登录后复制

上面代码我们以为输出的会是1-5,可事实上输出的是5个6,这是为啥啊 -- 闭包啊。

延迟函数的回调会在循环结束时执行。事实上,当定时器运行时即使每个迭代的是setTimerout(...,0),所有的回调函数依然是循环结束后才会执行。我猜是跟js执行机制有关系吧。至于为什么都是6. 因为即使5个函数是在各个迭代中分别定义的,但是他们又被封闭在一个共享的全局作用域中因此实际上只有一个i.而怎么解决呢,立即执行函数来了!!!

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

}
登录后复制

打印出来1,2,3,4,5了欧,这回是你想要的数了。解释一下,5次循环创建了5个立即执行函数,这5个函数的作用域都不相同,立即函数接收的参数是当前循环的i.所以当timer执行时访问的就是自己立即执行函数对应的作用域。也就是说5个timer函数分别对应5个作用域,每个作用域保存的变量i都不同,解决啦!!!

你懂闭包了吗?

js执行机制

JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。那么,为什么JavaScript不能有多个线程呢?这样能提高效率啊。JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。

单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。JavaScript语言的设计者意识到这个问题,将所有任务分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。只要主线程空了,就会去读取"任务队列",这就是JavaScript的运行机制。

哪些语句会放入异步任务队列及放入时机一般来说,有以下四种会放入异步任务队列:setTimeout 和 setlnterval  ,DOM事件,ES6中的Promise,Ajax异步请求

 本文来自 js教程 栏目,欢迎学习!

以上是浅谈JS函数及闭包的详细内容。更多信息请关注PHP中文网其他相关文章!

本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

Video Face Swap

Video Face Swap

使用我们完全免费的人工智能换脸工具轻松在任何视频中换脸!

热工具

记事本++7.3.1

记事本++7.3.1

好用且免费的代码编辑器

SublimeText3汉化版

SublimeText3汉化版

中文版,非常好用

禅工作室 13.0.1

禅工作室 13.0.1

功能强大的PHP集成开发环境

Dreamweaver CS6

Dreamweaver CS6

视觉化网页开发工具

SublimeText3 Mac版

SublimeText3 Mac版

神级代码编辑软件(SublimeText3)

热门话题

Java教程
1662
14
CakePHP 教程
1418
52
Laravel 教程
1311
25
PHP教程
1261
29
C# 教程
1234
24
golang函数动态创建新函数的技巧 golang函数动态创建新函数的技巧 Apr 25, 2024 pm 02:39 PM

Go语言提供了两种动态函数创建技术:closures和反射。closures允许访问闭包作用域内的变量,而反射可使用FuncOf函数创建新函数。这些技术在自定义HTTP路由器、实现高度可定制的系统和构建可插拔的组件方面非常有用。

C++ 函数中闭包的优点和缺点是什么? C++ 函数中闭包的优点和缺点是什么? Apr 25, 2024 pm 01:33 PM

闭包是一种嵌套函数,它能访问外层函数作用域的变量,优点包括数据封装、状态保持和灵活性。缺点包括内存消耗、性能影响和调试复杂性。此外,闭包还可以创建匿名函数,并将其作为回调或参数传递给其他函数。

C++ Lambda 表达式如何实现闭包? C++ Lambda 表达式如何实现闭包? Jun 01, 2024 pm 05:50 PM

C++Lambda表达式支持闭包,即保存函数作用域变量并供函数访问。语法为[capture-list](parameters)->return-type{function-body}。capture-list定义要捕获的变量,可以使用[=]按值捕获所有局部变量,[&]按引用捕获所有局部变量,或[variable1,variable2,...]捕获特定变量。Lambda表达式只能访问捕获的变量,但无法修改原始值。

C++ 函数命名中参数顺序的考虑 C++ 函数命名中参数顺序的考虑 Apr 24, 2024 pm 04:21 PM

在C++函数命名中,考虑参数顺序至关重要,可提高可读性、减少错误并促进重构。常见的参数顺序约定包括:动作-对象、对象-动作、语义意义和遵循标准库。最佳顺序取决于函数目的、参数类型、潜在混淆和语言惯例。

excel函数公式大全 excel函数公式大全 May 07, 2024 pm 12:04 PM

1、 SUM函数,用于对一列或一组单元格中的数字进行求和,例如:=SUM(A1:J10)。2、AVERAGE函数,用于计算一列或一组单元格中的数字的平均值,例如:=AVERAGE(A1:A10)。3、COUNT函数,用于计算一列或一组单元格中的数字或文本的数量,例如:=COUNT(A1:A10)4、IF函数,用于根据指定的条件进行逻辑判断,并返回相应的结果。

如何在Java中写出高效和可维护的函数? 如何在Java中写出高效和可维护的函数? Apr 24, 2024 am 11:33 AM

编写高效和可维护的Java函数的关键在于:保持简洁。使用有意义的命名。处理特殊情况。使用适当的可见性。

C++ 函数异常进阶:定制错误处理 C++ 函数异常进阶:定制错误处理 May 01, 2024 pm 06:39 PM

C++中的异常处理可通过定制异常类增强,提供特定错误消息、上下文信息以及根据错误类型执行自定义操作。定义继承自std::exception的异常类,提供特定的错误信息。使用throw关键字抛出定制异常。在try-catch块中使用dynamic_cast将捕获到的异常转换为定制异常类型。实战案例中,open_file函数抛出FileNotFoundException异常,捕捉并处理该异常可提供更具体的错误消息。

Golang 函数接收 map 参数时的注意事项 Golang 函数接收 map 参数时的注意事项 Jun 04, 2024 am 10:31 AM

在Go中传递map给函数时,默认会创建副本,对副本的修改不影响原map。如果需要修改原始map,可通过指针传递。空map需小心处理,因为技术上是nil指针,传递空map给期望非空map的函数会发生错误。

See all articles