ホームページ >ウェブフロントエンド >フロントエンドQ&A >JavaScript クロージャーはスコープですか?

JavaScript クロージャーはスコープですか?

青灯夜游
青灯夜游オリジナル
2021-09-07 16:58:121844ブラウズ

JavaScript では、クロージャはスコープではなく、永続化できる関数コンテキスト アクティブ オブジェクトであり、関数オブジェクトとスコープ オブジェクトの両方への参照を含むオブジェクトです。クロージャは主に、スコープ チェーンまたはプロトタイプ チェーン上の変数または値を取得するために使用されます。

JavaScript クロージャーはスコープですか?

このチュートリアルの動作環境: Windows7 システム、JavaScript バージョン 1.8.5、Dell G3 コンピューター。

スコープ チェーンが識別子を検索する順序は、現在のスコープから始まり、一度に 1 レベルずつ上位に検索されることがわかっています。したがって、スコープ チェーンを通じて、JavaScript 関数内の変数は関数の外部の変数を読み取ることができますが、逆に、関数の外部の変数は通常、関数内の変数を読み取ることはできません。実際のアプリケーションでは、関数の外部で関数のローカル変数にアクセスする必要がある場合がありますが、この場合、最も一般的な方法はクロージャを使用することです。

クロージャは JavaScript の重要な機能の 1 つであり、関数型プログラミングにおいて重要な役割を果たしますが、このセクションではクロージャの構造と基本的な使い方を紹介します。

だからクロージャとは何ですか?

クロージャは、永続化できる関数コンテキスト アクティブ オブジェクトで、関数オブジェクトとスコープ オブジェクトの両方への参照を含むオブジェクトです。クロージャは主に、スコープ チェーンまたはプロトタイプ チェーン上の変数または値を取得するために使用されます。クロージャを作成する最も一般的な方法は、関数内で内部関数 (入れ子関数とも呼ばれます) を宣言し、その内部関数を返すことです。

現時点では、関数の外で関数を呼び出すことで内部関数を取得し、その後、内部関数を呼び出して関数のローカル変数にアクセスできます。この時の内部関数はクロージャです。クロージャの概念によれば、外部変数にアクセスするすべての 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 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。