Làm thế nào để đóng JavaScript hoạt động?


7636

Làm thế nào bạn có thể giải thích các bao đóng JavaScript cho ai đó có kiến ​​thức về các khái niệm mà chúng bao gồm (ví dụ các hàm, biến và tương tự), nhưng không hiểu chính chúng đóng?

Tôi đã thấy ví dụ Scheme đưa ra trên Wikipedia, nhưng tiếc là nó không giúp được gì.


391
Vấn đề của tôi với những điều này và nhiều câu trả lời là họ tiếp cận nó từ góc độ lý thuyết, trừu tượng, thay vì bắt đầu giải thích đơn giản tại sao việc đóng cửa lại cần thiết trong Javascript và các tình huống thực tế mà bạn sử dụng chúng. Bạn kết thúc với một bài viết tl; dr mà bạn phải lướt qua, tất cả thời gian suy nghĩ, "nhưng, tại sao?". Tôi chỉ đơn giản là bắt đầu với: đóng cửa là một cách xử lý gọn gàng với hai thực tế sau đây của JavaScript: a. phạm vi là ở cấp độ chức năng, không phải cấp độ khối và, b. phần lớn những gì bạn làm trong thực tế trong JavaScript là không đồng bộ / hướng sự kiện.
Jeremy Burton

53
@Redsandro Đối với một, nó làm cho mã hướng sự kiện dễ dàng hơn nhiều để viết. Tôi có thể kích hoạt một chức năng khi trang tải để xác định các chi tiết cụ thể về HTML hoặc các tính năng khả dụng. Tôi có thể định nghĩa và đặt trình xử lý trong hàm đó và có sẵn tất cả thông tin ngữ cảnh đó mỗi khi trình xử lý được gọi mà không phải truy vấn lại. Giải quyết vấn đề một lần, sử dụng lại trên mỗi trang trong đó trình xử lý đó là cần thiết với việc giảm chi phí cho việc gọi lại trình xử lý. Bạn có bao giờ thấy cùng một dữ liệu được ánh xạ lại hai lần trong một ngôn ngữ không có chúng không? Đóng cửa làm cho nó dễ dàng hơn rất nhiều để tránh loại điều đó.
Erik Reppen

1
@Erik Reppen cảm ơn câu trả lời. Trên thực tế, tôi đã tò mò về lợi ích của closuremã khó đọc này, trái ngược với việc Object Literalsử dụng lại chính nó và giảm chi phí như nhau, nhưng yêu cầu mã gói ít hơn 100%.
Redsandro

6
Đối với các lập trình viên Java, câu trả lời ngắn gọn là đó là hàm tương đương với một lớp bên trong. Một lớp bên trong cũng giữ một con trỏ ẩn cho một thể hiện của lớp bên ngoài và được sử dụng cho nhiều mục đích tương tự (nghĩa là tạo các trình xử lý sự kiện).
Boris van Schooten

8
Tôi thấy ví dụ thực tế này rất hữu ích: youtube.com/watch?v=w1s9PgtEoJs
Abhi

Câu trả lời:


7359

Một bao đóng là một cặp:

  1. Một chức năng, và
  2. Tham chiếu đến phạm vi bên ngoài của chức năng đó (môi trường từ vựng)

Một môi trường từ vựng là một phần của mọi bối cảnh thực hiện (khung ngăn xếp) và là bản đồ giữa các định danh (ví dụ: tên biến cục bộ) và các giá trị.

Mọi chức năng trong JavaScript đều duy trì một tham chiếu đến môi trường từ vựng bên ngoài của nó. Tham chiếu này được sử dụng để cấu hình bối cảnh thực hiện được tạo khi một hàm được gọi. Tham chiếu này cho phép mã bên trong hàm "nhìn thấy" các biến được khai báo bên ngoài hàm, bất kể khi nào và ở đâu hàm được gọi.

Nếu một hàm được gọi bởi một hàm, đến lượt nó được gọi bởi một hàm khác, thì một chuỗi các tham chiếu đến các môi trường từ vựng bên ngoài được tạo ra. Chuỗi này được gọi là chuỗi phạm vi.

Trong đoạn mã sau, innertạo thành một bao đóng với môi trường từ vựng của bối cảnh thực thi được tạo khi foođược gọi, đóng trên biến secret:

function foo() {
  const secret = Math.trunc(Math.random()*100)
  return function inner() {
    console.log(`The secret number is ${secret}.`)
  }
}
const f = foo() // `secret` is not directly accessible from outside `foo`
f() // The only way to retrieve `secret`, is to invoke `f`

Nói cách khác: trong JavaScript, các hàm mang tham chiếu đến một "hộp trạng thái" riêng tư mà chỉ chúng (và bất kỳ hàm nào khác được khai báo trong cùng môi trường từ vựng) có quyền truy cập. Hộp trạng thái này là vô hình đối với người gọi hàm, cung cấp một cơ chế tuyệt vời để ẩn dữ liệu và đóng gói.

Và hãy nhớ rằng: các hàm trong JavaScript có thể được truyền xung quanh như các biến (hàm hạng nhất), nghĩa là các cặp chức năng và trạng thái này có thể được truyền qua chương trình của bạn: tương tự như cách bạn có thể truyền một thể hiện của một lớp xung quanh trong C ++.

Nếu JavaScript không có các bao đóng, thì nhiều trạng thái sẽ phải được chuyển qua giữa các hàm một cách rõ ràng , làm cho danh sách tham số dài hơn và mã nhiễu hơn.

Vì vậy, nếu bạn muốn một hàm luôn có quyền truy cập vào một trạng thái riêng tư, bạn có thể sử dụng một bao đóng.

... và thường xuyên chúng ta muốn nhà nước liên kết với một hàm. Ví dụ, trong Java hoặc C ++, khi bạn thêm một biến cá thể riêng và một phương thức cho một lớp, bạn đang liên kết trạng thái với chức năng.

Trong C và hầu hết các ngôn ngữ phổ biến khác, sau khi hàm trả về, tất cả các biến cục bộ không còn truy cập được do khung stack bị hủy. Trong JavaScript, nếu bạn khai báo một hàm trong một hàm khác, thì các biến cục bộ của hàm ngoài có thể vẫn có thể truy cập được sau khi trở về từ hàm đó. Theo cách này, trong đoạn mã trên, secretvẫn có sẵn cho đối tượng hàm inner, sau khi nó được trả về foo.

Công dụng của đóng cửa

Đóng là hữu ích bất cứ khi nào bạn cần trạng thái riêng tư liên quan đến một chức năng. Đây là một kịch bản rất phổ biến - và hãy nhớ rằng: JavaScript không có cú pháp lớp cho đến năm 2015 và nó vẫn không có cú pháp trường riêng. Đóng cửa đáp ứng nhu cầu này.

Biến sơ thẩm

Trong đoạn mã sau, chức năng toStringđóng trên các chi tiết của chiếc xe.

function Car(manufacturer, model, year, color) {
  return {
    toString() {
      return `${manufacturer} ${model} (${year}, ${color})`
    }
  }
}
const car = new Car('Aston Martin','V8 Vantage','2012','Quantum Silver')
console.log(car.toString())

Lập trình chức năng

Trong đoạn mã sau, hàm innerđóng trên cả hai fnargs.

function curry(fn) {
  const args = []
  return function inner(arg) {
    if(args.length === fn.length) return fn(...args)
    args.push(arg)
    return inner
  }
}

function add(a, b) {
  return a + b
}

const curriedAdd = curry(add)
console.log(curriedAdd(2)(3)()) // 5

Lập trình hướng sự kiện

Trong đoạn mã sau, hàm onClickđóng trên biến BACKGROUND_COLOR.

const $ = document.querySelector.bind(document)
const BACKGROUND_COLOR = 'rgba(200,200,242,1)'

function onClick() {
  $('body').style.background = BACKGROUND_COLOR
}

$('button').addEventListener('click', onClick)
<button>Set background color</button>

Mô đun hóa

Trong ví dụ sau, tất cả các chi tiết triển khai được ẩn bên trong một biểu thức hàm được thực thi ngay lập tức. Các chức năng ticktoStringđóng trên trạng thái riêng tư và các chức năng họ cần để hoàn thành công việc của họ. Việc đóng cửa đã cho phép chúng tôi mô đun hóa và đóng gói mã của chúng tôi.

let namespace = {};

(function foo(n) {
  let numbers = []
  function format(n) {
    return Math.trunc(n)
  }
  function tick() {
    numbers.push(Math.random() * 100)
  }
  function toString() {
    return numbers.map(format)
  }
  n.counter = {
    tick,
    toString
  }
}(namespace))

const counter = namespace.counter
counter.tick()
counter.tick()
console.log(counter.toString())

Ví dụ

ví dụ 1

Ví dụ này cho thấy rằng các biến địa phương không được sao chép trong việc đóng cửa: việc đóng cửa duy trì một tham chiếu đến các biến ban đầu mình . Như thể khung stack vẫn tồn tại trong bộ nhớ ngay cả khi chức năng bên ngoài thoát ra.

function foo() {
  let x = 42
  let inner  = function() { console.log(x) }
  x = x+1
  return inner
}
var f = foo()
f() // logs 43

Ví dụ 2

Trong đoạn mã sau, ba phương pháp log, incrementupdatetất cả gần so với môi trường từ vựng tương tự.

Và mỗi khi createObjectđược gọi, một bối cảnh thực hiện mới (khung stack) được tạo và một biến hoàn toàn mới x, và một tập hợp các hàm mới ( logv.v.) được tạo ra, gần với biến mới này.

function createObject() {
  let x = 42;
  return {
    log() { console.log(x) },
    increment() { x++ },
    update(value) { x = value }
  }
}

const o = createObject()
o.increment()
o.log() // 43
o.update(5)
o.log() // 5
const p = createObject()
p.log() // 42

Ví dụ 3

Nếu bạn đang sử dụng các biến được khai báo bằng cách sử dụng var, hãy cẩn thận bạn hiểu biến nào bạn đang đóng. Các biến được khai báo sử dụng varđược nâng lên. Đây không phải là một vấn đề trong JavaScript hiện đại do sự ra đời của letconst.

Trong đoạn mã sau, mỗi lần xung quanh vòng lặp, một hàm mới innersẽ được tạo, sẽ đóng lại i. Nhưng vì var iđược nâng bên ngoài vòng lặp, tất cả các hàm bên trong này đóng cùng một biến, có nghĩa là giá trị cuối cùng của i(3) được in, ba lần.

function foo() {
  var result = []
  for (var i = 0; i < 3; i++) {
    result.push(function inner() { console.log(i) } )
  }
  return result
}

const result = foo()
// The following will print `3`, three times...
for (var i = 0; i < 3; i++) {
  result[i]() 
}

Điểm cuối cùng:

  • Bất cứ khi nào một hàm được khai báo trong JavaScript, một bao đóng được tạo.
  • Trả về một functiontừ bên trong một chức năng khác là ví dụ kinh điển về việc đóng, bởi vì trạng thái bên trong chức năng bên ngoài hoàn toàn có sẵn cho chức năng bên trong được trả về, ngay cả sau khi chức năng bên ngoài đã thực hiện xong.
  • Bất cứ khi nào bạn sử dụng eval()bên trong một chức năng, một đóng cửa được sử dụng. Văn bản bạn evalcó thể tham chiếu các biến cục bộ của hàm và trong chế độ không nghiêm ngặt, bạn thậm chí có thể tạo các biến cục bộ mới bằng cách sử dụng eval('var foo = …').
  • Khi bạn sử dụng new Function(…)( hàm tạo hàm ) bên trong một hàm, nó không đóng trên môi trường từ vựng của nó: thay vào đó, nó sẽ đóng trên bối cảnh toàn cầu. Hàm mới không thể tham chiếu các biến cục bộ của hàm ngoài.
  • Việc đóng trong JavaScript giống như giữ một tham chiếu ( KHÔNG phải là bản sao) cho phạm vi tại điểm khai báo hàm, lần lượt giữ tham chiếu đến phạm vi bên ngoài của nó, v.v., tất cả các cách đến đối tượng toàn cầu ở đầu chuỗi phạm vi.
  • Một bao đóng được tạo khi một hàm được khai báo; việc đóng này được sử dụng để cấu hình bối cảnh thực thi khi hàm được gọi.
  • Một tập hợp các biến cục bộ mới được tạo ra mỗi khi hàm được gọi.

Liên kết


74
Điều này nghe có vẻ hay: "Việc đóng trong JavaScript giống như giữ một bản sao của tất cả các biến cục bộ, giống như khi chúng thoát khỏi một chức năng." Nhưng nó là sai lệch vì một vài lý do. (1) Cuộc gọi chức năng không phải thoát để tạo một bao đóng. (2) Nó không phải là bản sao của các giá trị của các biến cục bộ mà là chính các biến đó. (3) Nó không nói ai có quyền truy cập vào các biến này.
dlaliberte

27
Ví dụ 5 hiển thị "gotcha" trong đó mã không hoạt động như dự định. Nhưng nó không chỉ ra cách khắc phục. Câu trả lời khác này cho thấy một cách để làm điều đó.
Matt

