每宣告一個函數就會產生一個作用域。而外面的作用域存取不了裡面的作用域(把裡面的變數和函數隱藏起來),而裡面的可以存取到外面的。對於隱藏變數和函數是一個非常有用的技術。
基於作用域隱藏的方法叫做最小授權或最小暴露原則。
這個原則是指在軟體設計中,應該最小限度的暴露必要內容,而將其內容都隱藏起來,例如某個模組或物件得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中文網其他相關文章!