首頁  >  文章  >  web前端  >  javascript閉包是作用域嗎

javascript閉包是作用域嗎

青灯夜游
青灯夜游原創
2021-09-07 16:58:121803瀏覽

在javascript中,閉包不是作用域,而是一個能夠持續存在的函數上下文活動對象,是同時含有對函數對像以及作用域對象引用的對象。閉包主要是用來取得作用域鍊或原型鏈上的變數或值。

javascript閉包是作用域嗎

本教學操作環境:windows7系統、javascript1.8.5版、Dell G3電腦。

我們知道,作用域鏈尋找標識符的順序是從目前作用域開始一級一級往上查找。因此,透過作用域鏈,JavaScript 函數內部可以讀取函數外部的變量,但反過來,函數的外部通常無法讀取函數內部的變數。在實際應用中,有時需要在函數外部存取函數的局部變量,此時最常用的方法就是使用閉包。

閉包是 JavaScript 的重要特性之一,在函數式程式設計中有著重要的作用,本節介紹閉包的結構與基本用法。

那麼什麼是閉包?

閉包是一個能夠持續存在的函數上下文活動對象,是同時含有對函數對像以及作用域對象引用的對象。閉包主要是用來取得作用域鍊或原型鏈上的變數或值。創建閉包最常用的方式是在一個函數中聲明內部函數(也稱為巢狀函數),並傳回內部函數。

此時在函數外部就可以透過呼叫函數得到內部函數,進而呼叫內部函數來實現對函數局部變數的存取。此時的內部函數就是一個閉包。雖然依照閉包的概念,所有存取了外部變數的 JavaScript 函數都是閉包,但我們平常絕大部分時候所謂的閉包其實指的就是內部函數閉包。

閉包可以將一些資料封裝為私有屬性以確保這些變數的安全訪問,這個功能為應用程式帶來了極大的好處。要注意的是,閉包如果使用不當,也會帶來一些意想不到的問題。以下就透過幾個範例來示範閉包的建立、使用和可能存在的問題及其解決方法。

形成原理

函數被呼叫時,會產生一個暫存上下文活動物件。它是函數作用域的頂級對象,作用域內所有私有方法有變數、參數、私有函數等都會作為上下文活動對象的屬性而存在。

函數被呼叫後,在預設情況下上下文活動物件會被立即釋放,避免佔用系統資源。但是,若函數內的私有變數、參數、私有函數等被外界引用,則這個上下文活動物件暫時會繼續存在,直到所有外界引用都被註銷。

但是,函數作用域是封閉的,外界無法存取。那麼在什麼情況下,外界可以存取函數內的私有成員呢?

根據作用域鏈,內部函數可以存取外部函數的私有成員。如果內部函數引用了外部函數的私有成員,同時內部函數又被傳給外界,或是對外界開放,那麼閉包體就形成了。這個外部函數就是一個閉包體,它被呼叫後,活動物件暫時不會被註銷,其屬性會繼續存在,透過內部函數可以持續讀寫外部函數的私有成員。

閉包結構

典型的閉包體是一個巢狀結構的函數。內部函數引用外部函數的私有成員,同時內部函數又被外界引用,當外部函數被呼叫後,就形成了閉包。這個函數也稱為閉包函數。

下面是一個典型的閉包結構。

function f(x) {  //外部函数
    return function (y) {  //内部函数,通过返回内部函数,实现外部引用
        return x + y;  //访问外部函数的参数
    };
}
var c = f(5);  //调用外部函数,获取引用内部函数
console.log(c(6));  //调用内部函数,原外部函数的参数继续存在

