JavaScript クロージャーについて知っておくべきこと

WBOY
リリース: 2022-01-21 17:58:51
転載
1385 人が閲覧しました

この記事では、クロージャ、メソッド スタック、クロージャの役割など、JavaScript クロージャに関する学習メモを提供します。

JavaScript クロージャーについて知っておくべきこと

定義上、これはスクリプト言語であり、比較的習得が簡単なスクリプト言語です。専門知識がなくても、ある程度の js (JavaScript の略) コードを使用することもできます。

もちろん、フロントエンドの知識を学んだ方ならこのツールの役割は理解できると思いますが、ページ要素間の間隔を表示するのに非常に便利なツールです。簡単なブラウザ操作を実行しただけで、上記のコードの内容さえ理解していないかもしれませんが、閲覧中のページに js コードを埋め込んだだけです (明らかに無害です。ご自由に埋め込んでください)使ってください)アップマスターのCodingStartupミニマム講座の動画【これでWebページが設計図と同じになります】と動画下のアップマスターArcRainさんの返信のおかげです

この目的学習ノートは私を記録するためのものです。私は JS の学習過程でいくつかの洞察と経験を持っています。また、教えるためではないと思われるヒントもいくつかあるので、一部の内容の原則については答えません。正確に説明することはできませんが、まだ理解できていない可能性があり、私のレベルは非常に限られています。文章に間違いがあれば、ご批判ください。

1. JavaScript を学ぶ機会

私はトレーニング クラスで JavaScript を正式に学びました。はい、私は専攻からではなく、トレーニング クラスから来たと言えます。非常に草の根的です。私が勉強していたとき、ES6 標準はまだ普及しておらず、変数にはまだ非常に伝統的な var を使用して名前が付けられていました。私が最初に学んだコードは古典的な console.log('Hello, world!') でした。コンソールに表示されます。

もちろん、研修機関の JavaScript コンテンツは非常に単純で、最も基本的な変数の定義と命名、関数宣言、コールバック関数、ajax、および最も基本的な dom 操作のみです。明らかに、これらの内容では作品としてはまったく不十分です。

jsを学ぶきっかけは仕事でした、仕事で初めてnodeのことを知り、jsでもバックエンドとして使えることを知り(JAVAトレーニングをしています)、少しずつ勉強できるようになりました一部の ES6 標準に準拠しています。もちろん、これらはすべて後回しで、最初にぶつかった最大の障害はこの製品でした。

2. '気持ち悪い' クロージャー

ああ、私はほんの少しの知識しかなく、弊社でカプセル化した jsonp コードはまったく理解できません。

  var jsonp = (function(){
        var JSONP;
       return function(url){
           if (JSONP) {
             document.getElementsByTagName("head")[0].removeChild(JSONP);
          }
         JSONP = document.createElement("script");
          JSONP.type = "text/javascript";
          JSONP.src = url;
          document.getElementsByTagName("head")[0].appendChild(JSONP);
       }
     }())
ログイン後にコピー

もちろん、この方法はブラウザのコンソールから直接使用できなくなりました。XSS 攻撃を防ぐために、ブラウザはそのようなコードの挿入を禁止しましたが、サーバー上では引き続き使用できます。 . もちろん、これらは重要ではありません。

ここが重要なポイントです

    if (JSONP) {
       //dosome
 }
ログイン後にコピー

もしあなたが私と同じで、クロージャが何なのかわからない、あるいはクロージャについてあまり理解していなかったとしても、これについて疑問を持つはずです。アイデアは大まかに次のようなものです

行 2 は JSONP を定義していますが、値を割り当てていません。現在、JSONP 値は null です。3 行目はメソッドを返します。4 行目は JSONP 値が空かどうかを検出します。は空ではありません、何かをします。、わかりました、後で読む必要はありません、これが無駄に書かれている場合、入力は 100% 不可能です。

ご覧のとおり、前に代入がなく、直接判定されると、明らかに null になります。しかし、実際に使ってみると、最初にこの場所を呼び出したときは確かにこのブランチには入りませんが、2 回目に呼び出した場合には 100% このブランチに入ることがわかります。

// 这个是一个可以在控制台输出的闭包版本,你可以自己试一下
var closedhull = (function() {
    let name = null; // 这里直接赋值为null
    return function(msg){
        if(name) {
            console.log('name:', name)
            return name += msg;
        }
        return name = msg;
    }
}())
closedhull('我是第一句。') //我是第一句。
closedhull('我是第二句。') //我是第一句。我是第二句。
ログイン後にコピー

