>  기사  >  웹 프론트엔드  >  JavaScript 요약 공유 클로저

JavaScript 요약 공유 클로저

WBOY
WBOY앞으로
2022-11-07 16:31:081193검색

이 글은 JavaScript에 대한 관련 지식을 제공하며, 클로저가 무엇인지, 왜 이런 방식으로 설계되었는지, 어떻게 사용할 수 있는지 등 클로저와 관련된 문제를 주로 소개합니다. 다음은 살펴보겠습니다. 도움이 되기를 바랍니다. 모든 사람.

JavaScript 요약 공유 클로저

[관련 권장 사항: JavaScript 비디오 튜토리얼, 웹 프론트 엔드]

클로저란 무엇인가요?

지식 포인트의 경우, 나는 어디서 시작하든 이 지식 포인트를 진정으로 이해하려면 세 가지 질문을 철저히 이해한 다음 이를 숙달하려면 실제로 연습해야 한다고 항상 믿어 왔습니다. 이 세 가지 질문은 다음과 같습니다.

  • 가 무엇인가요?
  • 왜 디자인하나요?
  • 어디서 사용할 수 있나요?

먼저 클로저가 무엇인지에 대한 질문에 대답하세요. 대부분의 사람들이 관련 글을 많이 읽어봤을 것이고, 많은 분들이 나름대로 설명을 해주셨기 때문에 먼저 제가 이해하는 설명을 드리고자 합니다. 두 가지 전제 조건이 있습니다.

  • 종결어미는 어휘 분석 중에 결정되었으므로 어휘 범위와 관련이 있습니다.

  • 클로저가 존재하기 위한 전제 조건은 프로그래밍 언어가 일류 시민으로서 기능을 지원해야 하므로 기능과 관련이 있다는 것입니다.

최종 결론은 다음과 같습니다.

  • 클로저는 먼저 구조입니다. 이 구조의 구성 요소는 함수 + 함수의 어휘 범위闭包首先是一个结构体,这个结构体的组成部分为  一个函数 + 该函数所处的词法作用域
  • 也就是闭包是由一个函数并且该函数能够记住声明自己的词法作用域所产生的结构体
  • 在内存中理解就是, 当一个函数被调用时,它所产生的函数执行上下文里的作用域链保存有其父词法作用域,所以父变量对象由于存在被引用而不会销毁,驻留在内存中供其使用。这样的情况就称为闭包。

上述的解释对于已经了解过闭包的人应该是一目了然的,但其实如果对于一个完全不知晓闭包的人来说,很可能是完全看不懂的。更甚至很多人其实仅仅只是记住了这种定义,而不是真的理解了这内涵。

所以我想用一个不一定精准的类比去帮助理解什么是闭包这东西,想象你写了一篇文章放在自己的服务器上,并且引用了自己的3篇文章作为参考。那么此时 一篇文章 + 服务器的环境 就类似于闭包。

在发表到网络上后被转载到其他的平台上,而其他平台上的读者点开你的文章阅读后想继续看你所引用的那些文章,就被准确无误的跳转到了你服务器里的文章中去。

在这个例子中,这篇文章保存了写这篇文章的服务器环境里的引用。  因而不论是在哪里读到文章,文章里所记得的参考文章引用指向永远是服务器里的地址。 这种情况叫做使用了闭包的特性。

可能例子还是不太好理解,毕竟它也没有很准确,闭包这概念就是有点抽象,没有想到现实中有什么具体的例子可以用来比喻。 如果有人想出更好的类比可以指出,我加以注释和描述。

为什么要设计出闭包?

对于为什么设计这点,仅以我自己粗浅的理解就是由于JavaScript是异步单线程的语言。对于异步编程来说,最大的问题就是当你编写了函数,而等到它真正调用的时机可能是之后任意的时间节点。

这对于内存管理来说是一个很大的问题,正常同步执行的代码,函数声明时和被调用时所需要的数据都还存留在内存中,可以无障碍的获取。而异步的代码,往往声明该函数的上下文可能已经销毁,等到在调用它时,如果内存中已经把它所需要的一些外部数据给清理了,这就是个很大的问题。