190
Tôi thích cách bài đăng này bắt đầu bằng những chữ in đậm lớn có nội dung "Đóng cửa không phải là phép thuật" và kết thúc ví dụ đầu tiên của nó bằng "Điều kỳ diệu là trong JavaScript, một tham chiếu chức năng cũng có một tham chiếu bí mật về việc đóng nó được tạo ra".
Andrew Macheret

6
Ví dụ # 3 là kết hợp các bao đóng với javascripts. Bây giờ tôi nghĩ rằng việc giải thích chỉ đóng cửa là đủ khó mà không mang lại hành vi cẩu thả. Điều này giúp tôi nhiều nhất: Closures are functions that refer to independent (free) variables. In other words, the function defined in the closure 'remembers' the environment in which it was created.từ developer.mozilla.org/en-US/docs/Web/JavaScript/Closures
caramba

3
ECMAScript 6 có thể thay đổi một cái gì đó trong bài viết tuyệt vời này về việc đóng cửa. Ví dụ, nếu bạn sử dụng let i = 0thay vì var i = 0trong Ví dụ 5, thì nó testList()sẽ in những gì bạn muốn ban đầu.
Nier

3988

Mọi chức năng trong JavaScript đều duy trì một liên kết đến môi trường từ vựng bên ngoài của nó. Một môi trường từ vựng là một bản đồ của tất cả các tên (ví dụ: biến, tham số) trong một phạm vi, với các giá trị của chúng.

Vì vậy, bất cứ khi nào bạn nhìn thấy functiontừ khóa, mã bên trong hàm đó có quyền truy cập vào các biến được khai báo bên ngoài hàm.

function foo(x) {
  var tmp = 3;

  function bar(y) {
    console.log(x + y + (++tmp)); // will log 16
  }

  bar(10);
}

foo(2);

Điều này sẽ đăng nhập 16vì hàm barđóng trên tham số xvà biến tmp, cả hai đều tồn tại trong môi trường từ vựng của hàm ngoài foo.

Chức năng bar, cùng với liên kết của nó với môi trường từ vựng của chức năng foolà một đóng cửa.

Một hàm không phải trả về để tạo một bao đóng. Đơn giản là nhờ tuyên bố của nó, mọi chức năng đóng trên môi trường từ vựng kèm theo của nó, tạo thành một bao đóng.

function foo(x) {
  var tmp = 3;

  return function (y) {
    console.log(x + y + (++tmp)); // will also log 16
  }
}

var bar = foo(2);
bar(10); // 16
bar(10); // 17

Hàm trên cũng sẽ đăng nhập 16, vì mã bên trong barvẫn có thể tham chiếu đến đối số xvà biến tmp, mặc dù chúng không còn trực tiếp trong phạm vi.

Tuy nhiên, vì tmpvẫn còn treo xung quanh bên trong barđóng cửa, nên nó có sẵn để được tăng lên. Nó sẽ được tăng lên mỗi khi bạn gọi bar.

Ví dụ đơn giản nhất về việc đóng cửa là đây:

var a = 10;

function test() {
  console.log(a); // will output 10
  console.log(b); // will output 6
}
var b = 6;
test();

Khi một hàm JavaScript được gọi, bối cảnh thực thi mới ecđược tạo. Cùng với các đối số hàm và đối tượng đích, bối cảnh thực thi này cũng nhận được một liên kết đến môi trường từ vựng của bối cảnh thực thi cuộc gọi, nghĩa là các biến được khai báo trong môi trường từ vựng bên ngoài (trong ví dụ trên, cả hai ab) đều có sẵn ec.

Mọi chức năng tạo ra một bao đóng bởi vì mọi chức năng đều có một liên kết đến môi trường từ vựng bên ngoài của nó.

Lưu ý rằng chính các biến được hiển thị từ trong một bao đóng, không phải bản sao.


24
@feeela: Có, mọi hàm JS đều tạo một bao đóng. Các biến không được tham chiếu có thể sẽ đủ điều kiện để thu gom rác trong các công cụ JS hiện đại, nhưng nó không thay đổi thực tế là khi bạn tạo bối cảnh thực thi, bối cảnh đó có tham chiếu đến bối cảnh thực thi kèm theo và các biến của nó và hàm đó là một đối tượng có tiềm năng được chuyển đến một phạm vi biến khác nhau, trong khi vẫn giữ tham chiếu ban đầu đó. Đó là sự đóng cửa.

@Ali Tôi vừa phát hiện ra rằng jsFiddle tôi đã cung cấp không thực sự chứng minh bất cứ điều gì, vì deletethất bại. Tuy nhiên, môi trường từ vựng mà hàm sẽ mang theo là [[Phạm vi]] (và cuối cùng sử dụng làm cơ sở cho môi trường từ vựng riêng của nó khi được gọi) được xác định khi câu lệnh xác định hàm được thực thi. Điều này có nghĩa là hàm đang đóng trên các nội dung ENTIRE của phạm vi thực thi, bất kể giá trị nào nó thực sự đề cập đến và liệu nó có thoát khỏi phạm vi đó hay không. Vui lòng xem các phần 13.2 và 10 trong thông số kỹ thuật
Asad Saeeduddin

8
Đây là một câu trả lời tốt cho đến khi nó cố gắng giải thích các loại và tài liệu tham khảo nguyên thủy. Nó hoàn toàn sai và nói về nghĩa đen được sao chép, điều này thực sự không liên quan gì.
Ry-

12
Đóng là câu trả lời của JavaScript đối với lập trình hướng đối tượng dựa trên lớp. JS không dựa trên lớp, vì vậy người ta phải tìm một cách khác để thực hiện một số thứ không thể thực hiện bằng cách khác.
Bartłomiej Zalewski

2
đây sẽ là câu trả lời được chấp nhận Phép thuật không bao giờ xảy ra trong chức năng bên trong. Nó xảy ra khi gán hàm ngoài cho một biến. Điều này tạo ra một bối cảnh thực thi mới cho hàm bên trong, do đó "biến riêng" có thể được tích lũy. Tất nhiên nó có thể vì biến số hàm ngoài được gán cho đã duy trì bối cảnh. Câu trả lời đầu tiên chỉ làm cho toàn bộ sự việc trở nên phức tạp hơn mà không giải thích những gì thực sự xảy ra ở đó.
Albert Gao

2442

LỜI NÓI ĐẦU: câu trả lời này được viết khi câu hỏi là:

Giống như ông già Albert đã nói: "Nếu bạn không thể giải thích điều đó với một đứa trẻ sáu tuổi, bạn thực sự không hiểu điều đó. Tôi đã cố gắng giải thích việc đóng cửa của JS với một người bạn 27 tuổi và hoàn toàn thất bại.

Bất cứ ai cũng có thể xem xét rằng tôi 6 tuổi và lạ lùng quan tâm đến chủ đề đó?

Tôi khá chắc chắn rằng tôi là một trong những người duy nhất cố gắng đưa ra câu hỏi ban đầu theo nghĩa đen. Kể từ đó, câu hỏi đã đột biến nhiều lần, vì vậy câu trả lời của tôi bây giờ có vẻ vô cùng ngớ ngẩn & không đúng chỗ. Hy vọng rằng ý tưởng chung của câu chuyện vẫn còn vui cho một số người.


Tôi là một fan hâm mộ lớn của sự tương tự và ẩn dụ khi giải thích các khái niệm khó khăn, vì vậy hãy để tôi thử sức mình với một câu chuyện.

Ngày xửa ngày xưa:

Có một công chúa ...

function princess() {

Cô sống trong một thế giới tuyệt vời đầy những cuộc phiêu lưu. Cô đã gặp Hoàng tử quyến rũ của mình, cưỡi ngựa vòng quanh thế giới của mình trên một con kỳ lân, chiến đấu với những con rồng, bắt gặp những con vật biết nói và nhiều điều kỳ diệu khác.

    var adventures = [];

    function princeCharming() { /* ... */ }

    var unicorn = { /* ... */ },
        dragons = [ /* ... */ ],
        squirrel = "Hello!";

    /* ... */

Nhưng cô sẽ luôn phải quay trở lại thế giới công việc buồn tẻ và trưởng thành của mình.

    return {

Và cô ấy thường nói với họ về cuộc phiêu lưu tuyệt vời mới nhất của cô ấy như một công chúa.

        story: function() {
            return adventures[adventures.length - 1];
        }
    };
}

Nhưng tất cả những gì họ sẽ thấy là một cô bé ...

var littleGirl = princess();

... kể những câu chuyện về ma thuật và tưởng tượng.

littleGirl.story();

Và mặc dù những người trưởng thành biết về những nàng công chúa thực sự, họ sẽ không bao giờ tin vào những con kỳ lân hay con rồng bởi vì họ không bao giờ có thể nhìn thấy chúng. Những người trưởng thành nói rằng họ chỉ tồn tại trong trí tưởng tượng của cô bé.

Nhưng chúng ta biết sự thật có thật; rằng cô bé với công chúa bên trong ...

... thực sự là một công chúa với một cô bé bên trong.


340
Tôi thích lời giải thích này, thực sự. Đối với những người đọc nó và không theo dõi, điều tương tự là: hàm Princess () là một phạm vi phức tạp chứa dữ liệu riêng tư. Ngoài chức năng, dữ liệu riêng tư không thể được nhìn thấy hoặc truy cập. Công chúa giữ những con kỳ lân, rồng, phiêu lưu, vv trong trí tưởng tượng của mình (dữ liệu riêng tư) và những người trưởng thành không thể nhìn thấy chúng. NHƯNG trí tưởng tượng của công chúa bị bắt giữ trong story()chức năng đóng cửa , đây là giao diện duy nhất mà littleGirlthể hiện trong thế giới ma thuật.
Patrick M

Vì vậy, đây storylà đóng cửa nhưng đã có mã var story = function() {}; return story;sau đó littleGirlsẽ là đóng cửa. Ít nhất đó là ấn tượng mà tôi có được từ việc sử dụng các phương thức 'riêng tư' của MDN với các lần đóng : "Ba chức năng công khai đó là các bao đóng có chung một môi trường."
icc97

16
@ icc97, vâng, storylà một đóng cửa tham chiếu môi trường được cung cấp trong phạm vi princess. princesscũng là một bao đóng ngụ ý khác , tức là princesslittleGirlsẽ chia sẻ bất kỳ tham chiếu nào đến một parentsmảng tồn tại trở lại trong môi trường / phạm vi nơi littleGirltồn tại và princessđược xác định.
Jacob Swartwood

6
@BenjaminKrupp Tôi đã thêm một nhận xét mã rõ ràng để hiển thị / ngụ ý rằng có nhiều hoạt động trong cơ thể princesshơn những gì được viết. Thật không may, câu chuyện này bây giờ là một chút không đúng chỗ trên chủ đề này. Ban đầu câu hỏi được yêu cầu "giải thích việc đóng cửa JavaScript cho một 5yr cũ"; Phản ứng của tôi là người duy nhất thậm chí đã cố gắng làm điều đó. Tôi không nghi ngờ rằng nó sẽ thất bại thảm hại, nhưng ít nhất phản hồi này có thể đã có cơ hội giữ mối quan tâm của 5yr cũ.
Jacob Swartwood

11
Trên thực tế, với tôi điều này làm cho ý nghĩa hoàn hảo. Và tôi phải thừa nhận, cuối cùng cũng hiểu được việc đóng cửa JS bằng cách sử dụng những câu chuyện về các nàng công chúa và những cuộc phiêu lưu khiến tôi cảm thấy hơi kỳ lạ.
Kết tinh

753

Đặt câu hỏi một cách nghiêm túc, chúng ta nên tìm hiểu xem một đứa trẻ 6 tuổi điển hình có khả năng nhận thức như thế nào, mặc dù phải thừa nhận rằng, một người quan tâm đến JavaScript không quá điển hình.

Về sự phát triển của trẻ em: 5 đến 7 năm có ghi:

Con của bạn sẽ có thể làm theo hướng dẫn hai bước. Ví dụ, nếu bạn nói với con, "Hãy vào bếp và lấy cho tôi một túi rác" chúng sẽ có thể nhớ hướng đó.

Chúng ta có thể sử dụng ví dụ này để giải thích các bao đóng, như sau:

Nhà bếp là một đóng cửa có một biến địa phương, được gọi là trashBags. Có một chức năng trong nhà bếp được gọi là getTrashBaglấy một túi rác và trả lại.

Chúng ta có thể viết mã này bằng JavaScript như thế này:

function makeKitchen() {
  var trashBags = ['A', 'B', 'C']; // only 3 at first

  return {
    getTrashBag: function() {
      return trashBags.pop();
    }
  };
}

var kitchen = makeKitchen();

console.log(kitchen.getTrashBag()); // returns trash bag C
console.log(kitchen.getTrashBag()); // returns trash bag B
console.log(kitchen.getTrashBag()); // returns trash bag A