上記の例を実行した後、console.log() または戻り値から、実際に if(name) のブランチに入ったことを確認するのは難しくありません。これがクロージャのパフォーマンスです。クロージャの定義は次のとおりです。

クロージャは、他の関数の内部変数を読み取ることができる関数です。

3. クロージャとはどのようなものですか?

さて、クロージャが何であるかを理解したので、クロージャが使用できるかどうかについては話さないでください。少なくとも、私はそれを見ました。パッケージには独特の機能があります return function(){}

いいえ!

その特徴は、関数内関数です。

次のメソッドを確認してください

/*第一个案例*/
function test1(){
    // a应该在方法运行结束后销毁
    let a = 1;
    return {
        add: function(){
            return ++a;
        }
    }
}
let a = test1();
a.add()//2
a.add()//3
/*第二个案例*/
(function(){
    // b应该在方法运行结束后销毁
    let b = 1,
        timer = setInterval(()=>{
        console.log(++b)
    }, 2000)
    setTimeout(()=>{
        clearInterval(timer)
    }, 10000)
})()// 2 3 4 5 6
/*第三个案例*/
function showMaker(obj){
    // obj应该在方法运行结束后销毁
    return function(){
        console.log(JSON.stringify(obj))
    }
}
let shower = showMaker({a:1})
// 显然这里你还能看到他
shower(); // {"a":1}
/*第四个案例*/
let outObj = (function(){
    let c = 'hello',
        obj = {};
    Object.defineProperty(obj, 'out', {
        get(){
            return c;
        },
        set(v){
            c = v;
        }
    });
    return obj
})()
outObj.out // 可以读取并设置c的值
ログイン後にコピー

これら 4 つはクロージャであり、すべてメソッド内のメソッドの特性を持っています。

4. クロージャとメソッドスタック (原理に興味がない場合は読み飛ばしてください)

クロージャの定義 1. 変数のスコープ外から変数にアクセスできる。 2. 何らかの手段でローカル変数のライフサイクルを延長します。 3. ローカル変数の生存時間をタイムループの実行時間を超えるようにする

3 には後述するイベントループの概念が関係しますが、ここでは主に最初の 2 つのメソッドの定義について説明します。

メソッド スタックが何であるかを知っている場合は、スキップしてください

局部作用域:在ES6之前,一般指一个方法内部(从参数列表开始,到方法体的括号结束为止),ES6中增加let关键字后,在使用let的情况下是指在一个{}中的范围内(显然,你不能在隐式的{}中使用let,编译器会禁止你做出这种行为的,因为没有{}就没有块级作用域),咱们这里为了简化讨论内容,暂且不把let的块级作用域算作闭包的范畴(其实应该算,不过意义不大,毕竟,你可以在外层块声明它。天啊,JS的命名还没拥挤到需要在一个方法内再去防止污染的程度。)

局部变量:区别于全局变量,全局变量会在某些时候被意外额创造和使用,这令人非常的...恼火和无助。局部变量就是在局部作用域下使用变量声明关键字声明出来的变量,应该很好理解。

局部变量的生命周期:好了,你在一个局部作用域中通过关键字(var const let等)声明了一个变量,然后给它赋值,这个局部变量在这个局部作用域中冒险就开始了,它会被使用,被重新赋值(除了傲娇的const小姐外),被调用(如果它是个方法),这个局部变量的本质是一个真实的值,区别在于如果它是个对象(对象,数组,方法都是对象)那么,它其实本质是一个地址的指针。如果它一个基础类型,那么它就是那个真实的值。它之所以存活是因为它有个住所。内存。

局部作用域与内存:每当出现一个局部作用域,一个方法栈就被申请了出来,在这个方法栈大概长这样子

|  data5 |
|  data4 |
|  data3 |
|  data2 |
|__data1_|
ログイン後にコピー

当然,它是能够套娃的,长这个样子

|  | d2 |  |
|  |_d1_|  |
|  data3   |
|  data2   |
|__data1___|
ログイン後にコピー

如果上面的东西是在太过于抽象,那么,我可以用实际案例展示一下

function stack1(){
    var data1,
        data2,
        data3,
        data4,
        data5
}
function stack2(){
    var data1,
        data2,
        data3;
    function stackInner(){
        var d1,
            d2;
    }
}
ログイン後にコピー

如果方法栈能够直观的感受的话,大约就是这个样子,咱们重点来分析stack2的这种情况,同时写一点实际内容进去

