js 閉包 基礎 示例 高階

那塊程式碼沒問題發表於2020-11-03

瀏覽器垃圾回收機制

/**

  • GC:瀏覽器垃圾回收機制
  • 【棧記憶體:EC】
  • 全域性執行上下文:在瀏覽器載入頁面的時候形成,然後在頁面關閉的時候釋放(頁面重新整理:先釋放,重新載入中後再形成)
  • 私有上下文:函式執行會形成一個私有上下文(程式碼塊中let/const也會形成私有上下文)
  •    + 一般情況下程式碼執行完就會出棧釋放
    
  •    + 然後當前上下文中的某個內容(一般是一個堆(物件、函式))被上下文以外的事物佔用了,則當前上下文不能被釋放
    

*【堆記憶體:HEAP】

  • 以谷歌瀏覽器為例
    
  •     按照是否被引用來決定是否釋放,瀏覽器會定期間隔一段時間查詢所有堆記憶體是否被佔用了,如果沒有被佔用則釋放
    
  •       + 我們可以手動賦值為null,達到記憶體釋放的目的
    
  • IE瀏覽器按照引用計數方式實現垃圾回收
    
  •   +這種方式會出現計數混亂的問題,會導致記憶體洩漏
    

閉包定義

  • 函式執行,會形成一個私有的上下文,這個私有上下文會保護裡面的私有變數不受外界的干擾,我們把函式的這種保護機制成為閉包
  • 另一種說法:函式執行形成一個不被釋放的上下文,一方面可以保護裡面的私有變數不受外界干擾,還可以將這些變數儲存下來,這種方式才是閉包
  • 閉包機制會消耗記憶體(合理使用)
  • 閉包作用:儲存 保護
  • 示例1
  • 正常情況下函式執行完後所形成的上下文都會出棧釋放,私有上下文中的一切東西都銷燬,優化棧記憶體控制元件
  • 如果函式執行所形成的的上下文中有一個東西(一般是引用型別的地址)被當前上下文以外的事務所佔用,
  • 則當前上下文將不會釋放,將導致棧記憶體變大
  • 函式每次執行都會形成全新的私有上下文

*/
let x= 5;
function fn(x) {//形參變數也屬於私有變數
return function (y) {
console.log(y + (++x));
}
}
let f = fn(6);
f(7); //14
fn(8)(9) //18
f(10); //18
console.log(x);//5

let x= 5;
function fn() {
return function (y) {
console.log(y + (++x));
}
}
let f = fn(6);
f(7); //13
fn(8)(9) //16
f(10); //18
console.log(x);//8

let x= 5;
function fn() {
return function (y) {
console.log(y + (++x));
}
}
let f = fn();
f(7); //13
fn()(9) //16
f(10); //18
console.log(x);//8

//閉包的常見方式
//方式1.
function fun() {
var x = 0;
return function () {
return ++x;
}
}
//方式2.
function fn() {
return {
name:“xxx”
};
}
var obj = fn();
//方式3.
function fn() {
let x = 0;
document.body.onclick = function () {
console.log(x++);
}
}

/**

  • 示例1
    */
    let a= 10,b=9;
    function A(a) {
    A = function (b) {
    alert(a+b++);
    };
    alert(a++);
    }
    A(1); //1 A(1)執行完後,原來的A a上下文並不會被釋放
    A(2); // 4 alert(a+b++); 中的a是原來A a的變數

//----------問題-------------
var fnA= [];
for(var i = 0; i<5;i++){
fnA[i] = function () {
return i;
}
}
fnA0;//5
fnA4;//5

閉包解決迴圈賦值的問題

//-------閉包解決迴圈賦值的問題 方案1-1----------
var fnA= [];
for(var i = 0; i<5;i++){
fnA[i] = function (i) {
return function () {
return i;
};
}(i);
}
fnA0;//0
fnA4;//4

//-------閉包解決迴圈賦值的問題 方案1-2----------
var fnA= [];
for(var i = 0; i<5;i++){
(function(i) {
fnA[i] = function () {
return i;
};
})(i);
}
fnA0;//0
fnA4;//4