Những điểm khác giải thích tại sao việc đóng cửa lại thú vị:

  • Mỗi lần makeKitchen()được gọi, một bao đóng mới được tạo riêng biệt trashBags.
  • Các trashBagsbiến là địa phương để các bên trong của mỗi bếp và không thể truy cập bên ngoài, nhưng các chức năng bên trong trên getTrashBagtài sản không có quyền truy cập vào nó.
  • Mỗi lệnh gọi hàm tạo ra một bao đóng, nhưng sẽ không cần phải giữ bao đóng trừ khi một hàm bên trong, có quyền truy cập vào bên trong của bao đóng, có thể được gọi từ bên ngoài bao đóng. Trả lại đối tượng với getTrashBagchức năng làm điều đó ở đây.

6
Trên thực tế, thật khó hiểu, lệnh gọi hàm makeK Kitchen là đóng thực tế, không phải là đối tượng nhà bếp mà nó trả về.
dlaliberte

6
Có cách của tôi thông qua những người khác tôi thấy câu trả lời này là cách dễ nhất để giải thích về những gì và tại sao các đóng cửa.
Chetabahana

3
Thực đơn quá nhiều và món khai vị, không đủ thịt và khoai tây. Bạn có thể cải thiện câu trả lời đó chỉ bằng một câu ngắn gọn như: "Đóng là bối cảnh kín của một hàm, vì không có bất kỳ cơ chế phạm vi nào được cung cấp bởi các lớp."
Staplerfahrer

584

Người rơm

Tôi cần biết số lần nhấn nút và thực hiện thao tác trên mỗi lần nhấp thứ ba ...

Giải pháp khá rõ ràng

// Declare counter outside event handler's scope
var counter = 0;
var element = document.getElementById('button');

element.addEventListener("click", function() {
  // Increment outside counter
  counter++;

  if (counter === 3) {
    // Do something every third time
    console.log("Third time's the charm!");

    // Reset counter
    counter = 0;
  }
});
<button id="button">Click Me!</button>

Bây giờ điều này sẽ hoạt động, nhưng nó lấn sang phạm vi bên ngoài bằng cách thêm một biến, với mục đích duy nhất là theo dõi số đếm. Trong một số trường hợp, điều này sẽ được ưa thích hơn vì ứng dụng bên ngoài của bạn có thể cần quyền truy cập vào thông tin này. Nhưng trong trường hợp này, chúng tôi chỉ thay đổi hành vi của mỗi lần nhấp thứ ba, vì vậy tốt hơn là nên kèm theo chức năng này bên trong trình xử lý sự kiện .

Xem xét tùy chọn này

var element = document.getElementById('button');

element.addEventListener("click", (function() {
  // init the count to 0
  var count = 0;

  return function(e) { // <- This function becomes the click handler
    count++; //    and will retain access to the above `count`

    if (count === 3) {
      // Do something every third time
      console.log("Third time's the charm!");

      //Reset counter
      count = 0;
    }
  };
})());
<button id="button">Click Me!</button>

Lưu ý một vài điều ở đây.

Trong ví dụ trên, tôi đang sử dụng hành vi đóng của JavaScript. Hành vi này cho phép bất kỳ chức năng nào có quyền truy cập vào phạm vi mà nó được tạo, vô thời hạn. Để thực tế áp dụng điều này, tôi ngay lập tức gọi một hàm trả về một hàm khác và bởi vì hàm tôi đang trả về có quyền truy cập vào biến đếm bên trong (do hành vi đóng được giải thích ở trên) nên kết quả trong phạm vi riêng để sử dụng bởi kết quả Chức năng ... Không đơn giản như vậy? Hãy pha loãng nó xuống ...

Một đóng cửa một dòng đơn giản

//          _______________________Immediately invoked______________________
//         |                                                                |
//         |        Scope retained for use      ___Returned as the____      |
//         |       only by returned function   |    value of func     |     |
//         |             |            |        |                      |     |
//         v             v            v        v                      v     v
var func = (function() { var a = 'val'; return function() { alert(a); }; })();

Tất cả các biến bên ngoài hàm được trả về đều có sẵn cho hàm được trả về, nhưng chúng không có sẵn trực tiếp cho đối tượng hàm được trả về ...

func();  // Alerts "val"
func.a;  // Undefined

Hiểu rồi? Vì vậy, trong ví dụ chính của chúng tôi, biến đếm được chứa trong bao đóng và luôn có sẵn cho trình xử lý sự kiện, vì vậy nó giữ trạng thái của nó từ nhấp để nhấp.

Ngoài ra, trạng thái biến riêng tư này có thể truy cập đầy đủ , cho cả bài đọc và gán cho các biến phạm vi riêng của nó.

Có bạn đi; Bây giờ bạn đã gói gọn hành vi này.

Blog đầy đủ (bao gồm các cân nhắc về jQuery)


11
Tôi không đồng ý với định nghĩa của bạn về việc đóng cửa là gì. Không có lý do gì nó phải tự gọi. Cũng hơi đơn giản (và không chính xác) để nói rằng nó phải được "trả lại" (rất nhiều cuộc thảo luận về điều này trong các bình luận của câu trả lời hàng đầu cho câu hỏi này)
James Montagne

40
@James ngay cả khi bạn không đồng ý, ví dụ của anh ấy (và toàn bộ bài đăng) là một trong những điều tốt nhất tôi từng thấy. Mặc dù câu hỏi không cũ và đã được giải quyết cho tôi, nhưng nó hoàn toàn xứng đáng được +1.
e-satis

84
"Tôi cần biết số lần nhấn nút, và làm gì đó trên mỗi lần nhấp thứ ba ..." NÀY khiến tôi chú ý. Một trường hợp sử dụng và giải pháp cho thấy cách đóng cửa không phải là một điều bí ẩn và rất nhiều người trong chúng ta đã viết chúng nhưng không biết chính xác tên chính thức.
Chris22

Ví dụ hay bởi vì nó cho thấy "đếm" trong ví dụ thứ 2 giữ lại giá trị của "đếm" và không được đặt lại về 0 mỗi lần nhấp vào "phần tử". Rất nhiều thông tin!
Adam

+1 cho hành vi đóng cửa . Chúng ta có thể giới hạn hành vi đóng đối với các chức năng trong javascript hoặc khái niệm này cũng có thể được áp dụng cho các cấu trúc khác của ngôn ngữ không?
Dziamid

492

Đóng cửa rất khó để giải thích bởi vì chúng được sử dụng để làm cho một số hành vi hoạt động mà mọi người trực giác mong đợi để làm việc bằng mọi cách. Tôi tìm thấy cách tốt nhất để giải thích cho họ (và cách mà tôi đã học được những gì họ làm) là tưởng tượng tình huống mà không có họ:

    var bind = function(x) {
        return function(y) { return x + y; };
    }
    
    var plus5 = bind(5);
    console.log(plus5(3));

Điều gì sẽ xảy ra ở đây nếu JavaScript không biết đóng cửa? Chỉ cần thay thế cuộc gọi trong dòng cuối cùng bằng thân phương thức của nó (về cơ bản là những gì mà hàm gọi thực hiện) và bạn nhận được:

console.log(x + 3);

Bây giờ, định nghĩa của xđâu? Chúng tôi đã không xác định nó trong phạm vi hiện tại. Giải pháp duy nhất là để plus5 mang theo phạm vi của nó (hay đúng hơn là phạm vi của cha mẹ). Cách này, xđược xác định rõ và nó bị ràng buộc với giá trị 5.


11
Đây chính xác là loại ví dụ khiến nhiều người lầm tưởng rằng đó là các giá trị được sử dụng trong hàm trả về, chứ không phải chính biến có thể thay đổi. Nếu nó được thay đổi thành "return x + = y", hoặc tốt hơn là cả hàm đó và hàm khác "x * = y", thì rõ ràng là không có gì được sao chép. Đối với những người được sử dụng để xếp chồng khung hình, hãy tưởng tượng sử dụng khung heap thay vào đó, có thể tiếp tục tồn tại sau khi hàm trả về.
Matt

14
@Matt tôi không đồng ý. Một ví dụ không phải là tài liệu đầy đủ tất cả các thuộc tính. Nó có nghĩa là để rút gọn và minh họa tính năng nổi bật của một khái niệm. OP yêu cầu một lời giải thích đơn giản (cho một người già sáu tuổi). Lấy câu trả lời được chấp nhận: Nó hoàn toàn thất bại trong việc đưa ra một lời giải thích ngắn gọn, chính xác bởi vì nó cố gắng hết sức. (Tôi đồng ý với bạn rằng đó là một tài sản quan trọng của JavaScript mà ràng buộc là bằng cách tham khảo chứ không phải theo giá trị ... nhưng một lần nữa, một lời giải thích thành công là một trong đó làm giảm đến mức tối thiểu.)
Konrad Rudolph

@KonradRudolph Tôi thích phong cách và sự ngắn gọn trong ví dụ của bạn. Tôi chỉ đơn giản khuyên bạn nên thay đổi nó một chút để phần cuối cùng, "Giải pháp duy nhất là ...", trở thành sự thật. Hiện tại trên thực tế có một giải pháp khác, đơn giản hơn cho kịch bản của bạn, không tương ứng với các phần tiếp theo của javascript và không tương ứng với một quan niệm sai lầm phổ biến về phần tiếp theo là gì. Do đó, ví dụ ở dạng hiện tại là nguy hiểm. Điều này không phải làm với các thuộc tính liệt kê đầy đủ, nó phải làm với việc hiểu x là gì trong hàm được trả về, đó là sau tất cả các điểm chính.
Matt

@Matt Hmm, tôi không chắc là tôi hoàn toàn hiểu bạn nhưng tôi bắt đầu thấy rằng bạn có thể có một điểm hợp lệ. Vì các bình luận quá ngắn, bạn có thể giải thích ý của bạn trong một ý chính / pastie hoặc trong một phòng chat không? Cảm ơn.
Konrad Rudolph

2
@KonradRudolph Tôi nghĩ rằng tôi đã không rõ ràng về mục đích của x + = y. Mục đích chỉ là để hiển thị rằng các cuộc gọi lặp lại cho hàm được trả về tiếp tục sử dụng cùng một biến x (trái ngược với cùng một giá trị , mà mọi người tưởng tượng là "được chèn" khi hàm được tạo). Điều này giống như hai cảnh báo đầu tiên trong fiddle của bạn. Mục đích của một hàm bổ sung x * = y sẽ chỉ ra rằng nhiều hàm được trả về đều có chung x.
Matt

378

TLD

Một bao đóng là một liên kết giữa một hàm và môi trường từ vựng bên ngoài của nó (ví dụ như được viết), sao cho các định danh (biến, tham số, khai báo hàm, v.v.) được xác định trong môi trường đó, bất kể khi nào hoặc từ trong đó hàm được gọi.

Chi tiết

Trong thuật ngữ của đặc tả ECMAScript, có thể nói việc đóng được thực hiện bằng [[Environment]]tham chiếu của mọi đối tượng hàm, trỏ đến môi trường từ vựng trong đó hàm được xác định.

Khi một hàm được gọi thông qua [[Call]]phương thức bên trong , [[Environment]]tham chiếu trên đối tượng hàm được sao chép vào tham chiếu môi trường bên ngoài của bản ghi môi trường của bối cảnh thực thi mới được tạo (khung stack).

Trong ví dụ sau, hàm fđóng trên môi trường từ vựng của bối cảnh thực thi toàn cục:

function f() {}

Trong ví dụ sau, hàm hđóng trên môi trường từ vựng của hàm g, lần lượt, đóng trên môi trường từ vựng của bối cảnh thực thi toàn cục.

function g() {
    function h() {}
}

Nếu một chức năng bên trong được trả về bởi một bên ngoài, thì môi trường từ vựng bên ngoài sẽ tồn tại sau khi chức năng bên ngoài đã trở lại. Điều này là do môi trường từ vựng bên ngoài cần phải có sẵn nếu chức năng bên trong cuối cùng được gọi.

Trong ví dụ sau, hàm jđóng trên môi trường từ vựng của hàm i, nghĩa là biến đó xcó thể nhìn thấy từ hàm bên trong j, rất lâu sau khi hàm iđã hoàn thành thực thi:

function i() {
    var x = 'mochacchino'
    return function j() {
        console.log('Printing the value of x, from within function j: ', x)
    }
} 

const k = i()
setTimeout(k, 500) // invoke k (which is j) after 500ms

Trong một đóng cửa, các biến trong môi trường từ vựng ngoài mình có sẵn, không bản.

function l() {
  var y = 'vanilla';

  return {
    setY: function(value) {
      y = value;
    },
    logY: function(value) {
      console.log('The value of y is: ', y);
    }
  }
}

const o = l()
o.logY() // The value of y is: vanilla
o.setY('chocolate')
o.logY() // The value of y is: chocolate

Chuỗi các môi trường từ vựng, được liên kết giữa các bối cảnh thực hiện thông qua các tham chiếu môi trường bên ngoài, tạo thành một chuỗi phạm vi và xác định các định danh có thể nhìn thấy từ bất kỳ chức năng nào.

Xin lưu ý rằng trong nỗ lực cải thiện sự rõ ràng và chính xác, câu trả lời này đã được thay đổi đáng kể so với ban đầu.