function stack2(){
    var data1 = '1',
        data2 = {x: '2'},
        data3 = '3';
    function stackInner(){
        var d1 = '4',
            d2 = {y: '5'};
    }
    stackInner()
}
stack2()
ログイン後にコピー

显然其中data1,data3,d1持有的是基本类型(string),data2,d2持有的是引用类型(object),反应到图上

运行时的方法栈的样子

            |------>{y: '5'}
            |    |->{x: '2'}
    |  | d2-|   || |
    |  |_d1='4'_|| |
    |  data3='3' | |
    |  data2 ----| |
    |__data1='1'___|
ログイン後にコピー

画有点抽象...就这样吧。具体对象在哪呢?他们在一个叫堆的地方,不是这次的重点,还是先看方法栈内的这些变量,运行结束后,按照先进后出的原则,把栈内的局部变量一个一个的销毁,同时堆里的两个对象,由于引用被销毁,没了继续存在的意义,等待被垃圾回收。

接下来咱们要做两件事情:

  • d1不再等于4了,而是引用data1

  • return stackInner 而不是直接调用

这样闭包就完成了

function stack2(){
    var data1 = {msg: 'hello'},
        data2 = {x: '2'},
        data3 = '3';
    function stackInner(){
        var d1 = data1,
            d2 = {y: '5'};
    }
    return stackInner
}
var out = stack2()
ログイン後にコピー

这里有一个要点,d2赋值给data1一定是在stackInner中完成的,原因?因为再stackInner方法中d2才被声明出来,如果你在stack2中d1 = data1那么恭喜你,你隐式的声明了一个叫d1的全局变量,而且在stackInner由于变量屏蔽的原因,你也看不到全局上的d2,原本计划的闭包完全泡汤。

变量屏蔽:不同作用域中相同名称的变量就会触发变量屏蔽。

看看栈现在的样子

运行时的方法栈的样子

               |------>{y: '5'}
out<---|       | |----|
    |  |  | d2-| | |  |  |
    |  |--|_d1---|_|  |  |
    |     data3=&#39;3&#39;   |  |
    |     data2(略)   |  |
    |_____data1<------|__|
ログイン後にコピー

好了,这个图可以和我们永别了,如果有可能,我后面会用画图工具替代,这么画图实在是太过邪典了。

这里涉及到了方法栈的一个特性,就是变量的穿透性,外部变量可以在内部的任意位置使用,因为再内部执行结束前,外部变量会一直存在。

由于stackInner被外部的out引用,导致这个对象不会随着方法栈的结束而销毁,接下来,最神奇的事情来了,由于stackInner这对象没有销毁,它内部d1依然保有data1所对应数据的引用,d1,d2一定会活下来,因为他们的爸爸stackInner活下来了,data1也以某种形式活了下来。

为什么说是某种形式,因为,本质上来说data1还是被销毁了。没错,只不过,data1所引用的那个对象的地址链接没有被销毁,这个才是本质。栈在调用结束后一定是会销毁的。但是调用本体(方法对象)只要存在,那么内部所引用的链接就不会断。

这个就是闭包的成因和本质。

5.闭包有什么用

OK,我猜测上一个章节估计很多人都直接跳过了,其实,跳过影响也不多,这个部分描述一下结论性的东西,闭包的作用。

它的最大作用就是给你的变量一个命名空间,防止命名冲突。要知道,你的框架,你export的东西,你import进来的东西,在编译的时候都会变成闭包,为的就是减少你变量对全局变量的污染,一个不依赖与import export的模块的代码大概长这个样子

(function(Constr, global){
    let xxx = new Constr(env1, env2, env3)
    global.NameSpace = xxx;
})(function(parm1, parm2, parm3) {
    //dosomeing
    reutrn {
        a: &#39;some1&#39;,
        b: &#39;some2&#39;,
        funcC(){
            //dosome
        },
        funcD(){
            //dosome
        }
    }
}, window)
ログイン後にコピー

当然这种封装代码的风格有多种多样的,但是大家都尽量把一套体系的内容都放到一个命名空间下,避免与其他框架产生冲突

相关推荐:javascript学习教程

以上がJavaScript クロージャーについて知っておくべきことの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

ソース:juejin.im
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
最新の問題
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート
私たちについて 免責事項 Sitemap
PHP中国語ウェブサイト:福祉オンライン PHP トレーニング,PHP 学習者の迅速な成長を支援します!