//-------閉包解決迴圈賦值的問題 方案1-3 let----------優於以上的程式碼
//還是基於閉包的的機制,但是不是自己去執行函式建立,而是利用es6中let產生私有上下文實現
var fnA= [];
for(let i = 0; i<5;i++){ //此處會產生塊級上下文,每輪迴圈都會產生一個私有的上下文,迴圈結束父級上線文將會被銷燬
//首先for let會產生 父級上下文生成變數i
//for迴圈體會產生私有上下文併產生自己的變數i,每次迴圈完會將父級上下文釋放,因為i獨立,私有上下文會影響父類中的i,但是每輪迴圈結束後私有上下文中的變數不受父級影響
fnA[i] = function () {
return i;
}
}
fnA0;//0
fnA4;//4

//-----------自定義屬性解決迴圈賦值的問題 方案2------不會產生閉包,但是還是會迴圈產生堆記憶體-------
//事先把資訊儲存到屬性身上,後期在其他操作上需要用到直接獲取即可
//例如button迴圈繫結事件時,給每個button新增自定義屬性
for (let i = 0; i < buttonList.length; i++) {
buttonList[i].myIndex = i;
buttonList[i].οnclick= function () {
console.log(this.myIndex);
}
}

//-----------事件代理機制 自定義標籤 解決迴圈賦值的問題 方案3 效能會提高>=40%-------
// button
document.body.onclick = function (ev) {
let target = ev.target;
if(target.tagName === “BUTTON”){
let index = + target.getAttribute(“data-index”); // + 轉數字
console.log(index);
}
}

//…
var a= 9;
function fn() {
a =0;
return function (b) {
return b + a++;
}
}
var f= fn();
console.log(f(5)); // 5;
console.log(fn()(5)); // 5
console.log(f(5));//6
console.log(a); // 2

套娃

//----------------------------
function fun(n,o) {
console.log(o);
return {
fun: function (m) {
return fun(m,n);//注意此處會執行後返回
}
};
}
var c = fun(0).fun(1); // undefined 0
c.fun(2); // 1
c.fun(3); // 1

var b =10;
(function b() { //匿名函式具名化,名字只能在內部使用,且預設無法被重新賦值,除非這個函式名在函式體中被重新宣告過
b =20;
console.log(b); //function b(){ … }
})();
console.log(b);

var b =10;
(function b() { //匿名函式具名化,名字只能在內部使用,且預設無法被重新賦值,除非這個函式名在函式體中被重新宣告過
console.log(b);// undefined
var b =20;
console.log(b); //20
})();
console.log(b);

模組化

//沒有物件和函式的情況下,編寫程式碼經常出現‘全域性變數汙染’
//…怎麼使用…
/**
*模組化/單例模式設計 (利用閉包的保護和物件的分組特徵一起實現)
*每一個物件都是Object的單獨例項

  • 1.let obj = {}; //字面量方式

  • 2.let obj = new Object();//建構函式方式,後臺語言只有這種方式

  • AModule 物件名且是名稱空間,分組特徵
    */
    let AModule = (function () {
    let step = 0;
    function fn() {

    }
    function query() {
    }
    function privateFn(){ //私有方法

    }
    //我們想把私有的東西暴露出去供外面呼叫、
    // 1. // 瑕疵:不能給外部暴露太多方法,否則出現‘全域性變數汙染’
    window.query = query;

    // 2. 基於物件(分組)的方法,把需要暴露的方法,都放置到一個空間下 好的方案
    return {
    fn, query,step,
    init() {
    //控制業務板塊中我們先執行誰,再執行誰
    }
    }
    })()

function f1() {
AModule.query();
}

//…jquery 模組化實現…
//https://code.jquery.com/jquery-3.5.1.js
var A = typeof window !== “undefined” ? window : this; //根據執行環境返回全域性物件
//利用暫時性死區:一個未被宣告的變數在typeof檢測時不會報錯,只是返回“undefined”
//檢測window是否存在
// + JS在瀏覽器中執行:是存在window的
// + JS在node中只執行:不存在window,全域性物件是global