所以JavaScript解决的方案就是让函数能够记得自己之前所能获取数据的范围,统统都保存在内存里,只要该函数没有被内存回收,它自身以及所能记住的范围都不会被销毁

A입니다. 클로저는 선언의 어휘 범위를 기억하는 함수에 의해 생성된 구조입니다.

메모리에서 이해하는 것은 함수가 호출될 때 생성된 함수 실행 컨텍스트의 범위 체인이 상위 어휘 범위를 저장하므로 상위 변수 개체가 존재하고 참조되기 때문에 파괴되지 않는다는 것입니다. 사용을 위해 메모리에. 이 상황을 폐쇄라고 합니다.

위의 설명은 이미 클로저를 이해한 사람들에게는 명확해야 하지만, 사실 클로저에 대해 전혀 모르는 사람이라면 전혀 이해하지 못할 수도 있습니다. 게다가 실제로 많은 사람들이 이 정의만 기억할 뿐 그 의미를 실제로 이해하지는 못합니다.

🎜그래서 클로저가 무엇인지 이해하는 데 반드시 정확하지는 않은 비유를 사용하고 싶습니다. 기사를 작성하여 자신의 서버에 올리고 자신의 기사 중 3개를 참고 자료로 인용했다고 가정해 보겠습니다. 그러면 이때 기사+서버 환경은 클로저와 비슷합니다. 🎜🎜인터넷에 게재된 후 다른 플랫폼에서 재인쇄되었으며, 다른 플랫폼의 독자들이 귀하의 기사를 읽기 위해 클릭하고 귀하가 인용한 기사를 계속 읽고 싶어하여 귀하의 기사로 정확하게 리디렉션되었습니다. . 🎜🎜이 예에서는 이 기사가 작성된 서버 환경에 대한 참조를 저장합니다. 따라서 기사를 어디에서 읽어도 기사에서 기억되는 참고 기사 참조는 항상 서버의 주소를 가리킬 것입니다. 이러한 상황을 폐쇄 기능 사용이라고 합니다. 🎜🎜아마도 예시가 아직 이해하기 쉽지 않을 수도 있고, 결국 그다지 정확하지도 않습니다. 클로저의 개념이 다소 추상적이고, 현실에서 비유로 사용할 수 있는 구체적인 예시가 생각나지 않았습니다. 누구든지 지적할 수 있는 더 나은 비유가 생각나면 주석을 달고 설명하겠습니다. 🎜

클로저를 디자인해야 하는 이유는 무엇인가요? 🎜🎜이것이 설계된 이유에 대해 간단히 이해하자면 JavaScript는 비동기 단일 스레드 언어라는 것입니다. 비동기 프로그래밍의 가장 큰 문제는 함수를 작성할 때 함수가 실제로 호출되는 시간이 어느 시점보다 늦을 수 있다는 것입니다. 🎜🎜이것은 메모리 관리에 큰 문제입니다. 일반적으로 동기적으로 실행되는 코드의 경우 함수가 선언되고 호출될 때 필요한 데이터가 여전히 메모리에 남아 있어 아무런 방해 없이 실행될 수 있습니다. 비동기 코드는 종종 함수의 컨텍스트가 호출될 때 필요한 외부 데이터가 메모리에서 지워졌을 수 있다고 선언합니다. 🎜🎜그래서 JavaScript의 해결책은 함수가 이전에 얻을 수 있었던 데이터 범위를 기억하고 이를 모두 메모리에 저장하도록 허용하는 것입니다. 함수와 함수가 기억할 수 있는 범위는 파괴되지 않습니다. 🎜🎜여기서 기억에 남는 범위는 어휘 범위를 말하며, 정적이기 때문에 기억해야 합니다. 🎜🎜이 역시 JavaScript의 정적 디자인 범위로 인해 발생합니다. 동적 범위인 경우 함수가 호출될 때 호출 시 환경만 필요하며 자체 범위를 기억할 필요가 없습니다. 🎜🎜요약하자면:🎜
  • 解决词法作用域引发的问题内存不好管理异步编程里数据获取에 대한 폐쇄가 생성되었습니다.

고전적인 질문