56
Wow, không bao giờ biết bạn có thể sử dụng thay thế chuỗi console.lognhư vậy. Nếu có ai quan tâm thì có nhiều hơn: developer.mozilla.org/en-US/docs/DOM/iêu
Flash

7
Các biến trong danh sách tham số của hàm cũng là một phần của bao đóng (ví dụ: không chỉ giới hạn ở var).
Thomas Eding 18/03/2015

Đóng cửa nghe có vẻ giống như các đối tượng và các lớp, v.v. Không chắc tại sao nhiều người không so sánh hai thứ này - sẽ dễ dàng hơn cho người mới học!
almaruf

376

OK, fan hâm mộ đóng cửa 6 tuổi. Bạn có muốn nghe ví dụ đơn giản nhất về việc đóng cửa không?

Hãy tưởng tượng tình huống tiếp theo: một người lái xe đang ngồi trong xe. Chiếc xe đó ở trong một chiếc máy bay. Máy bay đang ở sân bay. Khả năng người lái tiếp cận những thứ bên ngoài xe của mình, nhưng bên trong máy bay, ngay cả khi chiếc máy bay đó rời khỏi sân bay, là một sự đóng cửa. Đó là nó. Khi bạn bước sang tuổi 27, hãy xem phần giải thích chi tiết hơn hoặc vào ví dụ dưới đây.

Đây là cách tôi có thể chuyển đổi câu chuyện máy bay của mình thành mã.

var plane = function(defaultAirport) {

  var lastAirportLeft = defaultAirport;

  var car = {
    driver: {
      startAccessPlaneInfo: function() {
        setInterval(function() {
          console.log("Last airport was " + lastAirportLeft);
        }, 2000);
      }
    }
  };
  car.driver.startAccessPlaneInfo();

  return {
    leaveTheAirport: function(airPortName) {
      lastAirportLeft = airPortName;
    }
  }
}("Boryspil International Airport");

plane.leaveTheAirport("John F. Kennedy");


26
Cũng chơi và trả lời các poster ban đầu. Tôi nghĩ rằng đây là câu trả lời tốt nhất. Tôi sẽ sử dụng hành lý theo cách tương tự: hãy tưởng tượng bạn đến nhà bà ngoại và bạn gói chiếc hộp nintendo DS của bạn với thẻ trò chơi bên trong hộp đựng của bạn, nhưng sau đó đóng gói bên trong ba lô của bạn và cũng để thẻ trò chơi vào túi ba lô của bạn, và THÌ bạn để toàn bộ trong một chiếc vali lớn có nhiều thẻ trò chơi hơn trong túi của chiếc vali. Khi bạn đến nhà bà, bạn có thể chơi bất kỳ trò chơi nào trên DS của mình miễn là tất cả các trường hợp bên ngoài được mở. Hoặc một cái gì đó để có hiệu lực.
slartibartfast

365

Đây là một nỗ lực để làm sáng tỏ một số hiểu lầm (có thể) về việc đóng cửa xuất hiện trong một số câu trả lời khác.

  • Một bao đóng không chỉ được tạo khi bạn trả về một hàm bên trong. Trong thực tế, chức năng kèm theo hoàn toàn không cần phải trả về để đóng của nó được tạo. Thay vào đó, bạn có thể gán hàm bên trong của mình cho một biến trong phạm vi bên ngoài hoặc chuyển nó làm đối số cho hàm khác, nơi nó có thể được gọi ngay lập tức hoặc bất kỳ lúc nào sau đó. Do đó, việc đóng hàm bao vây có thể được tạo ngay khi hàm bao quanh được gọi vì bất kỳ hàm bên trong nào có quyền truy cập vào bao đóng đó bất cứ khi nào hàm bên trong được gọi, trước hoặc sau khi hàm bao quay trở lại.
  • Một bao đóng không tham chiếu một bản sao của các giá trị cũ của các biến trong phạm vi của nó. Các biến chính là một phần của bao đóng, và do đó, giá trị nhìn thấy khi truy cập một trong các biến đó là giá trị mới nhất tại thời điểm nó được truy cập. Đây là lý do tại sao các hàm bên trong được tạo bên trong các vòng lặp có thể khó khăn, vì mỗi hàm có quyền truy cập vào cùng một biến bên ngoài thay vì lấy một bản sao của các biến tại thời điểm hàm được tạo hoặc được gọi.
  • Các "biến" trong bao đóng bao gồm bất kỳ hàm được đặt tên nào được khai báo trong hàm. Chúng cũng bao gồm các đối số của hàm. Một bao đóng cũng có quyền truy cập vào các biến của bao đóng, tất cả đều thuộc phạm vi toàn cầu.
  • Đóng cửa sử dụng bộ nhớ, nhưng chúng không gây rò rỉ bộ nhớ vì JavaScript tự dọn sạch các cấu trúc vòng tròn của chính nó không được tham chiếu. Rò rỉ bộ nhớ Internet Explorer liên quan đến việc đóng được tạo khi không ngắt kết nối các giá trị thuộc tính DOM mà đóng tham chiếu, do đó duy trì tham chiếu đến các cấu trúc vòng tròn có thể.

15
James, tôi đã nói rằng việc đóng cửa là "có thể" được tạo ra tại thời điểm cuộc gọi của chức năng kèm theo vì có thể cho rằng việc triển khai có thể trì hoãn việc tạo ra một bao đóng cho đến một lúc nào đó, khi nó quyết định đóng là hoàn toàn cần thiết. Nếu không có chức năng bên trong được xác định trong chức năng kèm theo, thì sẽ không cần đóng. Vì vậy, có lẽ nó có thể đợi cho đến khi hàm bên trong đầu tiên được tạo để sau đó tạo một bao đóng khỏi bối cảnh cuộc gọi của hàm kèm theo.
dlaliberte

9
@ Beetroot-Củ cải đường Giả sử chúng ta có một chức năng bên trong được truyền cho một chức năng khác, nơi nó được sử dụng trước khi chức năng bên ngoài quay trở lại, và giả sử chúng ta cũng trả lại chức năng bên trong tương tự từ chức năng bên ngoài. Đây là chức năng giống hệt nhau trong cả hai trường hợp, nhưng bạn đang nói rằng trước khi hàm bên ngoài trả về, hàm bên trong bị "ràng buộc" với ngăn xếp cuộc gọi, trong khi sau khi trả về, hàm bên trong đột nhiên bị ràng buộc với một bao đóng. Nó hành xử giống hệt nhau trong cả hai trường hợp; ngữ nghĩa là giống hệt nhau, vì vậy bạn không chỉ nói về chi tiết thực hiện?
dlaliberte

7
@ Beetroot-Củ cải đường, cảm ơn phản hồi của bạn, và tôi rất vui vì tôi đã khiến bạn suy nghĩ. Tôi vẫn không thấy bất kỳ sự khác biệt ngữ nghĩa nào giữa bối cảnh trực tiếp của hàm ngoài và bối cảnh tương tự khi nó trở thành một bao đóng khi hàm trả về (nếu tôi hiểu định nghĩa của bạn). Các chức năng bên trong không quan tâm. Bộ sưu tập rác không quan tâm vì chức năng bên trong duy trì một tham chiếu đến bối cảnh / cách đóng, và người gọi của chức năng bên ngoài chỉ bỏ tham chiếu của nó vào ngữ cảnh cuộc gọi. Nhưng thật khó hiểu với mọi người, và có lẽ tốt hơn là chỉ gọi nó là một bối cảnh cuộc gọi.
dlaliberte

9
Bài viết đó rất khó đọc, nhưng tôi nghĩ nó thực sự hỗ trợ những gì tôi đang nói. Nó nói: "Một bao đóng được hình thành bằng cách trả về một đối tượng hàm [...] hoặc bằng cách gán trực tiếp một tham chiếu cho một đối tượng hàm như vậy, ví dụ, một biến toàn cục." Tôi không có nghĩa là GC không liên quan. Thay vào đó, vì GC và bởi vì chức năng bên trong được gắn với bối cảnh cuộc gọi của chức năng bên ngoài (hoặc [[scope]] như bài viết đã nói), nên việc gọi hàm bên ngoài có trả về vì liên kết với bên trong không quan trọng chức năng là điều quan trọng.
dlaliberte

3
Câu trả lời chính xác! Một điều bạn nên thêm là tất cả các hàm đóng trên toàn bộ nội dung của phạm vi thực thi mà chúng được xác định. Không quan trọng họ tham khảo một số hoặc không có biến nào trong phạm vi cha: tham chiếu đến môi trường từ vựng của phạm vi cha được lưu trữ dưới dạng [[Phạm vi]] vô điều kiện. Điều này có thể được nhìn thấy từ phần tạo chức năng trong thông số ECMA.
Asad Saeeduddin

236

Tôi đã viết một bài đăng blog một thời gian trở lại giải thích đóng cửa. Đây là những gì tôi đã nói về việc đóng cửa về lý do tại sao bạn muốn có một.

Đóng cửa là một cách để cho một hàm có các biến riêng, liên tục - nghĩa là các biến chỉ có một hàm biết, nơi nó có thể theo dõi thông tin từ những lần trước nó được chạy.

Theo nghĩa đó, họ để một hàm hoạt động giống như một đối tượng có thuộc tính riêng.

Bài đầy đủ:

Vì vậy, những điều đóng cửa là gì?


Vì vậy, lợi ích chính của việc đóng cửa có thể được nhấn mạnh với ví dụ này? Giả sử tôi có chức năng emailError (sendToAddress, errorString) Sau đó tôi có thể nói devError = emailError("devinrhode2@googmail.com", errorString)và sau đó có phiên bản tùy chỉnh riêng của chức năng emailError được chia sẻ không?
Devin G Rhode

Giải thích này và ví dụ hoàn hảo liên quan trong liên kết đến (các điều đóng cửa) là cách tốt nhất để hiểu các bao đóng và nên ở ngay trên đầu!
HopeKing

215

Đóng cửa rất đơn giản:

Ví dụ đơn giản sau đây bao gồm tất cả các điểm chính của việc đóng JavaScript. *  

Đây là một nhà máy sản xuất máy tính có thể cộng và nhân:

function make_calculator() {
  var n = 0; // this calculator stores a single number n
  return {
    add: function(a) {
      n += a;
      return n;
    },
    multiply: function(a) {
      n *= a;
      return n;
    }
  };
}

first_calculator = make_calculator();
second_calculator = make_calculator();

first_calculator.add(3); // returns 3
second_calculator.add(400); // returns 400

first_calculator.multiply(11); // returns 33
second_calculator.multiply(10); // returns 4000

Điểm mấu chốt: Mỗi lệnh gọi để make_calculatortạo một biến cục bộ mới n, tiếp tục có thể sử dụng được bởi máy tính đó addvà các multiplychức năng sau khi make_calculatortrả về.

Nếu bạn quen thuộc với các khung ngăn xếp, các máy tính này có vẻ lạ: Làm thế nào chúng có thể tiếp tục truy cập nsau khi make_calculatortrả về? Câu trả lời là hãy tưởng tượng rằng JavaScript không sử dụng "stack stack", mà thay vào đó sử dụng "heap frames", có thể tồn tại sau khi gọi hàm khiến chúng trả về.

Các hàm bên trong như addmultiply, các biến truy cập được khai báo trong hàm ngoài ** , được gọi là các bao đóng .

Đó là khá nhiều tất cả có để đóng cửa.



* Ví dụ, nó bao gồm tất cả các điểm trong bài viết "Đóng cửa cho người giả" được đưa ra trong một câu trả lời khác , ngoại trừ ví dụ 6, chỉ đơn giản cho thấy các biến có thể được sử dụng trước khi chúng được khai báo, một thực tế tốt để biết nhưng hoàn toàn không liên quan đến việc đóng. Nó cũng bao gồm tất cả các điểm trong câu trả lời được chấp nhận , ngoại trừ các điểm (1) có chức năng sao chép các đối số của chúng thành các biến cục bộ (đối số hàm được đặt tên) và (2) sao chép số tạo ra một số mới, nhưng sao chép tham chiếu đối tượng cung cấp cho bạn một tài liệu tham khảo khác cho cùng một đối tượng. Đây cũng là những điều tốt để biết nhưng một lần nữa hoàn toàn không liên quan đến việc đóng cửa. Nó cũng rất giống với ví dụ trong câu trả lời này nhưng ngắn hơn một chút và ít trừu tượng hơn. Nó không bao gồm quan điểm củaCâu trả lời này hoặc nhận xét này , đó là JavaScript gây khó khăn cho việc cắm hiện tạigiá trị của biến vòng lặp vào hàm bên trong của bạn: Bước "cắm vào" chỉ có thể được thực hiện với hàm trợ giúp bao quanh hàm bên trong của bạn và được gọi trên mỗi lần lặp vòng. (Nói một cách chính xác, hàm bên trong truy cập vào bản sao biến của hàm trợ giúp, thay vì có bất cứ thứ gì được cắm vào.) Một lần nữa, rất hữu ích khi tạo các bao đóng, nhưng không phải là một phần của việc đóng là gì hoặc cách thức hoạt động của nó. Có thêm sự nhầm lẫn do các bao đóng hoạt động khác nhau trong các ngôn ngữ chức năng như ML, trong đó các biến bị ràng buộc với các giá trị thay vì không gian lưu trữ, cung cấp một dòng người liên tục hiểu các cách đóng theo cách (cụ thể là cách "cắm vào") chỉ đơn giản là không chính xác cho JavaScript, trong đó các biến luôn bị ràng buộc vào không gian lưu trữ và không bao giờ có giá trị.