解析過程簡單描述如下:

  • 在 JavaScript 腳本預編譯期,宣告的函數 f 和變數 c,先被詞法預先解析。

  • 在 JavaScript 執行期,呼叫函數 f,並傳入值 5。

  • 在解析函數 f 時,會建立執行環境(函數作用域)和活動對象,並將參數和私有變數、內部函數都映射為活動對象的屬性。

  • 參數 x 的值為 5,對應到活動物件的 x 屬性。

  • 內部函數透過作用域連結引用了參數 x,但是還沒有被執行。

  • 外部函數被呼叫後,傳回內部函數,導致內部函數被外界變數 c 引用。

  • JavaScript 解析器偵測到外部函數的活動對象的屬性被外界引用,無法註銷該活動對象,於是在記憶體中繼續維持該對象的存在。

  • 當呼叫 c,也就是呼叫內部函數時,可以看到外部函數的參數 x 儲存的值繼續存在。這樣就可以實現後續運算操作,回傳 x y=5=6=11。

如下結構形式也可以形成閉包:透過全域變數引用內部函數,實作內部函數對外開放。

var c;  //声明全局变量
function f(x) {  //外部函数
    c = function (y) {  //内部函数,通过向全局变量开放实现外部引用
        return x + y;  //访问外部函数的参数
    };
}
f(5);  //调用外部函数
console.log(c(6));  //使用全局变量c调用内部函数,返回11

閉包變體

除了巢狀函數外,如果外部引用函數內部的私有陣列或對象,也容易形成閉包。

var add;  //全局变量
function f() {  //外部函数
    var a = [1,2,3];  //私有变量,引用型数组
    add = function (x) {  //测试函数,对外开放
        a[0] = x * x;  //修改私有数组的元素值
    }
    return a;  //返回私有数组的引用
}
var c = f();
console.log(c[0]);  //读取闭包内数组,返回1
add(5);  //测试修改数组
console.log(c[0]);  //读取闭包内数组,返回25
add(10);  //测试修改数组
console.log(c[0]);  //读取闭包内数组,返回100

与函数相同,对象和数组也是引用型数据。调用函数 f,返回私有数组 a 的引用,即传值给局部变量 c,而 a 是函数 f 的私有变量,当被调用后,活动对象继续存在,这样就形成了闭包。

这种特殊形式的闭包没有实际应用价值,因为其功能单一,只能作为一个静态的、单向的闭包。而闭包函数可以设计各种复杂的运算表达式,它是函数式变成的基础。

反之,如果返回的是一个简单的值,就无法形成闭包,值传递是直接复制。外部变量 c 得到的仅是一个值,而不是对函数内部变量的引用。这样当函数调用后,将直接注销对象。

function f(x) {  //外部函数
    var a = 1;  //私有变量
    return a;
}
var c = f(5);
console.log(c);  //仅是一个值,返回1

使用闭包

下面结合示例介绍闭包的简单使用,以加深对闭包的理解。

示例1

使用闭包实现优雅的打包,定义存储器。

var f = function () {  //外部函数
    var a = [];  //私有数组初始化
    return function (x) {  //返回内部函数
        a.push(x);  //添加元素
        return a;  //返回私有数组
    };
} ()  //直接调用函数,生成执行环境
var a = f(1);  //添加值
console.log(a);  //返回1
var b = f(2);  //添加值
console.log(b);  //返回1,2

在上面示例中,通过外部函数设计一个闭包,定义一个永久的存储器。当调用外部函数生成执行环境之后,就可以利用返回的匿名函数不断地的向闭包体内的数组 a 传入新值,传入的值会持续存在。

示例2

在网页中事件处理函数很容易形成闭包。

<script>
function f() {
    var a = 1;
    b = function () {
        console.log("a =" + a);
    }
    c = function () {
        a ++;
    }
    d = function () {
        a --;
    }
}
</script>
<button onclick="f()">生成闭包</button>
<button onclick="b()">查看 a 的值</button>
<button onclick="c()">递增</button>
<button onclick="d()">递减</button>

在浏览器中浏览时,首先点击“生成闭包”按钮,生成一个闭包;点击“查看 a 的值”按钮,可以随时查看闭包内私有变量 a 的值;点击“递增”“递减”按钮时,可以动态修改闭包内变量 a 的值,效果如图所示。