원래는 폐쇄 상황을 가장 낮은 수준부터 설명하자는 생각이었는데, 나중에 여러 글들을 확인해 보니 이미 잘 쓰여진 글이 하나 있었습니다. 이것이 JavaScript 클로저의 기본 작동 메커니즘입니다. 이 설명을 먼저 읽고 나중에 제가 작성한 내용을 읽어보시면 됩니다.

다음과 같은 아주 고전적인 인터뷰 질문부터 시작해서 기사가 많이 나오는데, 밑바닥부터 제대로 설명해주는 사람이 없는 것 같아서, 차이점을 이해하기 위해 전체적인 과정을 정리해 볼 예정입니다.

for (var i = 0; i < 3; i++) {  setTimeout(function cb() {    console.log(i);
  }, 1000);
}

기본적으로 기본기가 있는 사람이라면 누구나 한 눈에 봐도 출력이 3이 3개인 것을 알 수 있습니다.

그런 다음 순차적으로 출력하도록 수정합니다. 일반적으로 다음과 같이 var를 수정하면 됩니다.

for (let i = 0; i < 3; i++) {  setTimeout(function cb() {    console.log(i);
  }, 1000);
}

이 방법으로 출력은 0, 1, 2입니다. 그리고 매 한 번씩 출력하는 대신 동시에 출력됩니다. 1초.

그렇다면 질문은, 왜일까요?

아래 내용은 건너뛰시고 먼저 설명을 직접 작성하셔서 제가 쓴 내용과 같은지 확인하셔도 됩니다.

1 먼저 변수 i가 var인 상황을 살펴보겠습니다.

코드가 실행되기 시작하면 실행 컨텍스트 스택과 메모리의 상황은 다음과 같습니다. 그 중 전역 객체의 变量i和全局执行上下文里变量环境里的变量i는 동일한 변수입니다.

그런 다음 루프가 시작됩니다. i = 0이면 첫 번째 타이머가 매크로 작업 대기열에 포함됩니다. 당분간은 이벤트 루프 범주에 속합니다. setTimeout이 대기열에 들어간 다음 실행됩니다. 이때 콜백 함수 cb가 힙 메모리에 생성되고, 함수가 생성될 때 [[scope]]가 생성됩니다. 실제 ECMA 규칙에서 [[scope]]는 상위 범위를 가리킵니다. 현재 전역 개체인 함수(범위는 개념적인 것이며 메모리의 실제 표현은 개체 또는 다른 것일 수 있는 데이터를 저장하는 구조입니다). 그러나 V8 엔진 구현에서는 실제로 전역 개체를 가리키는 것이 아니라 상위 범위의 어떤 변수가 함수에서 사용되는지 분석하고 이러한 변수를 클로저에 저장한 다음 범위를 가리킵니다. 각 함수에는 정확히 하나의 Closure 객체가 있습니다.


다음은 Chrome에서 Closure 개체를 볼 수 있는 위치에 대한 정보입니다. 보시다시피 bar 함수가 생성되면 상위 범위의 name 변수만 참조하므로 클로저 개체에 변수 name만 저장되고 age 변수는 존재하지 않습니다.


마찬가지로 i = 1, i = 2는 동일하며 최종 결과는 다음과 같습니다.

결국 i++로 인해 i = 3이 되고 루프가 종료되며 전역 코드는 다음과 같습니다. 실행. 이때의 결과는 다음과 같습니다.

그러면 타이머 콜백 함수의 실행 과정이 시작되고, 첫 번째 타이머에서 콜백 함수 실행을 시작하고 이를 실행 컨텍스트 스택에 푸시한 후 출력 i를 실행합니다. 그러나 변수 i는 어휘 환경과 변수 환경에서 찾을 수 없으므로 자체 [[scope]로 이동합니다. ]를 찾아 보면 Closure 개체에서 i가 3과 같고 출력 결과는 3입니다.

마찬가지로 다음 두 타이머에 대해서도 프로세스는 동일하며 실제로 타이머가 시작되는 시간은 루프에서 즉시 실행되므로 세 기능의 1초 타이밍이 일관되게 됩니다. , 최종 출력 결과는 3개의 3을 거의 동시에 출력하는 것입니다. 물론 1초에 3개를 출력하는 것이 아니라 타이머 관련 지식입니다.

