相關學習推薦:javascript影片教學
閉包 永遠都是前端開發者繞不過去的一個坎,不管你喜歡與否,在工作和麵試中,都會遇到。每個人對閉包的理解都不盡相同,這裡筆者談談自身對閉包的理解。 (如果與您的理解有出入,請以自己為準 )
在給定義之前,不妨看看別人是如何定義閉包的:
函數物件可以透過作用域鏈相互關聯起來,函數體內部的變數都可以保存在函數作用域內,這種特性在電腦科學文獻中稱為「閉包」 -- JavaScript權威指南(第六版)
閉包是指有權存取另一個函數作用域中的變數的函數。建立閉包的常見方式,就是在一個函數內部建立另一個函數。 -- JavaScript高階程式設計(第三版)
當函數可以記住並存取所在的詞法作用域時,就產生了閉包,即使函數是在當前詞法作用域之外執行。 -- 你不知道的JavaScript(上卷)
雖然上面的幾段話描述起來並不一樣,但是您細細品味後還是能找出一些共同點。其中最重要的是不同作用域之間的聯繫。當然了,您可以直接引用上面的定義(畢竟上面幾個定義還是比較權威的),這裡筆者比較喜歡最後一段的定義,同時力推《你不知道的JavaScript(上卷)》這本書,值得反覆細讀。
光給出定義是遠遠不夠的,還必須探討內部涉及了哪些知識點。下面是筆者認為有用到的知識點。
嗯,其實筆者知道你們都想到了這點(不會吧,不會有人沒想到這點吧)。既然大家都了解作用域。這裡就簡單描述一下,過一下場即可。
作用域:根據名稱找出變數的一套規則。分為三種類型:全域作用域;函數作用域;區塊作用域。
要注意的是區塊作用域,ES6新增的規格。在花括號{}
裡面使用let,const
定義的變量,都會綁定到該作用範圍內,花括號以外的地方無法存取。 注意:在花括號開始 到 let變數宣告之前,存在暫時性死區
(該點不在本文討論範圍)。
作用域鏈:當不同的作用域 (混~困惑~在~一~起~ 呸,不小心齣戲了) 圈套在一起時,就形成了作用域鏈。注意的是,查找方向是從內到外的。
為什麼作用域的找出方向是從內到外的呢?這是個很有趣的問題。個人覺得是跟js執行函數的入棧方式決定的(感覺有點偏題了,有興趣的小夥伴可以去查一下資料)。
函數之所以可以存取另一個函數作用域的變數(或記住目前的作用域並在目前以外的地方存取)的關鍵點
就是詞法作用域
在運作。這一點很重要,但不是所有人都知道這個知識點,這裡簡單探討一下。
在程式設計界中,存在著兩種作用域工作模式,一種是被大多數程式語言所採用的
詞法作用域
;另一種就是與其相反的動態作用域
(這個不在本文的討論範圍)。
詞法作用域: 變數和區塊的作用域在您編寫程式碼的階段就已經確定好了,不會隨著呼叫的物件或地方的不同而改變(感覺跟this相反)。
要不,舉個栗子看看吧:
let a = 1; function fn(){ let a = 2; function fn2(){ console.log(a); } return fn2; } let fn3 = fn(); fn3();
從上面的定義可以知道,fn
是一個閉包函數,fn3
拿到了fn2
的指標位址,當fn3
執行的時候,其實是執行fn2
,而裡面的a
變量,根據作用域鏈的查找規則,找到的是fn
作用域內的變數a
,所以最終的輸出是2,而不是1。 (可以看下圖)
雖然詞法作用域是靜態的,但還是有辦法可以欺騙它,達到動態的效果。
第一种方法是使用eval. eval可以把字符串解析成一个脚本来运行,由于在词法分析阶段,无法预测eval运行的脚本,所以不会对其进行优化分析。
第二种方法是with. with通常被当作重复引用同一个对象中的多个属性的快捷方式,可以不需要重复引用对象本身。with本身比较难掌握,使用不当容易出现意外情况(如下例子),不推荐使用 -.-
function Fn(obj){ with(obj){ a = 2; } } var o1 = { a:1 } var o2 = { b:1 } Fn(o1); console.log(o1.a); //2 Fn(o2); console.log(o2.a); //undefined; console.log(a); //2 a被泄漏到全局里面去了 // 这是with的一个副作用, 如果当前词法作用域没有该属性,会在全局创建一个
闭包的使用场景可多了,平时使用的插件或者框架,基本上都有闭包的身影,可能您没留意过罢了。下面笔者列举一些比较常见的场景。
模拟私有变量和方法,进一步来说可以是模拟模块化
;目前常用的AMD,CommonJS等模块规范,都是利用闭包的思想;
柯里化函数或者偏函数;利用闭包可以把参数分成多次传参。如下面代码:
// 柯里化函数 function currying(fn){ var allArgs = []; function bindCurry(){ var args = [].slice.call(arguments); allArgs = allArgs.concat(args); return bindCurry; } bindCurry.toString = function(){ return fn.apply(null, allArgs); }; return bindCurry; }
实现防抖或者节流函数;
实现缓存结果(记忆化)的辅助函数:
// 该方法适合缓存结果不易改变的函数 const memorize = fn => { let memorized = false; let result = undefined; return (...args) => { if (memorized) { return result; } else { result = fn.apply(null,args); memorized = true; fn = undefined; return result; } }; };
说了那么多,我怎么知道自己写的代码是不是闭包呢?先不说新手,有些代码的确隐藏的深,老鸟不仔细看也可能发现不了。 那有没有方法可以帮助我们区分一个函数是不是闭包呢?答案是肯定的,要学会善于利用周边的工具资源,比如浏览器。
打开常用的浏览器(chrome或者其他),在要验证的代码中打上debugger断点,然后看控制台,在scope里面的Closure(闭包)里面是否有该函数(如下图)。
答案是有可能。内存泄漏的原因在于垃圾回收(GC)无法释放变量的内存,导致运行一段时候后,可用内存越来越少,最终出现内存泄漏的情况。常见的内存泄漏场景有4种:全局变量;闭包引用;DOM事件绑定;不合理使用缓存。其中,闭包导致内存泄漏都是比较隐蔽的,用肉眼查看代码判断是比较难,我们可用借助chrome浏览器的Memory标签栏工具来调试。由于篇幅问题,不展开说明了,有兴趣自己去了解一下如何使用。
想了解更多编程学习,敬请关注php培训栏目!
以上是一起認識閉包的詳細內容。更多資訊請關注PHP中文網其他相關文章!