javascript閉包是作用域嗎

闭包的局限性

闭包的价值是方便在表达式运算过程中存储数据。但是,它的缺点也不容忽视。

  • 由于函数调用后,无法注销调用对象,会占用系统资源,在脚本中大量使用闭包,容易导致内存泄漏。解决方法:慎用闭包,不要滥用。

  • 由于闭包的作用,其保存的值是动态,如果处理不当容易出现异常或错误。

示例

设计一个简单的选项卡效果。HTML 结构如下:

<div class="tab_wrap">
    <ul class="tab" id="tab">
        <li id="tab_1" class="hover">Tab1</li>
        <li id="tab_2" class="normal">Tab2</li>
        <li id="tab_3" class="normal">Tab3</li>
    </ul>
    <div class="content" id="content">
        <div id="content_1" class="show"><img  scr="image/1.jpg"    style="max-width:90%" / alt="javascript閉包是作用域嗎" ></div>
        <div id="content_2" class="show"><img  scr="image/2.jpg"    style="max-width:90%" / alt="javascript閉包是作用域嗎" ></div>
        <div id="content_3" class="show"><img  scr="image/3.jpg"    style="max-width:90%" / alt="javascript閉包是作用域嗎" ></div>
    </div>
</div>

下面请看 JavaScript 脚本。

window.onload = function () {
    var tab = document.getElementById("tab").getElementsByTagName("li"),
        content = document.getElementById("content").getElementByTagName("div");
    for (var i = 0; i < tab.length;i ++) {
        tab[i].addEventListener("mouseover"), function () {
            for (var n = 0; n < tab.length; n ++) {
                tab[n].className = "normal";
                content[n].className = "none";
            }
            tab[i].className = "hover";
            content[i].className = "show";
        });
    }
}

在 load 事件处理函数中,使用 for 语句为每个 li 属性元素绑定 mouseover 事件;在 mouseover 事件处理函数中重置所有选项卡 li 的类样式,然后设置当前 li 选项卡高亮显示,同时显示对应的内容容器。

但是在浏览器中预览时,会发现浏览器抛出异常。

SCRIPT5007:无法设置未定义或 null 引用的属性"className"

在 mouseover 事件处理函数中跟踪变量 i 的值,i 的值都变为了 3,tab[3] 自然是一个 null,所以也不能够读取 className 属性。

【原因分析】

上面 JavaScript 代码是一个典型的嵌套函数结构。外部函数为 load 事件处理函数,内部函数为 mouseover 事件处理函数,变量 i 为外部函数的私有变量。

通过事件绑定,mouseover 事件处理函数被外界引用(li 元素),这样就形成了一个闭包体。虽然在 for 语句中为每个选项卡 li 分别绑定事件处理函数,但是这个操作是动态的,因此 tab[i] 中 i 的值也是动态的,所以就出现了上述异常。

【解决方法】

解决闭包的缺陷,最简单的方法是阻断内部函数对外部函数的变量引用,这样就形成了闭包体。针对本示例,我们可以在内部函数(mouseover 事件处理函数)外边增加一层防火墙,不让其直接引用外部变量。

window.load = function () {
    var tab = document.getElementById("tab").getElementsByTagName("li"),
        content = document.getElementById("content").getElementsByTagName("div");
    for (var i = 0; i < tab.length; i ++ ) {
        (function (j) {
            tab[j].addEventListener("number", function () {
                for (var n = 0; n < tab.length; n ++) {
                    tab[n].className = "normal";
                    content[n].className = "none";
                }
                tab[j].className = "hover";
                conteng[j].className = "show";
            });
        }) (i);
    }
}

在 for 语句中,直接调用匿名函数,把外部函数的 i 变量传给调用函数,在调用函数中接收这个值,而不是引用外部变量 i,规避了闭包体带来的困惑。

【推荐学习:javascript高级教程

以上是javascript閉包是作用域嗎的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn