>  기사  >  웹 프론트엔드  >  JavaScript 클로저에 대해 알아야 할 사항

JavaScript 클로저에 대해 알아야 할 사항

WBOY
WBOY앞으로
2022-01-21 17:58:511419검색

이 글은 클로저, 메소드 스택, 클로저의 역할을 포함한 JavaScript 클로저에 대한 연구 노트를 제공합니다. 여러분에게 도움이 되기를 바랍니다.

JavaScript 클로저에 대해 알아야 할 사항

정의상 스크립팅 언어이고, 비교적 배우기 쉬운 스크립팅 언어입니다. 많은 전문 지식이 없어도 js(JavaScript의 약어) 코드를 어느 정도 사용할 수도 있습니다.

물론, 이미 프런트 엔드 지식을 배웠다면 이 도구의 역할을 이해할 수 있을 것입니다. 이 도구는 페이지 요소 사이의 간격을 표시하는 데 매우 편리한 도구입니다. 보시다시피, 방금 몇 가지 간단한 브라우저 작업을 수행했고 위 코드의 내용조차 이해할 수 없지만 현재 페이지에 js 코드 조각을 삽입했습니다(분명히 무해합니다. 자유롭게 사용하세요). 사용방법) 업마스터 CodingStartup 최소강좌 영상 [이를 사용하면 웹페이지가 디자인 도면과 동일해집니다] 그리고 영상 아래 업마스터 ArcRain님의 답변 덕분에

이 스터디 노트의 목적은 나만의 js를 기록하는 것입니다. 학습 여행 중 일부 통찰과 경험, 그리고 제가 생각하는 일부 팁은 교육 목적이 아니기 때문에 일부 내용의 원리에 대해서는 답변을 드리지 않을 수도 있습니다. 아직 이해하지 못했을 뿐더러, 제 수준은 상당히 제한되어 있습니다. 만약 본문에 틀린 부분이 있다면 비판해 주시기 바랍니다.

1. JavaScript를 배울 수 있는 기회

JavaScript를 정식으로 배우는 것은 훈련 수업에서였습니다. 네, 저는 전공이 아닌 훈련 수업을 나왔습니다. 내가 공부할 당시에는 ES6 표준이 아직 대중화되지 않았고 변수 이름 지정에 여전히 매우 전통적인 var가 사용되었습니다. 제가 배운 첫 번째 코드는 고전적인 console.log('Hello, world!')였습니다. 콘솔에 코스 프린트가 나왔습니다.

물론 교육 기관의 JavaScript 콘텐츠는 매우 간단하며 가장 기본적인 변수 정의 및 이름 지정, 함수 선언, 콜백 함수, Ajax 및 가장 기본적인 DOM 작업만 있습니다. 분명히 이러한 내용은 작업에 전혀 부족합니다.

JS를 더 공부할 수 있는 기회는 직장에서 처음으로 노드에 대해 배웠고, 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
 }

저처럼 클로저가 무엇인지 모르거나 클로저에 대한 이해가 제한적이라면 이에 대한 질문도 있어야 합니다. 아이디어는 대략 다음과 같습니다

The 두 번째 줄은 JSONP에 할당된 값이 없습니다. 세 번째 줄은 JSONP 값이 비어 있지 않은지 확인합니다. 나중에 읽을 필요는 없습니다. 시간 낭비라면 100% 입장이 불가능합니다!

보세요, 이전에는 과제가 없었고 직접 판단하면 분명히 null입니다. 그러나 실제로 사용하면 이 장소에 대한 첫 번째 호출은 실제로 이 분기에 들어 가지 않지만 두 번째 호출하는 한 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. 클로저는 어떤 모습인가요?

그래요, 클로저가 무엇인지는 살펴봤습니다. 적어도 클로저에는 반환 기능이 있다는 점은 확인했습니다. {}

아니요!

차별화된 특징은 함수 속의 함수!

다음 메소드를 관찰하세요

/*第一个案例*/
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. 클로저 및 메소드 스택(원칙에 관심이 없으면 건너뛰셔도 됩니다.)

클로저의 정의, 1. 변수 범위 밖에서 변수에 액세스할 수 있습니다. 2. 어떤 방법으로 지역 변수의 수명 주기를 연장합니다. 3. 지역 변수의 생존 시간이 타임 루프 실행 시간을 초과하도록 합니다.

이벤트 루프의 개념은 3에 포함되므로 여기서는 처음 두 가지 방법의 정의를 주로 논의하겠습니다.

메서드 스택이 무엇인지 알고 있다면 건너뛰셔도 됩니다

局部作用域:在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 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 juejin.im에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제