2. 그런 다음 var를 수정한 후 실제로 변경된 사항에 대해 논의합니다.

처음 생성되었을 때 표시된 상황은 다음과 같습니다.

그런 다음 i = 0일 때 루프 본문을 입력합니다.

그런 다음 i = 1일 때 상황에 들어갑니다.

마지막으로 i = 2일 때 상황에 들어갑니다. 이는 기본적으로 i = 1과 유사합니다.

마지막으로 i++는 i 값 3이 되고, 루프를 마칩니다. 타이머 작업 시작:

当执行第一个定时器的回调函数时,创建了函数执行上下文,此时执行输出语句i时,会先从自己的词法环境里寻找变量i的值,也就是在 record环境记录里搜索,但是不存在。因而通过自己外部环境引用outer找到原先创建的块级作用域里 i = 0的情况, 输出了i值为0的结果。

对于之后的定时器也都是一样的情况,原先的块级作用域由于被回调函数所引用到了,因而就产生了闭包的情况,不会在内存中被销毁,而是一直留着。

等到它们都执行完毕后,最终内存回收会将之全部都销毁。

其实以上画的图并不是很严谨,与实际在内存中的表现肯定是有差异的,但是对于理解闭包在内存里的情况还是不影响的。

闭包能用在哪?

首先需要先明确一点,那就是在JavaScript中,只要创建了函数,其实就产生了闭包。这是广义上的闭包,因为在全局作用域下声明的函数,也会记着全局作用域。而不是只有在函数内部声明的函数才叫做闭包。

通常意义上所讨论的闭包,是使用了闭包的特性

1. 函数作为返回值

let a = 1function outer() {  let a = 2

  function inside() {
    a += 1
    console.log(a)
  }  return inside
}const foo = outer()foo()

此处outer函数调用完时,返回了一个inside函数,在执行上下文栈中表示的既是outer函数执行上下文被销毁,但有一个返回值是一个函数。 该函数在内存中创建了一个空间,其[[scope]]指向着outer函数的作用域。因而outer函数的环境不会被销毁。

当foo函数开始调用时,调用的就是inside函数,所以它在执行时,先询问自身作用域是否存在变量a, 不存在则向上询问自己的父作用域outer,存在变量a且值为2,最终输出3。

2. 函数作为参数

var name = &#39;xavier&#39;function foo() {  var name = &#39;parker&#39;
  function bar() {    console.log(name)
  } console.log(name)  return bar
}function baz(fn) {  var name = &#39;coin&#39;
  fn()
}baz(foo())baz(foo)

对于第一个baz函数调用,输出的结果为两个'parker'。 对于第二个baz函数的调用,输出为一个'parker'。

具体的理解其实跟上面一致,只要函数被其他函数调用,都会存在闭包。

3. 私有属性

闭包可以实现对于一些属性的隐藏,外部只能获取到属性,但是无法对属性进行操作。

function foo(name) {  let _name = name  return {    get: function() {      return _name
    }
  }
}let obj = foo(&#39;xavier&#39;)
obj.get()

4. 高阶函数,科里化,节流防抖等

对于一些需要存在状态的函数,都是使用到了闭包的特性。

// 节流function throttle(fn, timeout) {  let timer = null
  return function (...arg) {    if(timer) return
    timer = setTimeout(() => {
    fn.apply(this, arg)
    timer = null
    }, timeout)
  }
}// 防抖function debounce(fn, timeout){  let timer = null
  return function(...arg){    clearTimeout(timer)
    timer = setTimeout(() => {
      fn.apply(this, arg)
    }, timeout)
  }
}

5. 模块化

在没有模块之前,对于不同地方声明的变量,可能会产生冲突。而闭包能够创造出一个封闭的私有空间,为模块化提供了可能性。 可以使用IIFE+闭包实现模块。

var moduleA = (function (global, doc) {  var methodA = function() {};  var dataA = {};  return {    methodA: methodA,    dataA: dataA
  };
})(this, document);

【相关推荐:JavaScript视频教程web前端

위 내용은 JavaScript 요약 공유 클로저의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

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