** Bất kỳ chức năng bên ngoài nào, nếu một số được lồng nhau, hoặc thậm chí trong bối cảnh toàn cầu, vì câu trả lời này chỉ ra rõ ràng.


Điều gì sẽ xảy ra nếu bạn gọi: second_calculator = first_calculator (); thay vì second_calculator = make_calculator (); ? Nên giống nhau, phải không?
Ronen Festinger

4
@Ronen: Vì first_calculatorlà một đối tượng (không phải là hàm) nên bạn không nên sử dụng dấu ngoặc đơn second_calculator = first_calculator;, vì đó là một phép gán, không phải là hàm gọi. Để trả lời câu hỏi của bạn, sau đó sẽ chỉ có một cuộc gọi đến make_calculator, do đó, chỉ có một máy tính được thực hiện và các biến First_calculator và second_calculator đều tham chiếu đến cùng một máy tính, vì vậy các câu trả lời sẽ là 3, 403, 4433, 44330.
Matt

204

Làm thế nào tôi giải thích nó với một đứa trẻ sáu tuổi:

Bạn có biết làm thế nào những người trưởng thành có thể sở hữu một ngôi nhà, và họ gọi nó là nhà? Khi mẹ có con, đứa trẻ không thực sự sở hữu bất cứ thứ gì, phải không? Nhưng cha mẹ của nó sở hữu một ngôi nhà, vì vậy bất cứ khi nào ai đó hỏi đứa trẻ "Nhà của bạn ở đâu?", Anh ấy / cô ấy có thể trả lời "ngôi nhà đó!", Và chỉ vào nhà của cha mẹ nó. "Đóng cửa" là khả năng của đứa trẻ luôn luôn (ngay cả khi ở nước ngoài) có thể nói nó có một ngôi nhà, mặc dù đó thực sự là cha mẹ sở hữu ngôi nhà.


200

Bạn có thể giải thích về việc đóng cửa cho một đứa trẻ 5 tuổi không? *

Tôi vẫn nghĩ rằng lời giải thích của Google hoạt động rất tốt và ngắn gọn:

/*
*    When a function is defined in another function and it
*    has access to the outer function's context even after
*    the outer function returns.
*
* An important concept to learn in JavaScript.
*/

function outerFunction(someNum) {
    var someString = 'Hey!';
    var content = document.getElementById('content');
    function innerFunction() {
        content.innerHTML = someNum + ': ' + someString;
        content = null; // Internet Explorer memory leak for DOM reference
    }
    innerFunction();
}

outerFunction(1);​

Bằng chứng là ví dụ này tạo ra một bao đóng ngay cả khi hàm bên trong không trả về

* Câu hỏi AC #


11
Mã này là "chính xác", như một ví dụ về việc đóng cửa, mặc dù nó không giải quyết được phần nào của nhận xét về việc sử dụng bao đóng sau khi hàm ngoài trả về. Vì vậy, nó không phải là một ví dụ tuyệt vời. Có nhiều cách khác để đóng cửa có thể được sử dụng mà không liên quan đến việc trả lại Hàm bên trong. ví dụ: InternalFunction có thể được chuyển đến một chức năng khác, nơi nó được gọi ngay lập tức hoặc được lưu trữ và được gọi một thời gian sau đó, và trong mọi trường hợp, nó có quyền truy cập vào bối cảnh bên ngoài được tạo khi nó được gọi.
dlaliberte

6
@syockit Không, Rêu là sai. Một bao đóng được tạo bất kể hàm có thoát khỏi phạm vi được xác định hay không và tham chiếu được tạo vô điều kiện đến môi trường từ vựng của cha mẹ làm cho tất cả các biến trong phạm vi cha có sẵn cho tất cả các hàm, bất kể chúng được gọi bên ngoài hay bên trong phạm vi mà chúng được tạo ra.
Asad Saeeduddin

176

Tôi có xu hướng học tốt hơn bằng cách so sánh TỐT / BAD. Tôi muốn thấy mã làm việc theo sau là mã không hoạt động mà ai đó có khả năng gặp phải. Tôi kết hợp một jsFiddle để so sánh và cố gắng tìm ra những điểm khác biệt để giải thích đơn giản nhất mà tôi có thể đưa ra.

Đóng cửa đúng:

console.log('CLOSURES DONE RIGHT');

var arr = [];

function createClosure(n) {
    return function () {
        return 'n = ' + n;
    }
}

for (var index = 0; index < 10; index++) {
    arr[index] = createClosure(index);
}

for (var index in arr) {
    console.log(arr[index]());
}
  • Trong đoạn mã trên createClosure(n)được gọi trong mỗi lần lặp của vòng lặp. Lưu ý rằng tôi đã đặt tên biến nđể làm nổi bật rằng đó là một biến mới được tạo trong phạm vi hàm mới và không phải là biến giống như indexđược ràng buộc với phạm vi bên ngoài.

  • Điều này tạo ra một phạm vi mới và nbị ràng buộc với phạm vi đó; điều này có nghĩa là chúng ta có 10 phạm vi riêng biệt, một phạm vi cho mỗi lần lặp.

  • createClosure(n) trả về một hàm trả về n trong phạm vi đó.

  • Trong mỗi phạm vi nđược ràng buộc với bất kỳ giá trị nào mà nó có khi createClosure(n)được gọi, do đó, hàm lồng nhau được trả về sẽ luôn trả về giá trị của nnó khi createClosure(n)được gọi.

Đóng cửa sai:

console.log('CLOSURES DONE WRONG');

function createClosureArray() {
    var badArr = [];

    for (var index = 0; index < 10; index++) {
        badArr[index] = function () {
            return 'n = ' + index;
        };
    }
    return badArr;
}

var badArr = createClosureArray();

for (var index in badArr) {
    console.log(badArr[index]());
}
  • Trong đoạn mã trên, vòng lặp đã được di chuyển trong createClosureArray()hàm và hàm bây giờ chỉ trả về mảng đã hoàn thành, thoạt nhìn có vẻ trực quan hơn.

  • Điều có thể không rõ ràng là vì createClosureArray()chỉ được gọi một lần duy nhất một phạm vi được tạo cho hàm này thay vì một phạm vi cho mỗi lần lặp của vòng lặp.

  • Trong hàm này, một biến có tên indexđược định nghĩa. Vòng lặp chạy và thêm các chức năng cho mảng trả về index. Lưu ý rằng indexđược định nghĩa trong createClosureArrayhàm chỉ được gọi một lần.

  • Bởi vì chỉ có một phạm vi trong createClosureArray()hàm, indexchỉ bị ràng buộc với một giá trị trong phạm vi đó. Nói cách khác, mỗi lần vòng lặp thay đổi giá trị index, nó sẽ thay đổi nó cho mọi thứ tham chiếu đến nó trong phạm vi đó.

  • Tất cả các hàm được thêm vào mảng trả về indexbiến SAME từ phạm vi cha, nơi nó được xác định thay vì 10 hàm khác nhau từ 10 phạm vi khác nhau như ví dụ đầu tiên. Kết quả cuối cùng là tất cả 10 hàm trả về cùng một biến từ cùng một phạm vi.

  • Sau khi vòng lặp kết thúc và indexđược hoàn thành, giá trị cuối là 10, do đó, mọi hàm được thêm vào mảng sẽ trả về giá trị của indexbiến đơn hiện được đặt thành 10.

Kết quả

ĐÓNG GÓP ĐÚNG QUYỀN
n = 0
n = 1
n = 2
n = 3
n = 4
n = 5
n = 6
n = 7
n = 8
n = 9

ĐÓNG GÓP SAU
N = 10
n = 10
n = 10
n = 10
n = 10
n = 10
n = 10
n = 10
n = 10
n = 10


1
Bổ sung tốt đẹp, cảm ơn. Chỉ cần làm cho nó rõ ràng hơn, người ta có thể tưởng tượng mảng "xấu" được tạo ra như thế nào trong vòng lặp "xấu" với mỗi lần lặp: Lần lặp thứ 1: [function () {return 'n =' + 0;}] Lặp lại lần 2: [( function () {return 'n =' + 1;}), (function () {return 'n =' + 1;})] Lặp lại lần thứ 3: [(function () {return 'n =' + 2;}) , (function () {return 'n =' + 2;}), (function () {return 'n =' + 2;})] v.v ... Vì vậy, mỗi khi giá trị chỉ số thay đổi, nó được phản ánh trong tất cả các hàm đã được thêm vào mảng.
Alex Alexeev

3
Sử dụng letđể varsửa chữa sự khác biệt.
Rupam Datta

Không phải ở đây "Đóng cửa được thực hiện đúng" là một ví dụ về "đóng cửa bên trong đóng cửa"?
TechnicalSmile

Ý tôi là, mọi chức năng về mặt kỹ thuật là một bao đóng nhưng phần quan trọng là hàm xác định một biến mới bên trong. Hàm nhận được trả về chỉ các tham chiếu nđược tạo trong một bao đóng mới. Chúng ta chỉ cần trả về một hàm để chúng ta có thể lưu nó trong mảng và gọi nó sau.
Chev

Nếu bạn muốn lưu trữ kết quả trong mảng trong lần lặp đầu tiên thì bạn có thể nội tuyến nó như thế này : arr[index] = (function (n) { return 'n = ' + n; })(index);. Nhưng sau đó, bạn đang lưu trữ chuỗi kết quả trong mảng chứ không phải là một hàm để gọi điểm đánh bại ví dụ của tôi.
Chev

164

Wikipedia về việc đóng cửa :

Trong khoa học máy tính, bao đóng là một hàm cùng với môi trường tham chiếu cho các tên không tiêu điểm (biến tự do) của hàm đó.

Về mặt kỹ thuật, trong JavaScript , mọi chức năng là một bao đóng . Nó luôn có quyền truy cập vào các biến được xác định trong phạm vi xung quanh.

Do cấu trúc xác định phạm vi trong JavaScript là một hàm , không phải là khối mã như trong nhiều ngôn ngữ khác, điều chúng ta thường hiểu khi đóng trong JavaScript là một hàm làm việc với các biến không nhắm mục tiêu được xác định trong hàm xung quanh đã được thực thi .

Các bao đóng thường được sử dụng để tạo các hàm với một số dữ liệu riêng tư bị ẩn (nhưng không phải lúc nào cũng như vậy).

var db = (function() {
    // Create a hidden object, which will hold the data
    // it's inaccessible from the outside.
    var data = {};

    // Make a function, which will provide some access to the data.
    return function(key, val) {
        if (val === undefined) { return data[key] } // Get
        else { return data[key] = val } // Set
    }
    // We are calling the anonymous surrounding function,
    // returning the above inner function, which is a closure.
})();

db('x')    // -> undefined
db('x', 1) // Set x to 1
db('x')    // -> 1
// It's impossible to access the data object itself.
// We are able to get or set individual it.

trẻ em

Ví dụ trên đang sử dụng một hàm ẩn danh, được thực thi một lần. Nhưng nó không phải như vậy. Nó có thể được đặt tên (ví dụ mkdb) và được thực hiện sau đó, tạo ra một hàm cơ sở dữ liệu mỗi khi nó được gọi. Mỗi hàm được tạo sẽ có đối tượng cơ sở dữ liệu ẩn riêng. Một ví dụ sử dụng khác về việc đóng cửa là khi chúng ta không trả về một hàm, nhưng một đối tượng chứa nhiều hàm cho các mục đích khác nhau, mỗi hàm đó có quyền truy cập vào cùng một dữ liệu.


2
Đây là lời giải thích tốt nhất cho việc đóng JavaScript. Nên là câu trả lời được chọn. Phần còn lại đủ để giải trí nhưng cái này thực sự hữu ích theo cách thực tế cho các lập trình viên JavaScript trong thế giới thực.
địa lý

136

Tôi tập hợp một hướng dẫn JavaScript tương tác để giải thích cách đóng cửa hoạt động. Đóng cửa là gì?

Đây là một trong những ví dụ:

var create = function (x) {
    var f = function () {
        return x; // We can refer to x here!
    };
    return f;
};
// 'create' takes one argument, creates a function

var g = create(42);
// g is a function that takes no arguments now

var y = g();
// y is 42 here

128

Những đứa trẻ sẽ luôn nhớ những bí mật mà chúng đã chia sẻ với cha mẹ, ngay cả sau khi cha mẹ chúng không còn nữa. Đây là những gì đóng cửa cho các chức năng.

Các bí mật cho các hàm JavaScript là các biến riêng tư

var parent = function() {
 var name = "Mary"; // secret
}

Mỗi khi bạn gọi nó, "tên" biến cục bộ được tạo và đặt tên "Mary". Và mỗi khi hàm thoát biến bị mất và tên bị quên.

Như bạn có thể đoán, bởi vì các biến được tạo lại mỗi khi hàm được gọi và không ai khác sẽ biết chúng, phải có một nơi bí mật nơi chúng được lưu trữ. Nó có thể được gọi là Phòng chứa bí mật hoặc ngăn xếp hoặc phạm vi địa phương nhưng nó không thực sự quan trọng. Chúng tôi biết họ ở đó, ở đâu đó, ẩn trong ký ức.

Nhưng, trong JavaScript có một điều rất đặc biệt là các hàm được tạo bên trong các hàm khác, cũng có thể biết các biến cục bộ của cha mẹ chúng và giữ chúng miễn là chúng còn sống.

var parent = function() {
  var name = "Mary";
  var child = function(childName) {
    // I can also see that "name" is "Mary"
  }
}

Vì vậy, miễn là chúng ta ở trong chức năng cha mẹ, nó có thể tạo một hoặc nhiều hàm con có chung các biến bí mật từ vị trí bí mật.

Nhưng điều đáng buồn là, nếu đứa trẻ cũng là một biến riêng của chức năng cha mẹ, nó cũng sẽ chết khi cha mẹ kết thúc, và những bí mật sẽ chết theo chúng.

Vì vậy, để sống, đứa trẻ phải rời đi trước khi quá muộn

var parent = function() {
  var name = "Mary";
  var child = function(childName) {
    return "My name is " + childName  +", child of " + name; 
  }
  return child; // child leaves the parent ->
}
var child = parent(); // < - and here it is outside 

Và bây giờ, mặc dù Mary "không còn chạy", ký ức về cô không bị mất và đứa con của cô sẽ luôn nhớ tên cô và những bí mật khác mà họ đã chia sẻ trong suốt thời gian bên nhau.

Vì vậy, nếu bạn gọi đứa trẻ là "Alice", cô ấy sẽ trả lời

child("Alice") => "My name is Alice, child of Mary"

Đó là tất cả những gì cần nói.


15
Đây là lời giải thích có ý nghĩa nhất đối với tôi bởi vì nó không thừa nhận kiến ​​thức quan trọng trước đây về các thuật ngữ kỹ thuật. Giải thích được bình chọn hàng đầu ở đây giả định rằng người không hiểu về sự đóng cửa có sự hiểu biết đầy đủ và đầy đủ về các thuật ngữ như 'phạm vi từ vựng' và 'bối cảnh thực thi' - trong khi tôi có thể hiểu những khái niệm này, tôi không nghĩ tôi là thoải mái với các chi tiết của chúng như tôi nên, và lời giải thích không có biệt ngữ nào trong đó là những gì đóng cửa cuối cùng nhấp cho tôi, cảm ơn bạn. Là một phần thưởng, tôi nghĩ nó cũng giải thích phạm vi rất chính xác.
Emma W

103

Tôi không hiểu tại sao câu trả lời lại phức tạp ở đây.

Đây là một đóng cửa:

var a = 42;

function b() { return a; }

Đúng. Bạn có thể sử dụng nhiều lần một ngày.


Không có lý do để tin rằng đóng cửa là một hack thiết kế phức tạp để giải quyết các vấn đề cụ thể. Không, đóng cửa chỉ là về việc sử dụng một biến xuất phát từ phạm vi cao hơn từ góc độ nơi hàm được khai báo (không chạy) .

Bây giờ những gì nó cho phép bạn làm có thể ngoạn mục hơn, xem câu trả lời khác.


5
Câu trả lời này dường như không có khả năng giúp những người không tự tin. Một tương đương thô trong ngôn ngữ lập trình truyền thống có thể là tạo b () như một phương thức trên một đối tượng cũng có hằng số riêng hoặc thuộc tính a. Theo tôi, điều ngạc nhiên là đối tượng phạm vi JS cung cấp amột cách hiệu quả như một thuộc tính chứ không phải là hằng số. Và bạn sẽ chỉ nhận thấy hành vi quan trọng đó nếu bạn sửa đổi nó, như trongreturn a++;
Jon Coombs

1
Chính xác những gì Jon nói. Trước khi cuối cùng tôi đóng cửa, tôi đã có một khoảng thời gian khó khăn để tìm ra những ví dụ thực tế. Vâng, floribon đã tạo ra một sự đóng cửa, nhưng với tôi vô học, điều này sẽ không dạy tôi điều gì hoàn toàn.
Chev

3
Điều này không định nghĩa đóng cửa là gì - nó chỉ là một ví dụ sử dụng một bao đóng. Và nó không giải quyết được sắc thái của những gì xảy ra khi phạm vi kết thúc; Tôi không nghĩ có ai có câu hỏi về phạm vi từ vựng khi tất cả các phạm vi vẫn còn, và đặc biệt là trong trường hợp biến toàn cục.
Gerard ONeill

91

Ví dụ cho điểm đầu tiên bởi dlaliberte:

Một bao đóng không chỉ được tạo khi bạn trả về một hàm bên trong. Trong thực tế, chức năng kèm theo hoàn toàn không cần trả về. Thay vào đó, bạn có thể gán hàm bên trong của mình cho một biến trong phạm vi bên ngoài hoặc chuyển nó làm đối số cho hàm khác, nơi nó có thể được sử dụng ngay lập tức. Do đó, việc đóng hàm bao quanh có lẽ đã tồn tại tại thời điểm hàm bao quanh được gọi vì bất kỳ hàm bên trong nào cũng có quyền truy cập vào nó ngay khi được gọi.

var i;
function foo(x) {
    var tmp = 3;
    i = function (y) {
        console.log(x + y + (++tmp));
    }
}
foo(2);
i(3);

Làm rõ nhỏ về một sự mơ hồ có thể. Khi tôi nói "Trên thực tế, chức năng kèm theo hoàn toàn không cần trả về." Tôi không có nghĩa là "trả lại không có giá trị" nhưng "vẫn hoạt động". Vì vậy, ví dụ không chỉ ra khía cạnh đó, mặc dù nó cho thấy một cách khác mà chức năng bên trong có thể được chuyển sang phạm vi bên ngoài. Điểm chính mà tôi đang cố gắng thực hiện là về thời điểm tạo ra bao đóng (đối với chức năng kèm theo), vì một số người dường như nghĩ rằng nó xảy ra khi chức năng kèm theo trở lại. Một ví dụ khác được yêu cầu để chỉ ra rằng bao đóng được tạo khi hàm được gọi .
dlaliberte

88

Một bao đóng là nơi một hàm bên trong có quyền truy cập vào các biến trong hàm ngoài của nó. Đó có lẽ là lời giải thích một dòng đơn giản nhất mà bạn có thể nhận được cho việc đóng cửa.


35
Đó chỉ là một nửa lời giải thích. Điều quan trọng cần lưu ý về việc đóng cửa là nếu chức năng bên trong vẫn được nhắc đến sau khi chức năng bên ngoài đã thoát, các giá trị cũ của chức năng bên ngoài vẫn có sẵn cho chức năng bên trong.
pcorcoran

22
Trên thực tế, nó không phải là các giá trị cũ của hàm ngoài có sẵn cho hàm bên trong, mà là các biến cũ , có thể có các giá trị mới nếu một số hàm có thể thay đổi chúng.
dlaliberte

86

Tôi biết đã có rất nhiều giải pháp, nhưng tôi đoán rằng kịch bản nhỏ và đơn giản này có thể hữu ích để thể hiện khái niệm:

// makeSequencer will return a "sequencer" function
var makeSequencer = function() {
    var _count = 0; // not accessible outside this function
    var sequencer = function () {
        return _count++;
    }
    return sequencer;
}

var fnext = makeSequencer();
var v0 = fnext();     // v0 = 0;
var v1 = fnext();     // v1 = 1;
var vz = fnext._count // vz = undefined

82

Bạn đang ngủ và bạn mời Dan. Bạn bảo Dan mang theo một bộ điều khiển XBox.

Dan mời Paul. Dan yêu cầu Paul mang theo một bộ điều khiển. Có bao nhiêu kiểm soát viên đã được đưa đến bữa tiệc?

function sleepOver(howManyControllersToBring) {

    var numberOfDansControllers = howManyControllersToBring;

    return function danInvitedPaul(numberOfPaulsControllers) {
        var totalControllers = numberOfDansControllers + numberOfPaulsControllers;
        return totalControllers;
    }
}

var howManyControllersToBring = 1;

var inviteDan = sleepOver(howManyControllersToBring);

// The only reason Paul was invited is because Dan was invited. 
// So we set Paul's invitation = Dan's invitation.

var danInvitedPaul = inviteDan(howManyControllersToBring);

alert("There were " + danInvitedPaul + " controllers brought to the party.");

80

Tác giả của Closures đã giải thích về việc đóng cửa khá tốt, giải thích lý do tại sao chúng ta cần chúng và cũng giải thích về LexicalEn Môi trường cần thiết để hiểu về việc đóng cửa.
Dưới đây là tóm tắt:

Điều gì nếu một biến được truy cập, nhưng nó không phải là cục bộ? Giống như ở đây:

Nhập mô tả hình ảnh ở đây

Trong trường hợp này, trình thông dịch tìm thấy biến trong LexicalEnvironmentđối tượng bên ngoài .

Quá trình này bao gồm hai bước:

  1. Đầu tiên, khi một hàm f được tạo, nó không được tạo trong một khoảng trống. Có một đối tượng LexicalEn Môi trường hiện tại. Trong trường hợp trên, cửa sổ của nó (a không được xác định tại thời điểm tạo chức năng).

Nhập mô tả hình ảnh ở đây

Khi một chức năng được tạo, nó nhận được một thuộc tính ẩn, được đặt tên [[Phạm vi]], tham chiếu đến LexicalEn Môi trường hiện tại.

Nhập mô tả hình ảnh ở đây

Nếu một biến được đọc, nhưng không thể tìm thấy ở bất cứ đâu, một lỗi sẽ được tạo ra.

Hàm lồng nhau

Các hàm có thể được lồng vào nhau, tạo thành một chuỗi các môi trường Lexical cũng có thể được gọi là chuỗi phạm vi.

Nhập mô tả hình ảnh ở đây

Vì vậy, hàm g có quyền truy cập vào g, a và f.

Đóng cửa

Hàm lồng nhau có thể tiếp tục sống sau khi hàm ngoài kết thúc:

Nhập mô tả hình ảnh ở đây

Đánh dấu lên môi trường học thuật:

Nhập mô tả hình ảnh ở đây

Như chúng ta thấy, this.saylà một thuộc tính trong đối tượng người dùng, vì vậy nó tiếp tục tồn tại sau khi Người dùng hoàn thành.

Và nếu bạn còn nhớ, khi this.sayđược tạo, nó (như mọi hàm) sẽ có một tham chiếu nội bộ this.say.[[Scope]]đến Môi trường hiện tại. Vì vậy, môi trường từ điển của việc thực thi Người dùng hiện tại vẫn còn trong bộ nhớ. Tất cả các biến của Người dùng cũng là thuộc tính của nó, vì vậy chúng cũng được giữ cẩn thận, không bị bỏ đi như thường lệ.

Toàn bộ vấn đề là đảm bảo rằng nếu chức năng bên trong muốn truy cập vào một biến ngoài trong tương lai, thì nó có thể làm như vậy.

Để tóm tắt:

  1. Hàm bên trong giữ một tham chiếu đến môi trường Lexical bên ngoài.
  2. Hàm bên trong có thể truy cập các biến từ nó bất cứ lúc nào ngay cả khi chức năng bên ngoài kết thúc.
  3. Trình duyệt giữ môi trường LexicalEn và tất cả các thuộc tính (biến) của nó trong bộ nhớ cho đến khi có một chức năng bên trong tham chiếu đến nó.

Điều này được gọi là đóng cửa.


78

Các hàm JavaScript có thể truy cập:

  1. Tranh luận
  2. Người dân địa phương (nghĩa là các biến cục bộ và các hàm cục bộ của họ)
  3. Môi trường, bao gồm:
    • toàn cầu, bao gồm DOM
    • bất cứ điều gì trong các chức năng bên ngoài

Nếu một hàm truy cập vào môi trường của nó, thì hàm đó là một bao đóng.

Lưu ý rằng các chức năng bên ngoài là không bắt buộc, mặc dù chúng mang lại lợi ích mà tôi không thảo luận ở đây. Bằng cách truy cập dữ liệu trong môi trường của nó, việc đóng sẽ giữ cho dữ liệu đó tồn tại. Trong trường hợp con của các chức năng bên ngoài / bên trong, một chức năng bên ngoài có thể tạo dữ liệu cục bộ và cuối cùng thoát ra, tuy nhiên, nếu bất kỳ (các) chức năng bên trong nào tồn tại sau khi chức năng bên ngoài thoát ra, thì (các) chức năng bên trong sẽ giữ dữ liệu cục bộ của chức năng bên ngoài sống sót.

Ví dụ về việc đóng cửa sử dụng môi trường toàn cầu:

Hãy tưởng tượng rằng các sự kiện nút Bỏ phiếu và Bỏ phiếu Xếp chồng tràn được thực hiện dưới dạng các bao đóng, voteUp_click và voteDown_click, có quyền truy cập vào các biến bên ngoài làVoteUp và isVoteDown, được xác định trên toàn cầu. (Để đơn giản, tôi đang đề cập đến các nút Câu hỏi Bầu chọn của StackOverflow, không phải là các nút Trả lời Bình chọn.)

Khi người dùng nhấp vào nút VoteUp, chức năng voteUp_click sẽ kiểm tra xem isVoteDown == true để xác định xem có nên bỏ phiếu hay chỉ đơn giản là hủy bỏ phiếu bầu. Hàm voteUp_click là một bao đóng vì nó đang truy cập vào môi trường của nó.

var isVotedUp = false;
var isVotedDown = false;

function voteUp_click() {
  if (isVotedUp)
    return;
  else if (isVotedDown)
    SetDownVote(false);
  else
    SetUpVote(true);
}

function voteDown_click() {
  if (isVotedDown)
    return;
  else if (isVotedUp)
    SetUpVote(false);
  else
    SetDownVote(true);
}

function SetUpVote(status) {
  isVotedUp = status;
  // Do some CSS stuff to Vote-Up button
}

function SetDownVote(status) {
  isVotedDown = status;
  // Do some CSS stuff to Vote-Down button
}

Tất cả bốn chức năng này đều đóng cửa vì tất cả đều truy cập vào môi trường của chúng.


59

Là cha của một đứa trẻ 6 tuổi, hiện đang dạy trẻ nhỏ (và một người mới làm quen với việc viết mã mà không có giáo dục chính thức nên sẽ phải sửa), tôi nghĩ bài học sẽ tốt nhất khi chơi bằng tay. Nếu đứa trẻ 6 tuổi sẵn sàng hiểu thế nào là đóng cửa, thì chúng đủ lớn để tự đi. Tôi khuyên bạn nên dán mã vào jsfiddle.net, giải thích một chút và để chúng một mình để tạo ra một bài hát độc đáo. Các văn bản giải thích dưới đây có lẽ thích hợp hơn cho một đứa trẻ 10 tuổi.

function sing(person) {

    var firstPart = "There was " + person + " who swallowed ";

    var fly = function() {
        var creature = "a fly";
        var result = "Perhaps she'll die";
        alert(firstPart + creature + "\n" + result);
    };

    var spider = function() {
        var creature = "a spider";
        var result = "that wiggled and jiggled and tickled inside her";
        alert(firstPart + creature + "\n" + result);
    };

    var bird = function() {
        var creature = "a bird";
        var result = "How absurd!";
        alert(firstPart + creature + "\n" + result);
    };

    var cat = function() {
        var creature = "a cat";
        var result = "Imagine That!";
        alert(firstPart + creature + "\n" + result);
    };

    fly();
    spider();
    bird();
    cat();
}

var person="an old lady";

sing(person);

HƯỚNG DẪN

DATA: Dữ liệu là một tập hợp các sự kiện. Nó có thể là số, từ, đo lường, quan sát hoặc thậm chí chỉ là mô tả của sự vật. Bạn không thể chạm vào nó, ngửi hoặc nếm nó. Bạn có thể viết nó ra, nói và nghe nó. Bạn có thể sử dụng nó để tạo mùi và vị giác bằng máy tính. Nó có thể được làm cho hữu ích bởi một máy tính sử dụng mã.

MÃ: Tất cả các văn bản trên được gọi là . Nó được viết bằng JavaScript.

JAVASCRIPT: JavaScript là một ngôn ngữ. Giống như tiếng Anh hoặc tiếng Pháp hoặc tiếng Trung Quốc là ngôn ngữ. Có rất nhiều ngôn ngữ được hiểu bởi máy tính và các bộ xử lý điện tử khác. Để máy tính hiểu được JavaScript, nó cần một trình thông dịch. Hãy tưởng tượng nếu một giáo viên chỉ nói tiếng Nga đến dạy lớp của bạn ở trường. Khi giáo viên nói "bình luận", cả lớp sẽ không hiểu. Nhưng may mắn thay, bạn có một học sinh người Nga trong lớp, người nói với mọi người điều này có nghĩa là "mọi người ngồi xuống" - vì vậy tất cả các bạn đều làm như vậy. Lớp học giống như một chiếc máy tính và học sinh người Nga là phiên dịch viên. Đối với JavaScript, trình thông dịch phổ biến nhất được gọi là trình duyệt.

BROWSER: Khi bạn kết nối Internet trên máy tính, máy tính bảng hoặc điện thoại để truy cập trang web, bạn sử dụng trình duyệt. Ví dụ bạn có thể biết là Internet Explorer, Chrome, Firefox và Safari. Trình duyệt có thể hiểu JavaScript và cho máy tính biết những gì nó cần làm. Các hướng dẫn JavaScript được gọi là các hàm.

CHỨC NĂNG: Một chức năng trong JavaScript giống như một nhà máy. Nó có thể là một nhà máy nhỏ chỉ có một máy bên trong. Hoặc nó có thể chứa nhiều nhà máy nhỏ khác, mỗi nhà máy có nhiều máy móc làm các công việc khác nhau. Trong một nhà máy sản xuất quần áo ngoài đời thực, bạn có thể có những mảnh vải và những sợi chỉ đi vào và áo phông và quần jean xuất hiện. Nhà máy JavaScript của chúng tôi chỉ xử lý dữ liệu, nó không thể may, khoan lỗ hoặc nung chảy kim loại. Trong dữ liệu nhà máy JavaScript của chúng tôi đi vào và dữ liệu đi ra.

Tất cả những thứ dữ liệu này nghe có vẻ hơi nhàm chán, nhưng nó thực sự rất tuyệt; chúng ta có thể có một chức năng nói cho robot biết phải làm gì cho bữa tối. Hãy nói rằng tôi mời bạn và bạn của bạn đến nhà tôi. Bạn thích chân gà nhất, tôi thích xúc xích, bạn của bạn luôn muốn những gì bạn muốn và bạn tôi không ăn thịt.

Tôi không có thời gian để đi mua sắm, vì vậy chức năng cần phải biết những gì chúng ta có trong tủ lạnh để đưa ra quyết định. Mỗi thành phần có thời gian nấu khác nhau và chúng tôi muốn mọi thứ được phục vụ nóng bởi robot cùng một lúc. Chúng ta cần cung cấp cho chức năng dữ liệu về những gì chúng ta thích, chức năng có thể 'nói chuyện' với tủ lạnh và chức năng có thể điều khiển robot.

Một hàm thường có tên, dấu ngoặc đơn và dấu ngoặc nhọn. Như thế này:

function cookMeal() {  /*  STUFF INSIDE THE FUNCTION  */  }

Lưu ý rằng /*...*///dừng mã đang được trình duyệt đọc.

NAME: Bạn có thể gọi một chức năng về bất cứ từ nào bạn muốn. Ví dụ "cookMeal" là điển hình trong việc nối hai từ lại với nhau và đặt từ thứ hai một chữ in hoa ngay từ đầu - nhưng điều này là không cần thiết. Nó không thể có một khoảng trống trong đó và nó không thể là một số riêng.

PHỤ HUYNH: "Dấu ngoặc đơn" hoặc ()là hộp thư trên cửa của nhà máy chức năng JavaScript hoặc hộp thư trên đường để gửi các gói thông tin đến nhà máy. Đôi khi, hộp thư có thể được đánh dấu chẳng hạn cookMeal(you, me, yourFriend, myFriend, fridge, dinnerTime) , trong trường hợp đó bạn biết dữ liệu nào bạn phải cung cấp.

BRACES: "Niềng răng" trông giống như đây {}là các cửa sổ được tô màu của nhà máy của chúng tôi. Từ bên trong nhà máy bạn có thể nhìn ra, nhưng từ bên ngoài bạn không thể nhìn thấy.

VÍ DỤ DÀI HẠN

Mã của chúng tôi bắt đầu bằng chức năng từ , vì vậy chúng tôi biết rằng đó là một! Sau đó, tên của hàm hát - đó là mô tả của riêng tôi về nội dung của hàm. Sau đó dấu ngoặc đơn () . Các dấu ngoặc luôn ở đó cho một hàm. Đôi khi chúng trống rỗng, và đôi khi chúng có một cái gì đó. Cái này có một từ trong : (person). Sau này có một cái nẹp như thế này {. Điều này đánh dấu sự bắt đầu của hàm sing () . Nó có một đối tác đánh dấu sự kết thúc của sing () như thế này}

function sing(person) {  /* STUFF INSIDE THE FUNCTION */  }

Vì vậy, chức năng này có thể có liên quan đến ca hát và có thể cần một số dữ liệu về một người. Nó có hướng dẫn bên trong để làm một cái gì đó với dữ liệu đó.

Bây giờ, sau hàm sing () , gần cuối mã là dòng

var person="an old lady";

BIỂU TƯỢNG: Các chữ cái var là viết tắt của "biến". Một biến giống như một phong bì. Ở bên ngoài phong bì này được đánh dấu "người". Ở bên trong nó chứa một tờ giấy với thông tin mà chức năng của chúng ta cần, một số chữ cái và khoảng trắng được nối với nhau như một chuỗi (nó được gọi là một chuỗi) tạo thành một cụm từ đọc "một bà già". Phong bì của chúng tôi có thể chứa các loại khác như số (được gọi là số nguyên), hướng dẫn (được gọi là hàm), danh sách (được gọi là mảng ). Bởi vì biến này được viết bên ngoài tất cả các dấu ngoặc nhọn {}và bởi vì bạn có thể nhìn ra ngoài qua các cửa sổ được tô màu khi bạn ở trong dấu ngoặc nhọn, biến này có thể được nhìn thấy từ bất kỳ đâu trong mã. Chúng tôi gọi đây là "biến toàn cầu".

BIỂU TƯỢNG TOÀN CẦU: người là một biến toàn cầu, nghĩa là nếu bạn thay đổi giá trị của nó từ "một bà già" thành "một người đàn ông trẻ", người đó sẽ tiếp tục là một chàng trai trẻ cho đến khi bạn quyết định thay đổi nó một lần nữa và bất kỳ chức năng nào khác trong mã có thể thấy rằng đó là một chàng trai trẻ. Nhấn F12nút hoặc xem cài đặt Tùy chọn để mở bảng điều khiển dành cho nhà phát triển của trình duyệt và nhập "người" để xem giá trị này là gì. Nhập person="a young man"để thay đổi và sau đó nhập "người" một lần nữa để thấy rằng nó đã thay đổi.

Sau này chúng ta có dòng

sing(person);

Dòng này đang gọi hàm, như thể nó đang gọi một con chó

"Hãy hát , đến và lấy người !"

Khi trình duyệt đã tải mã JavaScript đạt đến dòng này, nó sẽ khởi động chức năng. Tôi đặt dòng ở cuối để đảm bảo rằng trình duyệt có tất cả thông tin cần thiết để chạy nó.

Chức năng xác định hành động - chức năng chính là về ca hát. Nó chứa một biến được gọi là FirstPart , áp dụng cho việc hát về người áp dụng cho từng câu hát của bài hát: "Có" + người + "người nuốt". Nếu bạn nhập FirstPart vào bảng điều khiển, bạn sẽ không nhận được câu trả lời vì biến bị khóa trong một chức năng - trình duyệt không thể nhìn thấy bên trong các cửa sổ được tô màu của dấu ngoặc nhọn.

ĐÓNG GÓP: Các bao đóng là các hàm nhỏ hơn nằm trong hàm big sing () . Các nhà máy nhỏ bên trong nhà máy lớn. Mỗi người đều có niềng răng riêng, điều đó có nghĩa là các biến bên trong không thể nhìn thấy từ bên ngoài. Đó là lý do tại sao tên của các biến ( sinh vậtkết quả ) có thể được lặp lại trong các bao đóng nhưng với các giá trị khác nhau. Nếu bạn nhập các tên biến này trong cửa sổ giao diện điều khiển, bạn sẽ không nhận được giá trị của nó vì nó bị ẩn bởi hai lớp cửa sổ được tô màu.

Tất cả các bao đóng đều biết biến của hàm sing () được gọi là FirstPart là gì, bởi vì chúng có thể nhìn ra từ các cửa sổ được tô màu của chúng.

Sau khi đóng cửa đến dòng

fly();
spider();
bird();
cat();

Hàm sing () sẽ gọi từng hàm này theo thứ tự chúng được đưa ra. Sau đó, công việc của hàm sing () sẽ được thực hiện.


56

Được rồi, nói chuyện với một đứa trẻ 6 tuổi, tôi có thể sẽ sử dụng các hiệp hội sau đây.

Hãy tưởng tượng - bạn đang chơi với các anh chị em trong toàn bộ ngôi nhà, và bạn đang di chuyển xung quanh với đồ chơi của mình và mang một số trong số chúng vào phòng của anh trai bạn. Sau một thời gian, anh trai của bạn trở về từ trường và đi đến phòng của anh ấy, và anh ấy bị khóa bên trong nó, vì vậy bây giờ bạn không thể truy cập đồ chơi còn lại ở đó một cách trực tiếp. Nhưng bạn có thể gõ cửa và hỏi anh trai của bạn cho đồ chơi đó. Đây được gọi là đóng cửa đồ chơi ; anh trai của bạn đã làm điều đó cho bạn, và bây giờ anh ta đang ở trong phạm vi bên ngoài .

