閉包的形成與變數的作用域及生命周期密切相關,所以在理解閉包前,須理解變數作用域。作用域分全局和局部作用域,是指變數有效訪問的範圍。變數無權訪問子作用域,只能訪問自己和父級以上的作用域。 預編譯 當函數執行時,會創建一個執行期上下文(即作用域)的對象AO(存儲在 中), 一個新的AO指向 定義了一個函 ...
閉包的形成與變數的作用域及生命周期密切相關,所以在理解閉包前,須理解變數作用域。作用域分全局和局部作用域,是指變數有效訪問的範圍。變數無權訪問子作用域,只能訪問自己和父級以上的作用域。
預編譯
當函數執行時,會創建一個執行期上下文(即作用域)的對象AO(存儲在[[scope]]
中), 一個新的AO指向 定義了一個函數執行時的環境。
函數執行時對應的AO, 是獨一無二的,每次調用函數就創建一個OA, 函數執行完畢 AO的指向就會銷毀
[[scope]]
: 每個函數都是對象,對象中有些屬性可訪問,有些不可以,[[scope]]
就不可訪問,它存儲了運行期上下文的集合([GO,AO]
)。
作用域鏈:就是[[scope]]
中所存儲的AO對象集合,呈鏈式鏈接
查找變數:
函數剛定義就存儲了 所在環境的執行期上下文,執行時 創建自己的AO
function fun (a){
console.log(a);// function
var a=123;
function a (){};
console.log(a)// 123 函數聲明已提升所以不用管
var b =function(){};
console.log(b);//function 因為是函數表達式,只提升了 變數b, 這樣的函數體不會提升
}
fun(1); //函數 123 123 函數
/*
代碼執行
1. 會創建 一個(全局為GO)AO(Activation Object)對象(執行期上下文/作用域)一個存儲空間庫
2. 找形參和變數聲明,將形參和變數名作為AO的屬性名,值為undefined,重覆的只用寫一個
3. 將實參值和形參統一
4. 在函數體里找函數聲明,值為函數體
5. if/return 不用管 聲明還是會提升
AO{
a:function a (){};
b:undefined
}
記住函數是一等公民許可權最高
*/
函數內部聲明變數的時候,一定要使用var,let or const命令。如果不用的話,你實際上聲明瞭一個全局變數!
變數的生命周期: 全局變數可永久訪問,除非主動銷毀,而局部變數在函數運行結束時就會隨之銷毀,當局部變數還能被外界訪問時,將會保留,不被銷毀
閉包的理解:
在Javascript語言中,只有函數內部的子函數才能訪問該函數的變數,而定義在一個函數內部的函數並且外部能接收到這個函數,那麼這個函數就是閉包。(能夠讀取其他函數內部變數的函數。)
- 閉包是由函數以及創建該函數的詞法環境組合而成。這個環境包含了這個閉包創建時所能訪問的所有局部變數。
特點:
- 函數套函數,並且外部能訪問嵌套函數。
- 父函數被銷毀 的情況下,子函數的
[[scope]]
中仍然保留著父級的變數對象和作用域鏈,因此可以繼續訪問父級的變數對象,進而改變父作用域內部的變數值 - 占用記憶體,過多使用會產生性能問題,在IE中可能會導致記憶體泄漏。(可在銷毀函數前,將無用的變數刪除)
// 例一
function A() {
let a = 1
B = function ()
}
}
A()
B() // 1
//例二
function A() {
let a = 1
return function () {
console.log(a)
}
}
A()()//1
閉包與記憶體泄漏
記憶體泄漏是指,頁面隨著時間的延長使用的記憶體越來越多而沒有及時釋放。
javascript中 不需要開發人員像C語言一樣手動使用 malloc()
分配記憶體,也不需要用完後使用free()
回收;而是使用垃圾回收策略來自動管理記憶體,即找出那些不再使用的值,然後釋放所占用的記憶體。
垃圾回收只針對局部變數進行回收銷毀,全局變數只有網頁關閉才會消除。垃圾回收有兩種方法引用計數和標記清除
垃圾回收演算法主要依賴於引用的概念
什麼是引用:在記憶體管理的環境中,一個對象如果有訪問另一個對象的許可權(隱式或者顯式),叫做一個對象引用另一個對象。
引用計數
引用計數是跟蹤記錄每個值被引用的次數。即看一個對象是否有指向它的引用。如果沒有其他對象指向它(零引用),說明該對象已經不再需要了。
let obj = {
name:'owen'
}
// 此時 對象 { name:'owen' } 被創建並引用一次
obj = null
// 此時 對象引用次數為零,將被回收機制銷毀
引用計數有一個迴圈引用的問題:如果兩個對象互相引用,它們的引用次數永遠不會為零,將永遠不會被回收,從而占據記憶體
function obj(){
let obj1 = {
name:'owen'
}
// 此時 對象 { name:'owen' } 被創建並引用一次
let obj2 = {}
obj2.name = obj1
obj1.name = obj2
// 此時 兩個對象相互引用 ,倆個對象引用次數為二,永遠也不會被收回
}
obj()
標記清除
現代瀏覽器常用的方法,當變數進入環境時(例如,在函數中聲明一個變數),這個變數標記為“進入環境”;而當變數離開環境時,則將其標記為“離開環境”。
把"對象是否不再需要"簡化定義為"對象是否可以獲得"。如把JavaScript想象一個樹,每個JS都存在一個根(瀏覽器中為window對象,Node中為global對象 ),每當一個函數執行,就會生成一個節點。嵌套的函數調用就會有子節點。當函數執行完時,內部的變數都是無法被其他代碼訪問的,所以它就被標記為“無法被訪問”。GC 時,JS 引擎統一對所有這些狀態的對象進行回收。
當進行一輪垃圾回收時,主線程會被阻塞,各個瀏覽器的時間不同可能是10ms、50ms、1s
應用
封裝變數
將不需要暴露在全局的變數封裝成"私有變數"。
// 乘積
let mult = (...args) =>{
let num = 1;
for (let val of args ){
num *= val;
}
return num;
}
// 由於每次運行函數都會完全遍歷所以形參,效率較低下,我們可以加入緩存機制提供函數性能
let mult1 = (()=>{
let cache = {};
return (...args) => {
if(cache[args.join(',')]) return cache[args.join (',')];
let num = 1;
for (let val of args ){
num *= val;
}
return cache[args.join(',')]= num; // 緩存數值
}
})()
// 我們看到 cache 變數僅僅在 mult 函數中使用,我們可以將它封裝在函數內部,減少全局變數,變數發生不必要的錯誤
如果一個大函數中有些代碼塊能夠獨立出來,我們常常把這些代碼塊封裝在獨立的小函數里並有個良好的命名,將有助於復用,和註釋作用;如果小函數不需要在其他程式中使用,最好使用閉包封裝起來
let mult1 = (()=>{
let cache = {};
let calculate = (...args)=> {
let num = 1;
for (let val of args ){
num *= val;
}
return num
}
return (...args) => {
let property = args.join(',')
if(cache[property]) return cache[property];
return cache[property]= calculate.apply(null,args); // 緩存數值
}
})()
延續變數
// img案例
let imgSrc = (src) => {
let img = new Image();
img.src = src;
}
imgSrc('http://xxxx.com/img')
// 在一些低版本瀏覽器中使用 imgSrc 函數,會丟失一些數據,因為當函數調用結束後 img變數會隨之銷毀,此時可能未及時發出HTTP請求
// 使用閉包解決數據丟失問題
let imgSrc = (function (){
let imgs = [];
return function (src){
let img = new Image();
imgs.push(img)
img.src = src;
}
})()
imgSrc('http://xxxx.com/img')
三種方法解決迴圈中 var 定義函數的問題
//one 利用閉包
for (var i = 1; i <= 5; i++) {
(function(j) {
setTimeout(function timer() {
console.log(j);
}, j * 1000);
})(i);
}
//two 設置第三個參數
for (var i = 1; i <= 5; i++) {
setTimeout(function timer(j) {
console.log(j)
}, i * 1000,i)
}
//three 利用 let
for (let i = 1; i <= 5; i++) {
setTimeout(function timer(i) {
console.log(i)
}, i * 1000)
}
因為 setTimeout
是個非同步函數,所以會先把迴圈全部執行完畢,這時候i
就是 固定了,所以會輸出一堆 固定值。
函數中的this對象
普通函數
this 指向取決於 調用它時處在的執行上下文
對於new 的方式來說,this 被永遠綁定在了賦值變數上面,不會被任何方式改變 this
箭頭函數
箭頭函數中的 this 只取決包裹箭頭函數的第一個普通函數
的 this 否則指向全局。