首頁 >web前端 >js教程 >一起認識閉包

一起認識閉包

coldplay.xixi
coldplay.xixi轉載
2020-09-08 13:25:052619瀏覽

一起認識閉包

相關學習推薦: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的一个副作用, 如果当前词法作用域没有该属性,会在全局创建一个

闭包能干啥?

闭包的使用场景可多了,平时使用的插件或者框架,基本上都有闭包的身影,可能您没留意过罢了。下面笔者列举一些比较常见的场景。

  1. 模拟私有变量和方法,进一步来说可以是模拟模块化;目前常用的AMD,CommonJS等模块规范,都是利用闭包的思想;

  2. 柯里化函数或者偏函数;利用闭包可以把参数分成多次传参。如下面代码:

// 柯里化函数
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;
}
  1. 实现防抖或者节流函数;

  2. 实现缓存结果(记忆化)的辅助函数:

// 该方法适合缓存结果不易改变的函数
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中文網其他相關文章!

陳述:
本文轉載於:juejin.im。如有侵權,請聯絡admin@php.cn刪除