So sánh với tình huống khi một cánh cửa bị khóa bởi bản nháp và không có ai bên trong (thực thi chức năng chung), và sau đó một số đám cháy cục bộ xảy ra và thiêu rụi căn phòng (người thu gom rác: D), và sau đó một căn phòng mới được xây dựng và bây giờ bạn có thể rời đi một đồ chơi khác ở đó (ví dụ chức năng mới), nhưng không bao giờ có được những đồ chơi tương tự còn lại trong ví dụ phòng đầu tiên.

Đối với một đứa trẻ tiên tiến, tôi sẽ đặt một cái gì đó như sau. Nó không hoàn hảo, nhưng nó khiến bạn cảm thấy nó là gì:

function playingInBrothersRoom (withToys) {
  // We closure toys which we played in the brother's room. When he come back and lock the door
  // your brother is supposed to be into the outer [[scope]] object now. Thanks god you could communicate with him.
  var closureToys = withToys || [],
      returnToy, countIt, toy; // Just another closure helpers, for brother's inner use.

  var brotherGivesToyBack = function (toy) {
    // New request. There is not yet closureToys on brother's hand yet. Give him a time.
    returnToy = null;
    if (toy && closureToys.length > 0) { // If we ask for a specific toy, the brother is going to search for it.

      for ( countIt = closureToys.length; countIt; countIt--) {
        if (closureToys[countIt - 1] == toy) {
          returnToy = 'Take your ' + closureToys.splice(countIt - 1, 1) + ', little boy!';
          break;
        }
      }
      returnToy = returnToy || 'Hey, I could not find any ' + toy + ' here. Look for it in another room.';
    }
    else if (closureToys.length > 0) { // Otherwise, just give back everything he has in the room.
      returnToy = 'Behold! ' + closureToys.join(', ') + '.';
      closureToys = [];
    }
    else {
      returnToy = 'Hey, lil shrimp, I gave you everything!';
    }
    console.log(returnToy);
  }
  return brotherGivesToyBack;
}
// You are playing in the house, including the brother's room.
var toys = ['teddybear', 'car', 'jumpingrope'],
    askBrotherForClosuredToy = playingInBrothersRoom(toys);

// The door is locked, and the brother came from the school. You could not cheat and take it out directly.
console.log(askBrotherForClosuredToy.closureToys); // Undefined

// But you could ask your brother politely, to give it back.
askBrotherForClosuredToy('teddybear'); // Hooray, here it is, teddybear
askBrotherForClosuredToy('ball'); // The brother would not be able to find it.
askBrotherForClosuredToy(); // The brother gives you all the rest
askBrotherForClosuredToy(); // Nothing left in there

Như bạn có thể thấy, đồ chơi còn lại trong phòng vẫn có thể truy cập thông qua người anh em và cho dù phòng có bị khóa hay không. Đây là một jsbin để chơi xung quanh nó.


49

Một câu trả lời cho một đứa trẻ sáu tuổi (giả sử nó biết hàm là gì và biến là gì và dữ liệu là gì):

Các chức năng có thể trả về dữ liệu. Một loại dữ liệu bạn có thể trả về từ một chức năng là một chức năng khác. Khi hàm mới đó được trả về, tất cả các biến và đối số được sử dụng trong hàm tạo ra nó sẽ không biến mất. Thay vào đó, hàm cha đó "đóng lại." Nói cách khác, không có gì có thể nhìn vào bên trong nó và xem các biến được sử dụng ngoại trừ chức năng mà nó trả về. Chức năng mới đó có một khả năng đặc biệt để nhìn lại bên trong chức năng đã tạo ra nó và xem dữ liệu bên trong nó.

function the_closure() {
  var x = 4;
  return function () {
    return x; // Here, we look back inside the_closure for the value of x
  }
}

var myFn = the_closure();
myFn(); //=> 4

Một cách thực sự đơn giản khác để giải thích nó là về phạm vi:

Bất cứ khi nào bạn tạo một phạm vi nhỏ hơn bên trong phạm vi lớn hơn, phạm vi nhỏ hơn sẽ luôn có thể thấy những gì trong phạm vi lớn hơn.


49

Một hàm trong JavaScript không chỉ là một tham chiếu đến một tập hợp các hướng dẫn (như trong ngôn ngữ C), mà nó còn bao gồm một cấu trúc dữ liệu ẩn bao gồm các tham chiếu đến tất cả các biến không nhắm mục tiêu mà nó sử dụng (các biến được bắt). Các chức năng hai mảnh như vậy được gọi là đóng cửa. Mọi chức năng trong JavaScript có thể được coi là một bao đóng.

Đóng cửa là các chức năng với một nhà nước. Nó hơi giống với "cái này" theo nghĩa là "cái này" cũng cung cấp trạng thái cho một chức năng nhưng chức năng và "đây" là các đối tượng riêng biệt ("cái này" chỉ là một tham số ưa thích và là cách duy nhất để liên kết nó vĩnh viễn với một chức năng là tạo ra một bao đóng). Trong khi "cái này" và hàm luôn sống tách biệt, một hàm không thể tách rời khỏi bao đóng của nó và ngôn ngữ không cung cấp phương tiện để truy cập các biến đã bắt.

Bởi vì tất cả các biến ngoài này được tham chiếu bởi hàm lồng nhau thực sự là các biến cục bộ trong chuỗi các hàm bao quanh từ vựng của nó (các biến toàn cục có thể được coi là biến cục bộ của một số hàm gốc) và mỗi lần thực hiện một hàm đều tạo ra các trường hợp mới các biến cục bộ của nó, theo sau là mỗi lần thực hiện một hàm trả về (hoặc nếu không chuyển nó ra, chẳng hạn như đăng ký nó như một hàm gọi lại) thì một hàm lồng nhau sẽ tạo ra một bao đóng mới (với tập hợp các biến không tham chiếu có khả năng duy nhất của chính nó đại diện cho việc thực thi của nó bối cảnh).

Ngoài ra, phải hiểu rằng các biến cục bộ trong JavaScript được tạo không phải trên khung ngăn xếp, mà trên heap và chỉ bị hủy khi không có ai tham chiếu chúng. Khi một hàm trả về, các tham chiếu đến các biến cục bộ của nó bị giảm, nhưng chúng vẫn có thể là null nếu trong quá trình thực thi hiện tại, chúng trở thành một phần của bao đóng và vẫn được tham chiếu bởi các hàm lồng nhau của nó (chỉ có thể xảy ra nếu các tham chiếu đến các hàm lồng nhau này được trả về hoặc chuyển sang một số mã bên ngoài).

Một ví dụ:

function foo (initValue) {
   //This variable is not destroyed when the foo function exits.
   //It is 'captured' by the two nested functions returned below.
   var value = initValue;

   //Note that the two returned functions are created right now.
   //If the foo function is called again, it will return
   //new functions referencing a different 'value' variable.
   return {
       getValue: function () { return value; },
       setValue: function (newValue) { value = newValue; }
   }
}

function bar () {
    //foo sets its local variable 'value' to 5 and returns an object with
    //two functions still referencing that local variable
    var obj = foo(5);

    //Extracting functions just to show that no 'this' is involved here
    var getValue = obj.getValue;
    var setValue = obj.setValue;

    alert(getValue()); //Displays 5
    setValue(10);
    alert(getValue()); //Displays 10

    //At this point getValue and setValue functions are destroyed
    //(in reality they are destroyed at the next iteration of the garbage collector).
    //The local variable 'value' in the foo is no longer referenced by
    //anything and is destroyed too.
}

bar();

47

Có lẽ vượt xa tất cả nhưng sớm nhất là những đứa trẻ sáu tuổi, nhưng một vài ví dụ đã giúp cho khái niệm đóng cửa trong JavaScript nhấp vào cho tôi.

Một bao đóng là một hàm có quyền truy cập vào phạm vi của hàm khác (các biến và hàm của nó). Cách dễ nhất để tạo một bao đóng là với một hàm trong một hàm; Lý do là trong JavaScript, một hàm luôn có quyền truy cập vào phạm vi chứa hàm của nó.

function outerFunction() {
    var outerVar = "monkey";
    
    function innerFunction() {
        alert(outerVar);
    }
    
    innerFunction();
}

outerFunction();

CỨU: khỉ

Trong ví dụ trên, hàm ngoài được gọi là lần lượt gọi hàm bên trong. Lưu ý cách bên ngoài có sẵn cho hàm bên trong, được chứng minh bằng cách cảnh báo chính xác giá trị của outsVar.

Bây giờ hãy xem xét những điều sau đây:

function outerFunction() {
    var outerVar = "monkey";
    
    function innerFunction() {
        return outerVar;
    }
    
    return innerFunction;
}

var referenceToInnerFunction = outerFunction();
alert(referenceToInnerFunction());

CỨU: khỉ

ReferenceToInnerFunction được đặt thành outsFunction (), đơn giản là trả về một tham chiếu đến hàm bên trong. Khi tham chiếuToInnerFunction được gọi, nó sẽ trả về outsVar. Một lần nữa, như trên, điều này chứng tỏ rằng InternalFunction có quyền truy cập vào outsVar, một biến của hàm ngoài. Hơn nữa, thật thú vị khi lưu ý rằng nó vẫn giữ quyền truy cập này ngay cả sau khi hàm ngoài đã thực hiện xong.

Và đây là nơi mọi thứ trở nên thực sự thú vị. Nếu chúng ta muốn loại bỏ hàm ngoài, hãy đặt nó thành null, bạn có thể nghĩ rằng tham chiếuToInnerFunction sẽ mất quyền truy cập của nó vào giá trị của outsVar. Nhưng đây không phải là trường hợp.

function outerFunction() {
    var outerVar = "monkey";
    
    function innerFunction() {
        return outerVar;
    }
    
    return innerFunction;
}

var referenceToInnerFunction = outerFunction();
alert(referenceToInnerFunction());

outerFunction = null;
alert(referenceToInnerFunction());

ALERT: khỉ ALERT: khỉ

Nhưng làm thế nào là như vậy? Làm thế nào có thể tham chiếuToInnerFunction vẫn biết giá trị của outsVar khi mà hàm ngoài đã được đặt thành null?

Lý do mà ReferenceToInnerFunction vẫn có thể truy cập giá trị của outsVar là vì khi lần đóng đầu tiên được tạo bằng cách đặt InternalFactor bên trong hàm ngoài, hàm bên trong đã thêm một tham chiếu đến phạm vi của hàm ngoài (biến và hàm của nó) vào chuỗi phạm vi của nó. Điều này có nghĩa là hàm bên trong có một con trỏ hoặc tham chiếu đến tất cả các biến của hàm ngoài, bao gồm cả lớp ngoài. Vì vậy, ngay cả khi hàm ngoài đã thực hiện xong hoặc ngay cả khi nó bị xóa hoặc được đặt thành null, các biến trong phạm vi của nó, như outsVar, vẫn tồn tại trong bộ nhớ vì tham chiếu nổi bật đến chúng trên một phần của hàm bên trong đã được trả về tài liệu tham khảoToInnerFunction. Để thực sự giải phóng các biến ngoài và phần còn lại của các biến ngoài từ bộ nhớ, bạn sẽ phải loại bỏ tham chiếu nổi bật này đến chúng,

//////////

Hai điều khác về việc đóng cửa cần lưu ý. Đầu tiên, bao đóng sẽ luôn có quyền truy cập vào các giá trị cuối cùng của hàm chứa của nó.

function outerFunction() {
    var outerVar = "monkey";
    
    function innerFunction() {
        alert(outerVar);
    }
    
    outerVar = "gorilla";

    innerFunction();
}

outerFunction();

ALERT: khỉ đột

Thứ hai, khi một bao đóng được tạo, nó giữ lại một tham chiếu đến tất cả các biến và hàm của hàm kèm theo; nó không được chọn và chọn. Và vì vậy, đóng cửa nên được sử dụng một cách tiết kiệm, hoặc ít nhất là cẩn thận, vì chúng có thể cần nhiều bộ nhớ; rất nhiều biến có thể được giữ trong bộ nhớ lâu sau khi hàm chứa đã thực hiện xong.


45

Tôi chỉ đơn giản là trỏ chúng đến trang Mozilla Closures . Đó là lời giải thích tốt nhất, ngắn gọn và đơn giản nhất về những điều cơ bản về đóng cửa và cách sử dụng thực tế mà tôi đã tìm thấy. Rất khuyến khích mọi người học JavaScript.

Và vâng, tôi thậm chí còn giới thiệu nó cho một đứa trẻ 6 tuổi - nếu đứa trẻ 6 tuổi đang học về việc đóng cửa, thì thật hợp lý khi chúng sẵn sàng hiểu được lời giải thích ngắn gọn và đơn giản được cung cấp trong bài viết.


Tôi đồng ý: trang Mozilla nói đặc biệt đơn giản và súc tích. Thật đáng ngạc nhiên, bài viết của bạn đã không được đánh giá cao như những người khác.
Brice Coustillas
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.