var B = function( window, noGlobal ) {
//如果是在瀏覽器環境22中執行的js程式碼
// + window就是window noGlobla為undefined
//如果是node下執行 則 window是global ,noGlobal 為true
“use strict”;
var jquery = function (select, context) {
return new jQuery.fn.init(select, context);
};

jQuery.fn = jQuery.prototype = {

}

//瀏覽器環境下:暴露給全域性變數兩個變數,只都是jquery
if (typeof noGlobal === "undefined" ) {
    window.jQuery = window.$ = jQuery;
}
return jquery;

};
(function (global, factory) {
“use strict”;
// 嚴格模式:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Strict_mode
//驗證是否支援CommonJS/ES6Module規範
if(typeof module === “object” && typeof module.exports === “object”){
// 程式碼是基於node環境下的或者是基於webpack打包的專案
module.exports = global.document ? factory(global, true) :function (w) {
if(!w.document){
throw new Error(“jquery requires a window with a document”);
}
return factory(w);

    };
} else {
    // 執行在瀏覽器或者webview中
    factory(global);
}

})(A, B);

//…類庫、外掛、ui元件、框架…
/**

  • 在我們的專案中編寫自己的類庫、外掛、ui元件、框架的時候,我們需要基於閉包的機制進行’私有化’處理
    • 能夠在瀏覽器中執行
    • 也支援CommonJS、ES6Module規範(node\webpack)
      */
      (function () {
      function Banner() {
}
//瀏覽器環境
if(typeof window !== "undefined"){
    window.Banner = Banner;
}
//支援CommonJS\ESModule
if(typeof module !== "undefined" && typeof module.exports != "undefined"){
    module.exports = Banner;
}

})();

惰性函式…函式重構

//…惰性函式…函式重構…
/**

  • 懶 能執行一次絕不執行二次
    */

//這樣寫每次都需要判斷window.getComputedStyle,沒必要,第一次執行已經知道相容性了
function getCss(element, attr) {
if(window.getComputedStyle){
return window.getComputedStyle(element)[attr];
}
//IE678
return element.currentStyle[attr];
}
console.log(getCss(document.body, “width”));
console.log(getCss(document.body, “padding”));
//…
let isCompatible = ‘getComputedStyle’ in window;
function getCss(element, attr) {
if(isCompatible){
return window.getComputedStyle(element)[attr];
}
//IE678
return element.currentStyle[attr];
}
console.log(getCss(document.body, “width”));
console.log(getCss(document.body, “padding”));
//…惰性思想…
function getCss(element, attr) {
if(window.getComputedStyle){
getCss = function (element, attr) {
return window.getComputedStyle(element)[attr];
}

} else{
    //IE678
    getCss = function (element, attr) {
        return element.currentStyle[attr];
    }
}
return getCss(element, attr);

}
console.log(getCss(document.body, “width”));
console.log(getCss(document.body, “padding”));

閉包柯里化函式…預處理思想
//…閉包柯里化函式…預處理思想…
let res = fn(1,2)(3);
console.log(res); //實現 1 + 2 + 3 =>6
function fn(…outterArgs) {
return function anonymous(…innerArgs) {
let arr = outterArgs.concat(innerArgs);
return arr.reduce(function (total, item) {
return total + item;
})
}
}
//自定義reduce
function reduce(arr, callback, init) {
let arrindex = 0;
if(init === “undefined”){
init = arr[0];
arrindex++;
}
for (let i = arrindex; i < arr.length; i++) {
init = callback(init, arr[i], i);
}
return init;

}
let arr = [10, 20, 30, 40, 50];
let result = reduce(arr, function (result, item, index) {
return result + item;
}, 0);
console.log(result);

result = reduce(arr, function (result, item, index) {
return result + item;
}, 100);
console.log(result);

Compose函式

//…Compose函式…
const add1 = (x) => x+1;
const mul3 = (x) => x*3;
const div2 = (x) => x/2;
console.log(div2(mul3(add1(add1(0)))));
//這樣寫可讀性太差,我們可以構建一個compose函式,他接受任意多個函式作為引數(這些函式都只能接收一個引數),然後compose返回的也是一個函式,達到以下效果
const opera = compose(div2,mul3,add1, add1);
console.log(opera(0));//相當於div2(mul3(add1(add1(0))))
console.log(opera(2));//相當於div2(mul3(add1(add1(2))))
console.log(opera());
//方式1
function compose(…funcs) {
return function opera(x) {
if(funcs.length === 0){
return x;
}
if(funcs.length === 1){
return funcs0;
}

    return funcs.reduceRight(function (result, item, index) {
        if(index === funcs.length-2){
            return item(result(x));
        } else {
           return item(result);
        }
    });
}

}
//方式2
const add1 = (x) => x+1;
const mul3 = (x) => x*3;
const div2 = (x) => x/2;
console.log(div2(mul3(add1(add1(0)))));
//這樣寫可讀性太差,我們可以構建一個compose函式,他接受任意多個函式作為引數(這些函式都只能接收一個引數),然後compose返回的也是一個函式,達到以下效果
const opera = compose(div2,mul3,add1, add1);
console.log(opera(0));//相當於div2(mul3(add1(add1(0))))
console.log(opera(2));//相當於div2(mul3(add1(add1(2))))
console.log(opera());
function compose(…funcs) {
if(funcs.length === 0){
return arg =>{
return arg;
}
}
if(funcs.length === 1){
return funcs[0];
}

return funcs.reduce((a,b)=> {
    return (...args) => {
        a(b(...args));
    }
});

}

防抖 節流

//…防抖動防抖:對於頻繁觸發某個操作,我們只識別一次(只觸發一次函式) …
//點選做啥事情,一般都是防抖為主
// 防抖函式 點選做啥事情,一般都是防抖為主
// func[function]:最後要觸發執行的函式
// wait[number]: 頻繁的設定界限 多少時間點選算頻繁
// immediate[boolean]: = true 觸發第一次點選的 =false最後一次點選的 預設是最後一次點選的
// return 可以被呼叫執行的函式
function debounce(func,wait = 300,immediate = false){
console.log(‘debounce’,this) // debounce在click執行之前執行 是debounce的執行結果給了click 這個時候 debounce裡面的this 應該是window

let timer = null
return function anonymous(...params){
    console.log('ANY',this)  // 這個是click在頁面中點選執行的 this指向元素本身

    let now = immediate && timer === null;
    // 如果是立即執行,
    now ? func.call(this,...params) : null;

    // 每次點選都把之前設定的定時器清除
    clearTimeout(timer)

    // 重新設定一個新的定時器監聽wait時間內是否觸發第二次
    timer = setTimeout(()=>{
        // 手動恢復初始狀態
        timer = null;

        // this 是當前的元素
        // wait這麼久的等待中,沒有觸發第二次
        !immediate ? func.call(this,...params) : null;
    },wait)
}

}

function handle(){
setTimeout(()=>{
console.log(‘OK’)
},1000)
}

//document.body.onclick = handle; //如果瘋狂點選submit,handle會觸發很多次,那麼一秒後會輸出很多OK。

//body.onclick = function(){
// // 在匿名函式中 我們控制handle 只執行一次
//}

document.body.onclick = debounce(handle,500,true);

//…節流:在一段頻繁操作中,可以觸發多次,但是觸發的頻率由自己制定…
//滾動scroll,文字框輸入過程中的模糊匹配keydown都用節流
//例如:每次滾動過程中,瀏覽器都有最快反應時間(谷歌一般是5-6ms,ie一般是13-17ms),只要反應過來就會觸發一次函式,也就是說谷歌每5ms就會觸發一次,過於頻繁
function throttle(func, wait = 300) {
let timer=null,
previous = 0;//上一次觸發時間
return function anonymous(…params) {
let now = new Date(),
remaining = wait - (now - previous) // 還差多長時間 達到一次觸發頻率
if(remaining<=0){
// 兩次操作間隔時間已經超過wait了 可以觸發了
window.clearTimeout(timer)
previous = now
timer = null
func.call(this,…params)
}else if(!timer){
// 不符合觸發的頻率 設定定時器等待
timer = setTimeout(()=>{
timer = null
previous = new Date()
func.call(this,…params)
},remaining)
}
}
}
function handle() {
console.log(ok);
}
// window.scroll = handle; // 谷歌每5ms就會觸發一次,過於頻繁
window.scroll = throttle(handle);//每300ms觸發一次

相關文章