การปิด JavaScript ทำงานอย่างไร


7636

คุณจะอธิบายวิธีปิด JavaScript ให้คนที่มีความรู้เกี่ยวกับแนวคิดที่พวกเขาประกอบด้วย (เช่นฟังก์ชั่นตัวแปรและสิ่งที่คล้ายกัน) แต่ไม่เข้าใจว่าปิดตัวเองได้อย่างไร

ฉันเคยเห็นตัวอย่างโครงการที่ให้ไว้ใน Wikipedia แต่น่าเสียดายที่มันไม่ได้ช่วย


391
ปัญหาของฉันกับสิ่งเหล่านี้และคำตอบมากมายคือพวกเขาเข้าหามันจากมุมมองเชิงนามธรรมเชิงทฤษฎีแทนที่จะเริ่มต้นอธิบายว่าทำไมการปิดจึงจำเป็นใน Javascript และสถานการณ์จริงที่คุณใช้ คุณท้ายด้วย tl; dr บทความที่คุณต้องหวดผ่านตลอดเวลาคิด "แต่ทำไม" ฉันจะเริ่มด้วย: การปิดเป็นวิธีการจัดการกับสองความเป็นจริงของ JavaScript ต่อไปนี้: ขอบเขตอยู่ที่ระดับฟังก์ชันไม่ใช่ระดับบล็อกและ b สิ่งที่คุณทำในทางปฏิบัติใน JavaScript นั้นเป็นแบบอะซิงโครนัส / เหตุการณ์
Jeremy Burton

53
@Redandro สำหรับหนึ่งมันทำให้รหัสเหตุการณ์ขับเคลื่อนง่ายต่อการเขียน ฉันอาจใช้ฟังก์ชันเมื่อเพจโหลดเพื่อกำหนดข้อมูลเฉพาะเกี่ยวกับ HTML หรือคุณสมบัติที่มี ฉันสามารถกำหนดและตั้งค่าตัวจัดการในฟังก์ชันนั้นและมีข้อมูลบริบททั้งหมดที่มีอยู่ทุกครั้งที่ตัวจัดการถูกเรียกใช้โดยไม่ต้องค้นหาซ้ำ แก้ไขปัญหาหนึ่งครั้งใช้งานซ้ำในทุก ๆ หน้าซึ่งจำเป็นต้องใช้ตัวจัดการนั้นด้วยค่าใช้จ่ายที่ลดลงเมื่อเรียกใช้งานตัวจัดการซ้ำ คุณเคยเห็นข้อมูลเดิมถูกแมปซ้ำสองครั้งในภาษาที่ไม่มีหรือไม่ การปิดนั้นทำให้ง่ายขึ้นมากในการหลีกเลี่ยงสิ่งนั้น
Erik Reppen

1
@Erik Reppenขอบคุณสำหรับคำตอบ ที่จริงแล้วฉันอยากรู้เกี่ยวกับประโยชน์ของclosureรหัสที่อ่านยากนี้Object Literalซึ่งตรงข้ามกับที่นำมาใช้ซ้ำและลดค่าใช้จ่ายเท่าเดิม แต่ยังต้องใช้รหัสห่อน้อยลง 100%
Redsandro

6
สำหรับโปรแกรมเมอร์ Java คำตอบสั้น ๆ คือมันเป็นฟังก์ชั่นที่เทียบเท่ากับคลาสภายใน คลาสภายในยังเก็บตัวชี้โดยนัยไปยังอินสแตนซ์ของคลาสภายนอกและใช้สำหรับจุดประสงค์เดียวกันมาก (นั่นคือการสร้างตัวจัดการเหตุการณ์)
Boris van Schooten

8
ฉันพบตัวอย่างการปฏิบัตินี้มีประโยชน์มาก: youtube.com/watch?v=w1s9PgtEoJs
Abhi

คำตอบ:


7358

การปิดคือการจับคู่ของ:

  1. ฟังก์ชั่นและ
  2. การอ้างอิงถึงขอบเขตด้านนอกของฟังก์ชัน (สภาพแวดล้อมคำศัพท์)

สภาพแวดล้อมศัพท์เป็นส่วนหนึ่งของทุกบริบทการดำเนินการ (กรอบสแต็ค) และเป็นแผนที่ระหว่างตัวระบุ (เช่นชื่อตัวแปรท้องถิ่น) และค่า

ทุกฟังก์ชั่นใน JavaScript รักษาการอ้างอิงไปยังสภาพแวดล้อมคำศัพท์ภายนอก การอ้างอิงนี้ใช้เพื่อกำหนดค่าบริบทการดำเนินการที่สร้างขึ้นเมื่อมีการเรียกใช้ฟังก์ชัน การอ้างอิงนี้เปิดใช้งานโค้ดภายในฟังก์ชั่นเพื่อ "เห็น" ตัวแปรที่ประกาศภายนอกฟังก์ชั่นโดยไม่คำนึงถึงเวลาและสถานที่ที่เรียกใช้ฟังก์ชัน

หากฟังก์ชั่นถูกเรียกโดยฟังก์ชั่นซึ่งจะถูกเรียกโดยฟังก์ชั่นอื่นแล้วห่วงโซ่ของการอ้างอิงไปยังสภาพแวดล้อมศัพท์ภายนอกจะถูกสร้างขึ้น ห่วงโซ่นี้เรียกว่าห่วงโซ่ขอบเขต

ในรหัสต่อไปนี้จัดinnerรูปแบบการปิดด้วยสภาพแวดล้อมศัพท์ของบริบทการดำเนินการที่สร้างขึ้นเมื่อfooมีการเรียกใช้การปิดตัวแปร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`

กล่าวอีกนัยหนึ่ง: ใน JavaScript ฟังก์ชั่นมีการอ้างอิงไปยัง "box of state" ส่วนตัวซึ่งมีเพียงพวกเขาเท่านั้น (และฟังก์ชั่นอื่น ๆ ที่ประกาศภายในสภาพแวดล้อมคำศัพท์เดียวกัน) มีการเข้าถึง กล่องสถานะนี้จะมองไม่เห็นต่อผู้เรียกฟังก์ชั่นส่งมอบกลไกที่ยอดเยี่ยมสำหรับการซ่อนข้อมูลและการห่อหุ้ม

และจำไว้ว่า: ฟังก์ชั่นใน JavaScript สามารถส่งผ่านได้เช่นตัวแปร (ฟังก์ชั่นชั้นหนึ่ง) ซึ่งหมายถึงการจับคู่การทำงานและสถานะสามารถส่งผ่านโปรแกรมของคุณ: คล้ายกับวิธีที่คุณจะส่งตัวอย่างของคลาสใน C ++

หาก JavaScript ไม่ได้ปิดดังนั้นจะต้องส่งผ่านสถานะเพิ่มเติมระหว่างฟังก์ชันอย่างชัดเจนทำให้พารามิเตอร์แสดงรายการได้นานขึ้น

ดังนั้นหากคุณต้องการให้ฟังก์ชั่นเข้าถึงสถานะส่วนตัวได้เสมอคุณสามารถใช้การปิดได้

... และบ่อยครั้งที่เราไม่ต้องการรัฐเชื่อมโยงกับฟังก์ชั่น ตัวอย่างเช่นใน Java หรือ C ++ เมื่อคุณเพิ่มตัวแปรอินสแตนซ์ส่วนตัวและวิธีการในชั้นเรียนคุณกำลังเชื่อมโยงรัฐกับการทำงาน

ใน C และภาษาทั่วไปส่วนใหญ่อื่น ๆ หลังจากที่ฟังก์ชันส่งคืนแล้วตัวแปรโลคัลทั้งหมดจะไม่สามารถเข้าถึงได้อีกต่อไปเพราะสแตกเฟรมถูกทำลาย ใน JavaScript ถ้าคุณประกาศฟังก์ชั่นภายในฟังก์ชั่นอื่นแล้วตัวแปรท้องถิ่นของฟังก์ชั่นด้านนอกสามารถยังคงสามารถเข้าถึงได้หลังจากกลับมาจากมัน ด้วยวิธีนี้ในรหัสข้างต้นsecretยังคงใช้ได้กับวัตถุฟังก์ชั่นinner, หลังจากที่fooมันได้รับกลับมาจาก

การใช้ประโยชน์ของการปิด

การปิดมีประโยชน์เมื่อใดก็ตามที่คุณต้องการสถานะส่วนตัวที่เกี่ยวข้องกับฟังก์ชั่น นี่เป็นสถานการณ์ที่พบบ่อยมาก - และจำไว้ว่า: JavaScript ไม่มีไวยากรณ์ของคลาสจนถึงปี 2015 และยังไม่มีไวยากรณ์ของฟิลด์ส่วนตัว การปิดตอบสนองความต้องการนี้

ตัวแปรอินสแตนซ์ส่วนตัว

ในรหัสต่อไปนี้ฟังก์ชั่นtoStringปิดเหนือรายละเอียดของรถ

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())

ฟังก์ชั่นการเขียนโปรแกรม

ในรหัสต่อไป, ฟังก์ชั่นinnerปิดทั้งสองและ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

การเขียนโปรแกรมเชิงเหตุการณ์

ในรหัสต่อไป, ฟังก์ชั่นปิดมากกว่าตัวแปรonClickBACKGROUND_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>

modularization

ในตัวอย่างต่อไปนี้รายละเอียดการใช้งานทั้งหมดจะถูกซ่อนอยู่ภายในนิพจน์ฟังก์ชันที่เรียกใช้งานทันที ฟังก์ชั่นtickและtoStringปิดสถานะส่วนตัวและฟังก์ชั่นที่จำเป็นในการทำงานให้เสร็จสมบูรณ์ การปิดทำให้เราสามารถทำให้เป็นโมดูลและห่อหุ้มรหัสของเราได้

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())

ตัวอย่าง

ตัวอย่างที่ 1

นี้แสดงให้เห็นตัวอย่างว่าตัวแปรท้องถิ่นยังไม่ได้คัดลอกในการปิด: ปิดรักษาอ้างอิงกับตัวแปรเดิมตัวเอง ราวกับว่ากรอบสแต็คยังคงอยู่ในหน่วยความจำแม้หลังจากที่ฟังก์ชั่นด้านนอกออก

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

ตัวอย่างที่ 2

ในรหัสต่อไปนี้สามวิธีlog, incrementและupdateใกล้ทั่วทุกมุมสภาพแวดล้อมศัพท์เดียวกัน

และทุกครั้งที่createObjectถูกเรียกบริบทการดำเนินการใหม่ (กรอบสแต็ค) จะถูกสร้างขึ้นและตัวแปรใหม่อย่างสมบูรณ์xและมีการสร้างชุดฟังก์ชั่นใหม่ ( logฯลฯ ) ซึ่งใกล้เคียงกับตัวแปรใหม่นี้

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

ตัวอย่างที่ 3

หากคุณกำลังใช้ตัวแปรที่ประกาศโดยใช้varโปรดระวังว่าคุณเข้าใจตัวแปรตัวใดที่คุณปิด ตัวแปรที่ประกาศใช้varจะถูกยกขึ้น นี้มากน้อยของปัญหาในปัจจุบัน JavaScript เนื่องจากการแนะนำของและletconst

ในรหัสต่อไปนี้ทุกครั้งที่รอบวงที่ฟังก์ชั่นใหม่ที่จะถูกสร้างขึ้นซึ่งปิดมากกว่าinner iแต่เนื่องจากvar iถูกยกขึ้นภายนอกลูปฟังก์ชันภายในทั้งหมดเหล่านี้จึงอยู่ใกล้กับตัวแปรเดียวกันซึ่งหมายความว่าค่าสุดท้ายของi(3) จะถูกพิมพ์สามครั้ง

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]() 
}

คะแนนสุดท้าย:

  • เมื่อใดก็ตามที่มีการประกาศฟังก์ชันใน JavaScript จะมีการสร้างการปิด
  • การคืนค่า a functionจากภายในฟังก์ชันอื่นเป็นตัวอย่างคลาสสิกของการปิดเนื่องจากสถานะภายในฟังก์ชันภายนอกมีฟังก์ชัน implicly ภายในฟังก์ชันส่งคืนโดยปริยายแม้หลังจากฟังก์ชันภายนอกเสร็จสิ้นการดำเนินการ
  • เมื่อใดก็ตามที่คุณใช้eval()ภายในฟังก์ชั่นจะใช้การปิด ข้อความที่คุณสามารถอ้างอิงตัวแปรท้องถิ่นของฟังก์ชั่นและในโหมดที่ไม่ใช่เข้มงวดคุณยังสามารถสร้างตัวแปรท้องถิ่นใหม่โดยใช้evaleval('var foo = …')
  • เมื่อคุณใช้new Function(…)(ตัวสร้างฟังก์ชั่น ) ภายในฟังก์ชั่นมันจะไม่ปิดเหนือสภาพแวดล้อมคำศัพท์: มันปิดบริบททั่วโลกแทน ฟังก์ชั่นใหม่ไม่สามารถอ้างอิงตัวแปรท้องถิ่นของฟังก์ชั่นด้านนอก
  • การปิดใน JavaScript นั้นเหมือนกับการเก็บรักษาการอ้างอิง ( ไม่ใช่การคัดลอก) ไปยังขอบเขตที่จุดประกาศฟังก์ชั่นซึ่งจะทำการอ้างอิงไปยังขอบเขตด้านนอกของมันและอื่น ๆ ตลอดจนวัตถุทั่วโลกที่ด้านบนของ ห่วงโซ่ขอบเขต
  • การปิดถูกสร้างขึ้นเมื่อมีการประกาศฟังก์ชัน การปิดนี้ใช้เพื่อกำหนดค่าบริบทการดำเนินการเมื่อมีการเรียกใช้ฟังก์ชัน
  • ตัวแปรท้องถิ่นชุดใหม่ถูกสร้างขึ้นทุกครั้งที่มีการเรียกใช้ฟังก์ชัน

การเชื่อมโยง


74
สิ่งนี้ฟังดูดี: "การปิดใน JavaScript นั้นเหมือนกับการเก็บสำเนาของตัวแปรในตัวเครื่องทั้งหมดเหมือนกับที่เคยมีเมื่อออกจากฟังก์ชั่น" แต่มันก็ทำให้เข้าใจผิดด้วยเหตุผลสองประการ (1) การเรียกฟังก์ชั่นไม่จำเป็นต้องออกเพื่อสร้างการปิด (2) ไม่ใช่สำเนาของค่าของตัวแปรโลคอล แต่เป็นตัวแปรเอง (3) ไม่ได้บอกว่าใครมีสิทธิ์เข้าถึงตัวแปรเหล่านี้
dlaliberte

27
ตัวอย่างที่ 5 แสดง "gotcha" โดยที่โค้ดไม่ทำงานตามที่ต้องการ แต่มันไม่ได้แสดงวิธีการแก้ไข คำตอบอื่น ๆ นี้แสดงวิธีที่จะทำ
Matt

190
ฉันชอบวิธีที่โพสต์นี้เริ่มต้นด้วยตัวอักษรตัวหนาขนาดใหญ่ที่พูดว่า "Closures Are Not Magic" และจบลงด้วยตัวอย่างแรกด้วย "ความมหัศจรรย์คือใน JavaScript การอ้างอิงฟังก์ชันยังมีการอ้างอิงลับถึงการปิดที่ถูกสร้างขึ้น"
Andrew Macheret

6
ตัวอย่าง # 3 ผสมการปิดกับการยก javascripts ตอนนี้ฉันคิดว่าการอธิบายเพียงการปิดเป็นเรื่องยากพอโดยไม่นำพฤติกรรมการชักรอก สิ่งนี้ช่วยฉันได้มากที่สุด: 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.จากdeveloper.mozilla.org/en-US/docs/Web/JavaScript/Closures
caramba

3
ECMAScript 6 อาจมีการเปลี่ยนแปลงบางอย่างในบทความที่ดีเกี่ยวกับการปิด ตัวอย่างเช่นหากคุณใช้let i = 0แทนvar i = 0ในตัวอย่างที่ 5 testList()จะเป็นการพิมพ์สิ่งที่คุณต้องการในตอนแรก
Nier

3988

ทุกฟังก์ชั่นใน JavaScript รักษาลิงก์ไปยังสภาพแวดล้อมคำศัพท์ภายนอก สภาพแวดล้อมศัพท์เป็นแผนที่ของชื่อทั้งหมด (เช่นตัวแปรพารามิเตอร์) ภายในขอบเขตที่มีค่าของพวกเขา

ดังนั้นเมื่อใดก็ตามที่คุณเห็นfunctionคำหลักรหัสในฟังก์ชั่นนั้นสามารถเข้าถึงตัวแปรที่ประกาศนอกฟังก์ชั่น

function foo(x) {
  var tmp = 3;

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

  bar(10);
}

foo(2);

นี้จะเข้าสู่ระบบ16เพราะฟังก์ชั่นbarปิดเหนือพารามิเตอร์xและตัวแปรซึ่งทั้งสองอยู่ในสภาพแวดล้อมการทำงานของคำศัพท์ด้านนอกtmpfoo

ฟังก์ชั่นbarพร้อมกับการเชื่อมโยงกับสภาพแวดล้อมของฟังก์ชั่นคำศัพท์fooคือการปิด

ฟังก์ชั่นไม่จำเป็นต้องย้อนกลับเพื่อสร้างการปิด เพียงแค่อาศัยประกาศของมันทุกฟังก์ชั่นจะปิดเหนือสภาพแวดล้อมของคำศัพท์ล้อมรอบก่อให้เกิดการปิด

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

ฟังก์ชั่นด้านบนจะเข้าสู่ระบบ 16 เพราะรหัสภายในbarยังสามารถอ้างถึงอาร์กิวเมนต์xและตัวแปรtmpแม้ว่าพวกเขาจะไม่ได้อยู่ในขอบเขตโดยตรง

อย่างไรก็ตามเนื่องจากtmpยังคงแขวนอยู่รอบ ๆ ภายในbarของการปิดจึงสามารถเพิ่มได้ barมันจะเพิ่มขึ้นทุกครั้งที่คุณโทร

ตัวอย่างที่ง่ายที่สุดของการปิดคือ:

var a = 10;

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

เมื่อมีการเรียกใช้ฟังก์ชัน JavaScript บริบทการดำเนินการใหม่ecจะถูกสร้างขึ้น ร่วมกับฟังก์ชั่นการขัดแย้งและวัตถุเป้าหมายบริบทการดำเนินการนี้ยังได้รับการเชื่อมโยงกับสภาพแวดล้อมคำศัพท์ของบริบทการดำเนินการเรียกร้องความหมายตัวแปรที่ประกาศในสภาพแวดล้อมคำศัพท์ด้านนอก (ในตัวอย่างข้างต้นทั้งสองaและb) ecที่มีอยู่จาก

ทุกฟังก์ชั่นสร้างการปิดเพราะทุกฟังก์ชั่นมีลิงค์ไปยังสภาพแวดล้อมคำศัพท์ภายนอก

หมายเหตุตัวแปรที่ตัวเองมองเห็นได้จากภายในปิด, ไม่สำเนา


24
@feeela: ใช่ทุกฟังก์ชั่น JS สร้างการปิด ตัวแปรที่ไม่ได้อ้างอิงจะมีสิทธิ์ได้รับการเก็บขยะในเครื่องมือ JS สมัยใหม่ แต่จะไม่เปลี่ยนความจริงที่ว่าเมื่อคุณสร้างบริบทการดำเนินการบริบทนั้นมีการอ้างอิงถึงบริบทการปฏิบัติที่ล้อมรอบและตัวแปรและ ฟังก์ชั่นนั้นเป็นวัตถุที่มีศักยภาพที่จะถูกย้ายไปยังขอบเขตตัวแปรที่แตกต่างกันในขณะที่ยังคงการอ้างอิงดั้งเดิม นั่นคือการปิด

@Ali ฉันเพิ่งค้นพบว่า jsFiddle ที่ฉันให้ไว้ไม่ได้พิสูจน์อะไรเลยจริง ๆ ตั้งแต่ความdeleteล้มเหลว อย่างไรก็ตามสภาพแวดล้อมของคำศัพท์ที่ฟังก์ชั่นจะดำเนินการเป็น [[ขอบเขต]] (และท้ายที่สุดใช้เป็นฐานสำหรับสภาพแวดล้อมศัพท์ของมันเองเมื่อเรียกใช้) จะถูกกำหนดเมื่อคำสั่งที่กำหนดฟังก์ชั่น ซึ่งหมายความว่าฟังก์ชั่นจะปิดเนื้อหาทั้งหมดของขอบเขตการดำเนินการโดยไม่คำนึงถึงค่าที่มันหมายถึงจริงและไม่ว่าจะหลบหนีขอบเขต โปรดดูหัวข้อ 13.2 และ 10 ในข้อมูลจำเพาะ
ซาด Saeeduddin

8
นี่เป็นคำตอบที่ดีจนกระทั่งพยายามอธิบายประเภทและการอ้างอิงดั้งเดิม มันผิดอย่างสิ้นเชิงและพูดถึงการลอกเลียนแบบตัวอักษรซึ่งไม่มีส่วนเกี่ยวข้องอะไรเลย
Ry-

12
Closures คือคำตอบของ JavaScript สำหรับการวางโปรแกรมเชิงวัตถุ JS ไม่ใช่คลาสดังนั้นจึงต้องหาวิธีอื่นในการนำสิ่งต่าง ๆ มาใช้ซึ่งไม่สามารถนำไปใช้อย่างอื่นได้
Bartłomiej Zalewski

2
นี่ควรเป็นคำตอบที่ยอมรับได้ ความมหัศจรรย์ไม่เคยเกิดขึ้นในฟังก์ชั่นด้านใน มันเกิดขึ้นเมื่อกำหนดฟังก์ชั่นด้านนอกให้กับตัวแปร สิ่งนี้สร้างบริบทการดำเนินการใหม่สำหรับฟังก์ชันภายในเพื่อให้สามารถ "สะสมตัวแปรส่วนตัว" ได้ แน่นอนมันสามารถตั้งแต่ตัวแปรฟังก์ชันภายนอกที่ได้รับมอบหมายให้ดูแลบริบท คำตอบแรกแค่ทำให้ทุกอย่างซับซ้อนขึ้นโดยไม่อธิบายว่าเกิดอะไรขึ้นที่นั่น
Albert Gao

2442

คำปรารภ: คำตอบนี้ถูกเขียนเมื่อคำถามคือ:

เช่นเดียวกับอัลเบิร์ตคนเก่าที่พูดว่า: "ถ้าคุณไม่สามารถอธิบายให้เด็กอายุได้หกขวบคุณก็ไม่เข้าใจตัวเอง" ฉันพยายามอธิบาย JS ให้เพื่อนเก่าอายุ 27 ปีและล้มเหลวโดยสิ้นเชิง

ใครสามารถพิจารณาได้ว่าฉันอายุ 6 ขวบและสนใจเรื่องนี้อย่างประหลาด?

ฉันค่อนข้างแน่ใจว่าฉันเป็นหนึ่งในคนเท่านั้นที่พยายามใช้คำถามเริ่มต้นอย่างแท้จริง ตั้งแต่นั้นมาคำถามก็กลายพันธุ์หลายครั้งดังนั้นคำตอบของฉันตอนนี้อาจดูโง่และไม่น่าเชื่อ หวังว่าความคิดทั่วไปของเรื่องยังคงสนุกสำหรับบางคน


ฉันเป็นแฟนตัวยงของการอุปมาอุปมัยและอุปมาอุปมัยเมื่ออธิบายแนวความคิดที่ยากดังนั้นให้ฉันลองจับมือกับเรื่องราว

กาลครั้งหนึ่ง:

มีเจ้าหญิง ...

function princess() {

เธออาศัยอยู่ในโลกมหัศจรรย์ที่เต็มไปด้วยการผจญภัย เธอได้พบกับเจ้าชายชาร์มมิ่งขี่รอบโลกของเธอด้วยมังกรยูนิคอร์นต่อสู้กับสัตว์พูดคุยและสิ่งมหัศจรรย์อื่น ๆ อีกมากมาย

    var adventures = [];

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

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

    /* ... */

แต่เธอจะต้องกลับไปสู่โลกที่น่าเบื่อของเธอและผู้ใหญ่

    return {

และเธอมักจะบอกพวกเขาถึงการผจญภัยที่น่าตื่นตาตื่นใจล่าสุดของเธอในฐานะเจ้าหญิง

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

แต่สิ่งที่พวกเขาเห็นก็คือเด็กผู้หญิงตัวเล็ก ๆ ...

var littleGirl = princess();

... เล่าเรื่องเวทมนตร์และแฟนตาซี

littleGirl.story();

และถึงแม้ว่าผู้ใหญ่จะรู้จักเจ้าหญิงตัวจริงพวกเขาก็จะไม่เชื่อในยูนิคอร์นหรือมังกรเพราะพวกเขาไม่สามารถมองเห็นพวกมันได้ ผู้ใหญ่บอกว่าพวกเขามีอยู่ในจินตนาการของเด็กหญิงตัวเล็ก ๆ เท่านั้น

แต่เรารู้ความจริง ว่าเด็กหญิงตัวน้อยกับเจ้าหญิงภายใน ...

... เป็นเจ้าหญิงที่มีเด็กผู้หญิงอยู่ข้างใน


340
ฉันชอบคำอธิบายนี้จริงๆ สำหรับผู้ที่อ่านและไม่ปฏิบัติตามการเปรียบเทียบคือ: ฟังก์ชัน Princess () เป็นขอบเขตที่ซับซ้อนซึ่งมีข้อมูลส่วนตัว นอกฟังก์ชั่นข้อมูลส่วนตัวไม่สามารถมองเห็นหรือเข้าถึงได้ เจ้าหญิงเก็บยูนิคอร์นมังกรการผจญภัยและอื่น ๆ ในจินตนาการของเธอ (ข้อมูลส่วนตัว) และผู้ใหญ่ไม่สามารถมองเห็นพวกมันได้ด้วยตนเอง แต่จินตนาการของเจ้าหญิงถูกจับในการปิดสำหรับstory()ฟังก์ชั่นซึ่งเป็นอินเทอร์เฟซเดียวที่littleGirlอินสแตนซ์สัมผัสกับโลกแห่งเวทมนตร์
Patrick M

ดังนั้นนี่storyคือการปิด แต่มีรหัสvar story = function() {}; return story;แล้วlittleGirlจะปิด อย่างน้อยนั่นก็คือความประทับใจที่ฉันได้รับจากการใช้วิธี 'ส่วนตัว' ของ MDN ด้วยการปิด : "ฟังก์ชั่นสาธารณะทั้งสามนั้นเป็นการปิดที่ใช้สภาพแวดล้อมเดียวกัน"
icc97

16
@ icc97 ใช่จะปิดการอ้างอิงถึงสภาพแวดล้อมที่มีให้อยู่ในขอบเขตของstory ยังเป็นอีกการปิดโดยนัยคือและจะแบ่งปันการอ้างอิงใด ๆ ไปยังอาร์เรย์ที่จะมีอยู่ในสภาพแวดล้อม / ขอบเขตที่มีอยู่และกำหนดไว้ princessprincessprincesslittleGirlparentslittleGirlprincess
Jacob Swartwood

6
@BenjaminKrupp ฉันได้เพิ่มความคิดเห็นรหัสที่ชัดเจนเพื่อแสดง / บ่งบอกว่ามีการดำเนินการภายในเนื้อหาprincessมากกว่าที่เขียนไว้ น่าเสียดายที่ตอนนี้เรื่องราวนี้ไม่ได้อยู่ที่หัวข้อนี้ แต่เดิมคำถามก็ขอให้ "อธิบาย JavaScript การปิดเพื่อ 5yr เก่า"; คำตอบของฉันเป็นเพียงคนเดียวที่พยายามทำเช่นนั้น ฉันไม่สงสัยเลยว่ามันจะล้มเหลวอย่างน่าสังเวช แต่อย่างน้อยการตอบสนองนี้อาจมีโอกาสได้รับความสนใจจากผู้มีอายุ 5 ปี
Jacob Swartwood

11
อันที่จริงแล้วสำหรับฉันนี่ทำให้รู้สึกที่สมบูรณ์แบบ และฉันต้องยอมรับในที่สุดการทำความเข้าใจการปิด JS โดยใช้นิทานของเจ้าหญิงและการผจญภัยทำให้ฉันรู้สึกแปลก ๆ
ตกผลึก

753

จากการตั้งคำถามอย่างจริงจังเราควรทราบว่าเด็กอายุ 6 ปีที่มีความสามารถทางสติปัญญาเป็นที่รับรู้ได้อย่างไร แต่เป็นที่ยอมรับว่าคนที่มีความสนใจใน JavaScript นั้นไม่ธรรมดา

ใน การพัฒนาวัยเด็ก: 5 ถึง 7 ปีกล่าวว่า:

ลูกของคุณจะสามารถทำตามคำแนะนำสองขั้นตอน ตัวอย่างเช่นถ้าคุณพูดกับลูกของคุณว่า "ไปที่ครัวแล้วเอาถุงขยะมาให้ฉัน" พวกเขาจะสามารถจำทิศทางนั้นได้

เราสามารถใช้ตัวอย่างนี้เพื่ออธิบายการปิดดังต่อไปนี้:

trashBagsห้องครัวเป็นปิดที่มีตัวแปรท้องถิ่นที่เรียกว่า มีฟังก์ชั่นภายในห้องครัวที่เรียกgetTrashBagว่ารับถุงขยะหนึ่งใบและส่งคืน

เราสามารถรหัสนี้ใน JavaScript เช่นนี้:

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

ประเด็นเพิ่มเติมที่อธิบายว่าทำไมการปิดกิจการจึงน่าสนใจ:

  • แต่ละครั้งmakeKitchen()จะถูกเรียกการปิดใหม่จะถูกสร้างขึ้นโดยแยกจากกันtrashBagsจะเรียกว่าปิดใหม่จะถูกสร้างขึ้นด้วยการแยกของตัวเอง
  • trashBagsตัวแปรท้องถิ่นภายในของห้องครัวแต่ละและไม่ได้ออกไปข้างนอกเข้าถึงได้ แต่ฟังก์ชั่นด้านบนgetTrashBagสถานที่ให้บริการที่ไม่สามารถเข้าถึงได้
  • การเรียกใช้ฟังก์ชันทุกครั้งจะสร้างการปิด แต่จะไม่จำเป็นต้องปิดการปิดรอบเว้นแต่จะมีฟังก์ชั่นด้านในซึ่งสามารถเข้าถึงด้านในของการปิดสามารถเรียกได้จากด้านนอกการปิด กลับวัตถุด้วยgetTrashBagฟังก์ชั่นทำที่นี่

6
ที่จริงแล้วอย่างสับสนการเรียกฟังก์ชั่น makeKitchen เป็นการปิดจริงไม่ใช่วัตถุครัวที่ส่งคืน
dlaliberte

6
มีวิธีของฉันผ่านคนอื่นฉันพบคำตอบนี้เป็นวิธีที่ง่ายที่สุดในการอธิบายเกี่ยวกับสิ่งที่และทำไม closures.is
Chetabahana

3
เมนูและอาหารทานเล่นมากเกินไป, เนื้อสัตว์และมันฝรั่งไม่เพียงพอ. คุณสามารถปรับปรุงคำตอบนั้นด้วยประโยคสั้น ๆ เพียงประโยคเดียวเช่น: "การปิดคือบริบทที่ปิดผนึกของฟังก์ชั่นเนื่องจากไม่มีกลไกการกำหนดขอบเขตโดยคลาส"
Staplerfahrer

584

The Straw Man

ฉันจำเป็นต้องรู้ว่ามีการคลิกปุ่มกี่ครั้งและทำอะไรกับการคลิกที่สามทุกครั้ง ...

ทางออกที่ชัดเจนอย่างเป็นธรรม

// 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>

ตอนนี้สิ่งนี้จะได้ผล แต่มันจะรุกล้ำเข้าไปในขอบเขตด้านนอกโดยการเพิ่มตัวแปรซึ่งมีจุดประสงค์เพียงอย่างเดียวคือการติดตามการนับ ในบางสถานการณ์สิ่งนี้จะดีกว่าเนื่องจากแอปพลิเคชันภายนอกของคุณอาจต้องเข้าถึงข้อมูลนี้ แต่ในกรณีนี้เราเปลี่ยนพฤติกรรมการคลิกของทุก ๆ สามดังนั้นจึงเป็นการดีกว่าที่จะรวมฟังก์ชันการทำงานนี้ไว้ในตัวจัดการเหตุการณ์ใส่ฟังก์ชั่นนี้ภายในตัวจัดการเหตุการณ์

พิจารณาตัวเลือกนี้

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>

สังเกตเห็นบางสิ่งที่นี่

ในตัวอย่างข้างต้นฉันใช้พฤติกรรมการปิดของ JavaScript ลักษณะการทำงานนี้ช่วยให้ฟังก์ชันใด ๆ สามารถเข้าถึงขอบเขตที่มันถูกสร้างขึ้นโดยไม่มีกำหนดในการใช้งานจริงฉันเรียกใช้ฟังก์ชันที่ส่งคืนฟังก์ชันอื่นทันทีและเนื่องจากฟังก์ชันที่ฉันกลับมามีการเข้าถึงตัวแปรการนับภายใน (เนื่องจากพฤติกรรมการปิดที่อธิบายไว้ข้างต้น) ผลลัพธ์นี้เป็นขอบเขตส่วนตัวสำหรับการใช้งานโดยผลลัพธ์ ฟังก์ชั่น ... ไม่ง่ายอย่างนั้นเหรอ? มาเจือจางลง ...

การปิดหนึ่งบรรทัดอย่างง่าย

//          _______________________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); }; })();

ตัวแปรทั้งหมดที่อยู่นอกฟังก์ชั่นที่ส่งคืนจะสามารถใช้ได้กับฟังก์ชั่นที่ส่งคืน แต่พวกเขาจะไม่สามารถใช้ได้โดยตรงกับฟังก์ชั่นวัตถุที่ส่งคืน ...

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

รับมัน ดังนั้นในตัวอย่างหลักของเราตัวแปร count มีอยู่ภายในการปิดและพร้อมใช้งานสำหรับตัวจัดการเหตุการณ์เสมอดังนั้นจึงยังคงสถานะไว้ตั้งแต่คลิกเพื่อคลิก

นอกจากนี้สถานะตัวแปรส่วนตัวนี้สามารถเข้าถึงได้อย่างสมบูรณ์สำหรับทั้งการอ่านและการกำหนดให้กับตัวแปรที่กำหนดขอบเขตส่วนตัว

ไปแล้ว ตอนนี้คุณกำลังห่อหุ้มพฤติกรรมนี้อย่างเต็มที่

โพสต์บล็อกแบบเต็ม (รวมถึงข้อควรพิจารณา jQuery)


11
ฉันไม่เห็นด้วยกับคำจำกัดความของคุณว่าการปิดคืออะไร ไม่มีเหตุผลที่จะต้องเรียกตนเอง นอกจากนี้ยังเป็นเรื่องง่าย ๆ เล็กน้อย (และไม่ถูกต้อง) ที่จะบอกว่ามันต้องเป็น "คืน" (การอภิปรายจำนวนมากเกี่ยวกับเรื่องนี้ในความคิดเห็นของคำตอบที่ดีที่สุดสำหรับคำถามนี้)
James Montagne

40
@ James แม้ว่าคุณจะหมดกำลังใจตัวอย่างของเขา (และโพสต์ทั้งหมด) เป็นหนึ่งในสิ่งที่ดีที่สุดที่ฉันเคยเห็น ในขณะที่คำถามไม่เก่าและแก้ไขได้สำหรับฉันมันสมควรได้รับ +1 ทั้งหมด
E-satis

84
"ฉันจำเป็นต้องรู้ว่ามีการคลิกปุ่มกี่ครั้งและทำบางอย่างในทุก ๆ การคลิก ... " สิ่งนี้ทำให้ฉันสนใจ กรณีการใช้งานและวิธีแก้ปัญหาที่แสดงให้เห็นว่าการปิดไม่ใช่สิ่งลึกลับเช่นนั้นและพวกเราจำนวนมากได้เขียนถึงพวกเขา แต่ไม่รู้ชื่อทางการว่า
Chris22

ตัวอย่างที่ดีเพราะมันแสดงให้เห็นว่า "count" ในตัวอย่างที่ 2 เก็บค่า "count" และไม่รีเซ็ตเป็น 0 ทุกครั้งที่มีการคลิก "องค์ประกอบ" ข้อมูลมาก!
Adam

+1 สำหรับพฤติกรรมปิด เราสามารถ จำกัดพฤติกรรมปิดเพื่อฟังก์ชั่นใน JavaScript หรือแนวคิดนี้ยังสามารถนำไปใช้กับโครงสร้างอื่น ๆ ของภาษา?
Dziamid

492

การปิดล้อมนั้นยากที่จะอธิบายเพราะพวกมันคุ้นเคยกับการทำให้พฤติกรรมบางอย่างทำงานได้ตามที่ทุกคนคาดหวังไว้ ฉันพบวิธีที่ดีที่สุดในการอธิบายพวกเขา (และวิธีที่ฉันเรียนรู้สิ่งที่พวกเขาทำ) คือการจินตนาการถึงสถานการณ์ที่ไม่มีพวกเขา:

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

จะเกิดอะไรขึ้นถ้า JavaScript ไม่รู้จักการปิด เพียงแค่แทนที่การโทรในบรรทัดสุดท้ายด้วยเนื้อความวิธีการ (ซึ่งเป็นสิ่งที่การเรียกฟังก์ชั่นทำ) และคุณจะได้รับ:

console.log(x + 3);

ตอนนี้คำจำกัดความของอยู่xที่ไหน เราไม่ได้กำหนดไว้ในขอบเขตปัจจุบัน ทางออกเดียวคือให้plus5 พกพาขอบเขต (หรือมากกว่าขอบเขตของพาเรนต์) รอบ ๆ วิธีนี้xถูกกำหนดอย่างดีและถูกผูกไว้กับค่า 5


11
นี่เป็นตัวอย่างที่ทำให้คนหลายคนเข้าใจผิดว่าเป็นค่าที่ใช้ในฟังก์ชันที่ส่งคืนไม่ใช่ตัวแปรที่เปลี่ยนแปลงได้ ถ้ามันถูกเปลี่ยนเป็น "return x + = y" หรือดีกว่าทั้งสองอย่างนั้นและฟังก์ชั่นอื่น "x * = y" มันจะชัดเจนว่าไม่มีการคัดลอกใด ๆ สำหรับคนที่เคยใช้เฟรมสแต็คลองนึกภาพโดยใช้ฮีปเฟรมแทนซึ่งจะยังคงมีอยู่หลังจากที่ฟังก์ชันส่งคืน
Matt

14
@ แมทฉันไม่เห็นด้วย ตัวอย่างไม่ควรบันทึกคุณสมบัติทั้งหมดอย่างละเอียด มันหมายถึงการลดและแสดงให้เห็นถึงคุณสมบัติเด่นของแนวคิด OP ขอคำอธิบายง่ายๆ (“ สำหรับอายุหกขวบ”) ใช้คำตอบที่ได้รับการยอมรับ: มันล้มเหลวอย่างมากในการให้คำอธิบายที่กระชับเพราะมันพยายามอย่างละเอียดถี่ถ้วน (ฉันเห็นด้วยกับคุณว่ามันเป็นคุณสมบัติที่สำคัญของ JavaScript ที่มีผลผูกพันคือโดยการอ้างอิงมากกว่าโดยค่า ... แต่อีกครั้งคำอธิบายที่ประสบความสำเร็จคือคำอธิบายที่ช่วยลดค่าต่ำสุดเปลือย)
Konrad Rudolph

@ KonradRudolph ฉันชอบสไตล์และความกะทัดรัดของตัวอย่างของคุณ ฉันแนะนำให้เปลี่ยนมันเล็กน้อยเพื่อให้ส่วนสุดท้าย "ทางออกเดียวคือ ... " กลายเป็นจริง ในปัจจุบันมีอยู่แล้วในความเป็นจริงอีกวิธีแก้ปัญหาที่ง่ายกว่าสำหรับสถานการณ์ของคุณซึ่งไม่สอดคล้องกับการต่อเนื่องของจาวาสคริปต์และไม่สอดคล้องกับความเข้าใจผิดทั่วไปเกี่ยวกับความต่อเนื่อง ดังนั้นตัวอย่างในรูปแบบปัจจุบันจึงเป็นอันตราย สิ่งนี้ไม่ได้เกี่ยวข้องกับการแสดงรายการคุณสมบัติอย่างละเอียด แต่ก็เกี่ยวกับการทำความเข้าใจว่า x คืออะไรในฟังก์ชันส่งคืนซึ่งอยู่หลังจุดหลักทั้งหมด
Matt

@ Matt อืมฉันไม่แน่ใจว่าฉันเข้าใจคุณอย่างเต็มที่ แต่ฉันเริ่มเห็นว่าคุณอาจมีจุดที่ถูกต้อง เนื่องจากความคิดเห็นสั้นเกินไปคุณอาจจะอธิบายสิ่งที่คุณหมายถึงในส่วนสำคัญ / Pastie หรือในห้องแชท? ขอบคุณ
Konrad Rudolph

2
@ KonradRudolph ฉันคิดว่าฉันไม่ชัดเจนเกี่ยวกับจุดประสงค์ของ x + = y วัตถุประสงค์ก็เพื่อแสดงให้เห็นว่าการเรียกซ้ำไปยังฟังก์ชันที่ส่งคืนโดยใช้ตัวแปรเดียวกันx (ตรงข้ามกับค่าเดียวกันซึ่งผู้คนจินตนาการว่า "แทรก" เมื่อสร้างฟังก์ชัน) นี่เป็นเหมือนการแจ้งเตือนสองรายการแรกในซอของคุณ วัตถุประสงค์ของฟังก์ชั่นเพิ่มเติม x * = y จะแสดงให้เห็นว่าฟังก์ชั่นที่ส่งคืนจำนวนมากทั้งหมดใช้ x ร่วมกัน
Matt

376

ตกลงแฟนปิด 6 ปี คุณต้องการฟังตัวอย่างการปิดที่ง่ายที่สุดหรือไม่?

ลองจินตนาการถึงสถานการณ์ต่อไป: ผู้ขับขี่นั่งอยู่ในรถ รถคันนั้นอยู่ในระนาบ เครื่องบินอยู่ในสนามบิน ความสามารถของผู้ขับขี่ในการเข้าถึงสิ่งต่าง ๆ นอกรถของเขา แต่ภายในเครื่องบินแม้ว่าเครื่องบินลำนั้นจะออกจากสนามบิน แค่นั้นแหละ. เมื่อคุณอายุ 27 ปีให้ดูคำอธิบายโดยละเอียดเพิ่มเติมหรือตัวอย่างด้านล่าง

นี่คือวิธีที่ฉันสามารถแปลงเรื่องราวเครื่องบินของฉันเป็นรหัส

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
เล่นได้ดีและตอบรับโปสเตอร์ต้นฉบับ ฉันคิดว่านี่เป็นคำตอบที่ดีที่สุด ฉันจะใช้กระเป๋าเดินทางในทำนองเดียวกัน: ลองจินตนาการว่าคุณไปที่บ้านของคุณยายและคุณเก็บกระเป๋า Nintendo DS ของคุณด้วยการ์ดเกมไว้ในกระเป๋าของคุณ แต่จากนั้นเก็บซองไว้ในกระเป๋าเป้สะพายหลังของคุณ จากนั้นคุณใส่สิ่งของทั้งหมดลงในกระเป๋าเดินทางใบใหญ่ที่มีการ์ดเกมจำนวนมากในกระเป๋าของกระเป๋าเดินทาง เมื่อคุณไปถึงบ้านคุณยายคุณสามารถเล่นเกมใด ๆ ใน DS ของคุณตราบใดที่คดีภายนอกทั้งหมดเปิดอยู่ หรือบางสิ่งบางอย่างกับผลกระทบนั้น
slartibartfast

376

TLDR

การปิดคือการเชื่อมโยงระหว่างฟังก์ชั่นและสภาพแวดล้อมภายนอกของคำศัพท์ (เช่น. ตามที่เขียน) เช่นตัวระบุ (ตัวแปรพารามิเตอร์การประกาศฟังก์ชั่น ฯลฯ ) ที่กำหนดภายในสภาพแวดล้อมนั้นสามารถมองเห็นได้จากภายในฟังก์ชั่น โดยที่ฟังก์ชันนั้นถูกเรียกใช้

รายละเอียด

ในคำศัพท์ของข้อกำหนด ECMAScript การปิดสามารถกล่าวได้ว่าจะดำเนินการโดยการ[[Environment]]อ้างอิงของทุกฟังก์ชั่น - วัตถุซึ่งชี้ไปที่สภาพแวดล้อมศัพท์ที่ภายในฟังก์ชั่นที่กำหนดไว้

เมื่อมีการเรียกใช้ฟังก์ชันผ่าน[[Call]]วิธีการภายในการ[[Environment]]อ้างอิงในฟังก์ชั่น - วัตถุจะถูกคัดลอกไปยังการอ้างอิงสภาพแวดล้อมภายนอกของบันทึกสภาพแวดล้อมของบริบทการดำเนินการที่สร้างขึ้นใหม่(กรอบสแต็ก)

ในตัวอย่างต่อไปนี้ฟังก์ชั่นfปิดสภาพแวดล้อมศัพท์ของบริบทการดำเนินการทั่วโลก:

function f() {}

ในตัวอย่างต่อไปนี้ฟังก์ชั่นhปิดเหนือสภาพแวดล้อมของฟังก์ชั่นgซึ่งในทางกลับกันปิดเหนือสภาพแวดล้อมศัพท์ของบริบทการดำเนินการทั่วโลก

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

หากฟังก์ชั่นด้านในถูกส่งคืนโดย outer ภายนอกแล้วสภาวะแวดล้อมศัพท์ภายนอกจะยังคงอยู่หลังจากที่ฟังก์ชั่นด้านนอกได้กลับมา นี่เป็นเพราะสภาพแวดล้อมศัพท์ด้านนอกจะต้องมีถ้าฟังก์ชั่นภายในถูกเรียกในที่สุด

ในตัวอย่างต่อไปนี้ฟังก์ชั่นjปิดสภาพแวดล้อมของคำศัพท์ฟังก์ชั่นiหมายถึงตัวแปรที่xสามารถมองเห็นได้จากภายในฟังก์ชั่นjนานหลังจากที่ฟังก์ชั่นiเสร็จสิ้นการดำเนินการ:

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

ในการปิดตัวแปรในสภาพแวดล้อมคำศัพท์ด้านนอกของตัวเองที่มีอยู่ไม่ได้สำเนา

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

ห่วงโซ่ของสภาพแวดล้อมคำศัพท์ที่เชื่อมโยงระหว่างบริบทการดำเนินการผ่านการอ้างอิงสภาพแวดล้อมภายนอกสร้างห่วงโซ่ขอบเขตและกำหนดตัวบ่งชี้ที่มองเห็นได้จากฟังก์ชั่นที่กำหนดใด ๆ

โปรดทราบว่าในความพยายามที่จะปรับปรุงความชัดเจนและความถูกต้องคำตอบนี้ได้เปลี่ยนไปอย่างมากจากต้นฉบับ


56
ว้าวไม่เคยรู้เลยว่าคุณสามารถใช้การแทนที่สตริงได้console.logเช่นนี้ หากใครสนใจก็มีอีกมากมาย: developer.mozilla.org/en-US/docs/DOM/…
Flash

7
ตัวแปรที่อยู่ในรายการพารามิเตอร์ของฟังก์ชันก็เป็นส่วนหนึ่งของการปิด (เช่นไม่ จำกัด เพียงvar)
โทมัส Eding

การปิดเสียงเหมือนของวัตถุและคลาสเป็นต้นไม่แน่ใจว่าทำไมคนจำนวนมากไม่เปรียบเทียบสองสิ่งนี้ - จะง่ายกว่าสำหรับเราที่จะเรียนรู้สามเณร!
almaruf

365

นี่เป็นความพยายามที่จะกำจัดความเข้าใจผิดที่เป็นไปได้หลายประการเกี่ยวกับการปิดที่ปรากฏในคำตอบอื่น ๆ

  • การปิดไม่ได้สร้างขึ้นเฉพาะเมื่อคุณส่งคืนฟังก์ชันภายใน ในความเป็นจริงฟังก์ชั่นการปิดล้อมไม่จำเป็นต้องส่งคืนเลยเพื่อสร้างการปิด คุณอาจกำหนดฟังก์ชันภายในของคุณให้กับตัวแปรในขอบเขตด้านนอกหรือส่งผ่านเป็นอาร์กิวเมนต์ให้กับฟังก์ชันอื่นซึ่งสามารถเรียกใช้ได้ทันทีหรือภายหลังก็ได้ ดังนั้นการปิดฟังก์ชั่นการปิดล้อมจึงอาจถูกสร้างขึ้นทันทีที่มีการเรียกใช้ฟังก์ชั่นการปิดล้อมเนื่องจากฟังก์ชั่นภายในมีสิทธิ์เข้าถึงการปิดนั้นเมื่อใดก็ตามที่เรียกใช้ฟังก์ชั่นภายในก่อนหรือหลังฟังก์ชันปิดล้อม
  • การปิดไม่อ้างอิงสำเนาของค่าเก่าของตัวแปรในขอบเขต ตัวแปรเองเป็นส่วนหนึ่งของการปิดดังนั้นค่าที่เห็นเมื่อเข้าถึงหนึ่งในตัวแปรเหล่านั้นเป็นค่าล่าสุด ณ เวลาที่เข้าถึง นี่คือเหตุผลว่าทำไมฟังก์ชั่นภายในที่สร้างขึ้นภายในของลูปอาจมีความยุ่งยากเนื่องจากแต่ละคนมีการเข้าถึงตัวแปรภายนอกที่เหมือนกันแทนที่จะจับสำเนาของตัวแปรในเวลาที่ฟังก์ชั่นถูกสร้างหรือเรียก
  • "ตัวแปร" ในการปิดรวมถึงฟังก์ชั่นใด ๆ ที่มีชื่อประกาศไว้ภายในฟังก์ชั่น พวกเขายังมีข้อโต้แย้งของฟังก์ชั่น การปิดนั้นยังสามารถเข้าถึงตัวแปรของการปิดที่มีตลอดจนถึงขอบเขตทั่วโลก
  • การปิดใช้หน่วยความจำ แต่ไม่ทำให้หน่วยความจำรั่วเนื่องจาก JavaScript เองจะล้างโครงสร้างวงกลมของตัวเองที่ไม่ได้อ้างอิง การรั่วไหลของหน่วยความจำของ Internet Explorer ที่เกี่ยวข้องกับการปิดถูกสร้างขึ้นเมื่อไม่สามารถตัดการเชื่อมต่อค่าแอตทริบิวต์ DOM ที่การอ้างอิงปิดดังนั้นจึงคงการอ้างอิงไปยังโครงสร้างแบบวงกลม

15
ฉันพูดว่าการปิดเป็น "คงที่" ที่สร้างขึ้นในเวลาที่มีการเรียกใช้ฟังก์ชั่นการปิดล้อมเนื่องจากมีความเป็นไปได้ที่การใช้งานอาจเลื่อนการสร้างการปิดจนกว่าจะถึงเวลาต่อมา หากไม่มีฟังก์ชั่นภายในที่กำหนดไว้ในฟังก์ชั่นการปิดล้อมก็ไม่จำเป็นต้องปิด ดังนั้นอาจรอจนกว่าฟังก์ชันภายในแรกจะถูกสร้างขึ้นเพื่อสร้างการปิดบริบทการโทรของฟังก์ชันที่ล้อมรอบ
dlaliberte

9
@ Beetroot-Beetroot สมมติว่าเรามีฟังก์ชั่นภายในที่ถูกส่งผ่านไปยังฟังก์ชั่นอื่นที่มันถูกใช้ก่อนที่ฟังก์ชั่นด้านนอกส่งกลับและสมมติว่าเรายังส่งกลับฟังก์ชันภายในเดียวกันจากฟังก์ชั่นด้านนอก มันเป็นฟังก์ชันเดียวกันในทั้งสองกรณีเหมือนกัน แต่คุณกำลังบอกว่าก่อนที่ฟังก์ชันด้านนอกจะส่งคืนฟังก์ชันภายในจะ "ผูกมัด" กับสแต็กการเรียกในขณะที่หลังจากส่งคืนฟังก์ชันภายในจะถูกปิดทันที มันทำงานเหมือนกันในทั้งสองกรณี ความหมายเหมือนกันดังนั้นคุณไม่เพียงแค่พูดถึงรายละเอียดการปฏิบัติ
dlaliberte

7
@ Beetroot-Beetroot ขอบคุณสำหรับความคิดเห็นของคุณและฉันดีใจที่มีคุณคิด ฉันยังไม่เห็นความแตกต่างทางความหมายระหว่างบริบทสดของฟังก์ชั่นด้านนอกและบริบทเดียวกันเมื่อมันถูกปิดเมื่อฟังก์ชันส่งคืน (ถ้าฉันเข้าใจคำจำกัดความของคุณ) ฟังก์ชั่นด้านในไม่สนใจ การรวบรวมขยะไม่สนใจเนื่องจากฟังก์ชั่นด้านในรักษาการอ้างอิงถึงบริบท / การปิดอย่างใดอย่างหนึ่งและผู้เรียกของฟังก์ชั่นด้านนอกเพียงแค่ลดการอ้างอิงไปยังบริบทการโทร แต่มันสร้างความสับสนให้กับผู้คนและอาจดีกว่าที่จะเรียกมันว่าบริบทการโทร
dlaliberte

9
บทความนั้นอ่านยาก แต่ฉันคิดว่าจริง ๆ แล้วสนับสนุนสิ่งที่ฉันพูด มันบอกว่า: "การปิดเกิดขึ้นจากการคืนค่าฟังก์ชันวัตถุ [... ] หรือโดยการกำหนดการอ้างอิงโดยตรงไปยังวัตถุฟังก์ชันเช่นตัวอย่างเช่นตัวแปรโกลบอล" ฉันไม่ได้หมายความว่า GC ไม่เกี่ยวข้อง แทนที่จะเป็นเพราะ GC และเนื่องจากฟังก์ชั่นด้านในติดอยู่กับบริบทการโทรของฟังก์ชั่นด้านนอก (หรือ [[ขอบเขต]] ตามที่บทความกล่าว) แล้วมันไม่สำคัญว่าการเรียกฟังก์ชั่นด้านนอกส่งกลับเพราะผูกพันกับภายใน ฟังก์ชั่นเป็นสิ่งที่สำคัญ
dlaliberte

3
คำตอบที่ดี! สิ่งหนึ่งที่คุณควรเพิ่มคือฟังก์ชั่นทั้งหมดจะปิดเนื้อหาทั้งหมดของขอบเขตการดำเนินการที่กำหนดไว้ ไม่สำคัญว่าจะอ้างถึงตัวแปรบางส่วนหรือไม่จากขอบเขตพาเรนต์: การอ้างอิงถึงสภาวะแวดล้อมของขอบเขตพาเรนต์จะถูกเก็บไว้เป็น [[ขอบเขต]] โดยไม่มีเงื่อนไข สามารถดูได้จากส่วนการสร้างฟังก์ชันในข้อมูลจำเพาะ ECMA
ซาด Saeeduddin

236

ฉันเขียนบล็อกโพสต์ในขณะที่อธิบายการปิดกลับ นี่คือสิ่งที่ฉันพูดเกี่ยวกับการปิดในแง่ของเหตุผลที่คุณต้องการ

การปิดเป็นวิธีที่จะให้ฟังก์ชันมีตัวแปรส่วนตัวที่คงอยู่นั่นคือตัวแปรที่มีเพียงหนึ่งฟังก์ชันเท่านั้นที่รู้ซึ่งสามารถติดตามข้อมูลจากเวลาก่อนหน้าที่ถูกเรียกใช้

ในแง่นั้นพวกเขาปล่อยให้ฟังก์ชั่นทำหน้าที่เหมือนวัตถุที่มีคุณสมบัติส่วนตัว

โพสต์แบบเต็ม:

ดังนั้นสิ่งที่ปิดเหล่านี้คืออะไร?


ดังนั้นประโยชน์หลักของการปิดกิจการจึงสามารถเน้นกับตัวอย่างนี้ได้หรือไม่? ว่าฉันมีฟังก์ชั่น emailError (sendToAddress, errorString) ฉันสามารถพูดdevError = emailError("devinrhode2@googmail.com", errorString)แล้วมีฟังก์ชั่น emailError ที่ใช้ร่วมกันในเวอร์ชันที่กำหนดเองได้หรือไม่
Devin G Rhode

คำอธิบายนี้และตัวอย่างที่สมบูรณ์แบบที่เกี่ยวข้องในลิงค์ไปยัง (ปิด thingys) เป็นวิธีที่ดีที่สุดในการทำความเข้าใจการปิดและควรจะอยู่ด้านบน!
HopeKing

215

การปิดง่าย:

ตัวอย่างง่ายๆต่อไปนี้ครอบคลุมจุดสำคัญทั้งหมดของการปิด JavaScript * * * *  

นี่คือโรงงานที่ผลิตเครื่องคิดเลขที่สามารถเพิ่มและทวีคูณ:

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

จุดสำคัญ: การโทรแต่ละครั้งเพื่อmake_calculatorสร้างตัวแปรท้องถิ่นใหม่nซึ่งยังคงสามารถใช้งานได้โดยเครื่องคิดเลขaddและmultiplyฟังก์ชั่นที่ยาวนานหลังจากmake_calculatorกลับมา

หากคุณคุ้นเคยกับกรอบสแต็คเครื่องคิดเลขเหล่านี้จะดูแปลก: พวกเขาจะสามารถเข้าถึงnหลังจากmake_calculatorกลับมาได้อย่างไร คำตอบคือจินตนาการว่า JavaScript ไม่ใช้ "stack frames" แต่ใช้ "heap frames" แทนซึ่งสามารถคงอยู่ได้หลังจากการเรียกใช้ฟังก์ชันที่ทำให้พวกเขากลับมา

ฟังก์ชั่นภายในเช่นaddและmultiplyซึ่งตัวแปรการเข้าถึงประกาศในฟังก์ชั่นด้านนอก**จะถูกเรียกว่าปิด

นั่นคือทั้งหมดที่มีเพื่อปิด



*ยกตัวอย่างเช่นมันครอบคลุมประเด็นทั้งหมดในบทความ "Closures for Dummies" ที่ให้ไว้ในคำตอบอื่นยกเว้นตัวอย่างที่ 6 ซึ่งแสดงให้เห็นว่าตัวแปรสามารถใช้งานได้ก่อนที่จะมีการประกาศความจริงที่รู้ แต่ไม่เกี่ยวข้องกับการปิดทั้งหมด นอกจากนี้ยังครอบคลุมจุดทั้งหมดในคำตอบที่ได้รับการยอมรับยกเว้นสำหรับจุด (1) ที่ทำหน้าที่คัดลอกอาร์กิวเมนต์ของพวกเขาไปยังตัวแปรท้องถิ่น (ชื่อฟังก์ชันอาร์กิวเมนต์) และ (2) ที่คัดลอกตัวเลขสร้างหมายเลขใหม่ แต่คัดลอกการอ้างอิงวัตถุ ให้การอ้างอิงอื่นแก่วัตถุเดียวกัน สิ่งเหล่านี้เป็นสิ่งที่ดีที่จะรู้ แต่ไม่เกี่ยวข้องกับการปิดอีกครั้ง นอกจากนี้ยังคล้ายกับตัวอย่างในคำตอบแต่สั้นกว่าเล็กน้อยและเป็นนามธรรมน้อยกว่า มันไม่ครอบคลุมจุดคำตอบนี้หรือความคิดเห็นนี้ปัจจุบันซึ่งเป็นที่ทำให้ JavaScript ยากต่อการเสียบค่าของตัวแปรลูปในฟังก์ชั่นด้านในของคุณ: ขั้นตอน "เสียบใน" สามารถทำได้ด้วยฟังก์ชั่นผู้ช่วยที่ล้อมรอบฟังก์ชั่นภายในของคุณและจะถูกเรียกใช้ในการวนซ้ำแต่ละครั้ง (พูดอย่างเคร่งครัดฟังก์ชั่นด้านในจะเข้าถึงสำเนาของฟังก์ชั่นตัวช่วยแทนการเสียบปลั๊กอะไร) อีกครั้งมีประโยชน์มากเมื่อสร้างการปิด แต่ไม่ได้เป็นส่วนหนึ่งของการปิดหรือการทำงานของมัน มีความสับสนเพิ่มเติมเนื่องจากการปิดการทำงานแตกต่างกันในภาษาที่ใช้งานได้เช่น ML ซึ่งตัวแปรถูกผูกไว้กับค่ามากกว่าที่จะเป็นพื้นที่จัดเก็บทำให้เกิดกระแสคงที่ของผู้ที่เข้าใจการปิดในทาง (กล่าวคือ "การเสียบ") เพียงแค่ไม่ถูกต้องสำหรับ JavaScript ที่ตัวแปรถูกผูกไว้กับพื้นที่เก็บข้อมูลเสมอและไม่เคยมีค่า

**ฟังก์ชั่นด้านนอกใด ๆ หากมีหลายซ้อนกันหรือแม้กระทั่งในบริบทโลกตามที่คำตอบนี้ชี้ให้เห็นอย่างชัดเจน


จะเกิดอะไรขึ้นถ้าคุณเรียกว่า: second_calculator = first_calculator (); แทน second_calculator = make_calculator (); ? ควรเหมือนกันใช่มั้ย
Ronen Festinger

4
@Ronen: เนื่องจากfirst_calculatorเป็นวัตถุ (ไม่ใช่ฟังก์ชั่น) คุณไม่ควรใช้วงเล็บsecond_calculator = first_calculator;เนื่องจากมันเป็นการกำหนดไม่ใช่การเรียกฟังก์ชัน ในการตอบคำถามของคุณจะมีเพียงการเรียกไปยัง make_calculator เพียงครั้งเดียวดังนั้นจะมีเพียงหนึ่งเครื่องคิดเลขเท่านั้นและตัวแปร first_calculator และ second_calculator ทั้งคู่จะอ้างถึงเครื่องคิดเลขเดียวกันดังนั้นคำตอบจะเป็น 3, 403, 4433, 44330
แมตต์

204

ฉันจะอธิบายยังไงกับเด็กอายุหกขวบ:

คุณรู้หรือไม่ว่าผู้ใหญ่สามารถเป็นเจ้าของบ้านได้อย่างไรและพวกเขาเรียกมันว่าบ้าน? เมื่อแม่มีลูกลูกจะไม่ได้เป็นเจ้าของอะไรเลยใช่มั้ย แต่พ่อแม่ของมันเป็นเจ้าของบ้านดังนั้นเมื่อใดก็ตามที่มีคนถามเด็กว่า "บ้านของคุณอยู่ที่ไหน" เขา / เธอสามารถตอบว่า "บ้านหลังนี้!" และชี้ไปที่บ้านของพ่อแม่ "การปิด" คือความสามารถของเด็กที่จะพูดเสมอ (ถึงแม้ว่าในต่างประเทศ) จะสามารถพูดได้ว่ามันมีบ้านแม้ว่ามันจะเป็นพ่อแม่ที่เป็นเจ้าของบ้าน


200

คุณช่วยอธิบายถึงการปิดกิจการให้กับเด็กอายุ 5 ปีได้ไหม *

ฉันยังคิดว่าคำอธิบายของ Googleทำงานได้ดีมากและกระชับ:

/*
*    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);​

พิสูจน์ว่าตัวอย่างนี้สร้างการปิดแม้ว่าฟังก์ชั่นด้านในจะไม่กลับมา

* คำถาม AC #


11
รหัสคือ "ถูกต้อง" เป็นตัวอย่างของการปิดแม้ว่าจะไม่ได้อยู่ในส่วนของความคิดเห็นเกี่ยวกับการใช้การปิดหลังจาก outerFunction กลับมา ดังนั้นจึงไม่ใช่ตัวอย่างที่ดี มีวิธีอื่นอีกหลายวิธีที่การปิดสามารถใช้ที่ไม่เกี่ยวข้องกับการคืนค่าฟังก์ชันภายใน เช่น innerFunction สามารถส่งผ่านไปยังฟังก์ชั่นอื่นซึ่งถูกเรียกทันทีหรือเก็บไว้และเรียกบางครั้งในภายหลังและในทุกกรณีจะสามารถเข้าถึงบริบท outerFunction ที่สร้างขึ้นเมื่อมันถูกเรียก
dlaliberte

6
@ sockock ไม่ Moss ผิด การปิดถูกสร้างขึ้นโดยไม่คำนึงถึงว่าฟังก์ชั่นที่เคยหลบหนีขอบเขตที่มันถูกกำหนดและการอ้างอิงที่สร้างขึ้นโดยไม่มีเงื่อนไขไปยังสภาพแวดล้อมคำศัพท์ของผู้ปกครองทำให้ตัวแปรทั้งหมดในขอบเขตผู้ปกครองสามารถใช้ได้สำหรับฟังก์ชั่นทั้งหมดโดยไม่คำนึงว่าพวกเขาจะ ขอบเขตที่สร้างขึ้น
ซาด Saeeduddin

176

ฉันมักจะเรียนรู้ได้ดีขึ้นจากการเปรียบเทียบ GOOD / BAD ฉันชอบที่จะเห็นรหัสการทำงานตามมาด้วยรหัสที่ไม่ทำงานซึ่งบางคนมีโอกาสที่จะพบ ฉันรวบรวมjsFiddle ที่ทำการเปรียบเทียบและพยายามที่จะลดความแตกต่างของคำอธิบายที่ง่ายที่สุดที่ฉันสามารถหาได้

การปิดบัญชีทำได้ถูกต้อง:

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]());
}
  • ในโค้ดข้างต้นcreateClosure(n)จะถูกเรียกใช้ในการวนซ้ำทุกครั้งของลูป โปรดทราบว่าฉันตั้งชื่อตัวแปรnเพื่อเน้นว่าเป็นตัวแปรใหม่ที่สร้างขึ้นในขอบเขตฟังก์ชั่นใหม่และไม่ได้เป็นตัวแปรเดียวกับindexที่ถูกผูกไว้กับขอบเขตด้านนอก

  • สิ่งนี้จะสร้างขอบเขตใหม่และnถูกผูกไว้กับขอบเขตนั้น นี่หมายความว่าเรามี 10 ขอบเขตแยกกันสำหรับแต่ละการทำซ้ำ

  • createClosure(n) ส่งคืนฟังก์ชันที่คืนค่า n ภายในขอบเขตนั้น

  • ภายในแต่ละขอบเขตnจะผูกพันกับค่าใด ๆ ที่มีเมื่อcreateClosure(n)ถูกเรียกใช้ดังนั้นฟังก์ชันซ้อนที่ได้รับคืนจะส่งกลับค่าของค่าnที่ได้รับเมื่อcreateClosure(n)เรียกใช้เสมอ

การปิดผิด:

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]());
}
  • ในโค้ดข้างต้นลูปจะถูกย้ายภายในcreateClosureArray()ฟังก์ชั่นและตอนนี้ฟังก์ชั่นก็แค่ส่งคืนอาเรย์ที่เสร็จสมบูรณ์

  • สิ่งที่อาจไม่ชัดเจนคือเนื่องจากcreateClosureArray()ถูกเรียกใช้เพียงครั้งเดียวจะสร้างขอบเขตเดียวสำหรับฟังก์ชันนี้แทนที่จะเป็นหนึ่งขอบเขตสำหรับการวนซ้ำทุกครั้งของลูป

  • ภายในฟังก์ชั่นนี้ตัวแปรชื่อindexถูกกำหนดไว้ indexห่วงทำงานและเพิ่มฟังก์ชั่นไปยังอาร์เรย์ที่กลับมา โปรดทราบว่าindexมีการกำหนดไว้ภายในcreateClosureArrayฟังก์ชั่นที่เคยถูกเรียกใช้เพียงครั้งเดียวเท่านั้น

  • เนื่องจากมีเพียงขอบเขตเดียวภายในcreateClosureArray()ฟังก์ชันindexจึงถูกผูกไว้กับค่าภายในขอบเขตนั้นเท่านั้น กล่าวอีกนัยหนึ่งทุกครั้งที่วงวนเปลี่ยนค่าindexมันเปลี่ยนทุกอย่างที่อ้างอิงภายในขอบเขตนั้น

  • ฟังก์ชันทั้งหมดที่เพิ่มในอาร์เรย์ส่งคืนindexตัวแปรSAME จากขอบเขตพาเรนต์ที่กำหนดไว้แทน 10 รายการที่แตกต่างจาก 10 ขอบเขตที่แตกต่างกันเช่นตัวอย่างแรก ผลลัพธ์ที่ได้คือฟังก์ชั่นทั้ง 10 ฟังก์ชั่นคืนค่าตัวแปรเดียวกันจากขอบเขตเดียวกัน

  • หลังจากลูปเสร็จสิ้นและindexถูกแก้ไขเสร็จสิ้นค่าสุดท้ายคือ 10 ดังนั้นทุกฟังก์ชั่นที่เพิ่มเข้ามาในอาร์เรย์จะส่งคืนค่าของindexตัวแปรเดี่ยวซึ่งตอนนี้ตั้งค่าเป็น 10

ผลลัพธ์

การปิดทำการด้านขวา
n = 0
n = 1
n = 2
n = 3
n = 4
n = 5
n = 6
n = 7
n = 8
n = 9

การปิดกระทำผิด
n = 10
n = 10
n = 10
n = 10
n = 10
n = 10
n = 10
n = 10
n = 10
n = 10


1
นอกจากนี้ขอขอบคุณ เพียงเพื่อทำให้ชัดเจนยิ่งขึ้นสามารถจินตนาการได้ว่าอาร์เรย์ "ไม่ดี" ถูกสร้างขึ้นในลูป "เลว" กับการวนซ้ำแต่ละครั้ง: การวนซ้ำครั้งที่ 1: [function () {return 'n =' + 0;}] ซ้ำครั้งที่ 2: [( function () {return 'n =' + 1;}), (function () {return 'n =' + 1;})] การวนซ้ำครั้งที่ 3: [(ฟังก์ชั่น () {return 'n =' + 2;}) , (function () {return 'n =' + 2;}), (function () {return 'n =' + 2;})] ฯลฯ ) ดังนั้นทุกครั้งที่ค่าดัชนีเปลี่ยนแปลงจะสะท้อนให้เห็นในทุกฟังก์ชั่น เพิ่มไปยังอาร์เรย์แล้ว
Alex Alexeev

3
ใช้letสำหรับvarแก้ไขความแตกต่าง
Rupam Datta

ไม่ได้อยู่ที่นี่ "การปิดที่ถูกต้อง" เป็นตัวอย่างของ
TechnicalSmile

ฉันหมายความว่าทุกฟังก์ชั่นปิดทางเทคนิค แต่ส่วนที่สำคัญคือฟังก์ชั่นกำหนดตัวแปรใหม่ภายใน ฟังก์ชั่นที่ได้รับผลตอบแทนเพียงแค่การอ้างอิงที่nสร้างขึ้นในการปิดใหม่ เราเพิ่งส่งคืนฟังก์ชันเพื่อให้เราสามารถเก็บไว้ในอาร์เรย์และเรียกใช้ในภายหลัง
Chev

หากคุณต้องการเพียงแค่เก็บผลในอาร์เรย์ในการย้ำแรกแล้วคุณสามารถ inline arr[index] = (function (n) { return 'n = ' + n; })(index);มันเช่นนี้ แต่คุณกำลังเก็บสตริงผลลัพธ์ในอาร์เรย์มากกว่าฟังก์ชั่นที่จะเรียกใช้ซึ่งเอาชนะจุดตัวอย่างของฉัน
Chev

164

Wikipedia เกี่ยวกับการปิด :

ในวิทยาการคอมพิวเตอร์การปิดเป็นฟังก์ชันร่วมกับสภาพแวดล้อมอ้างอิงสำหรับชื่อ nonlocal (ตัวแปรอิสระ) ของฟังก์ชันนั้น

เทคนิคในJavaScript , ฟังก์ชั่นทุกคนมีการปิด มันมีการเข้าถึงตัวแปรที่กำหนดไว้ในขอบเขตโดยรอบเสมอ

เนื่องจากการสร้างขอบเขตที่กำหนดใน JavaScript เป็นฟังก์ชั่นไม่ใช่บล็อกโค้ดเหมือนในภาษาอื่น ๆสิ่งที่เรามักจะหมายถึงโดยการปิดใน JavaScriptคือฟังก์ชั่นที่ทำงานกับตัวแปร nonlocal ที่กำหนดในฟังก์ชันรอบข้างแล้ว

การปิดมักจะใช้สำหรับการสร้างฟังก์ชั่นที่มีข้อมูลส่วนตัวที่ซ่อนอยู่ (แต่ก็ไม่ได้เป็นเช่นนั้นเสมอไป)

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.

EMS

ตัวอย่างข้างต้นคือการใช้ฟังก์ชั่นที่ไม่ระบุชื่อซึ่งถูกดำเนินการครั้งเดียว แต่มันไม่จำเป็นต้องเป็น มันสามารถตั้งชื่อ (เช่นmkdb) และดำเนินการในภายหลังการสร้างฟังก์ชั่นฐานข้อมูลทุกครั้งที่มีการเรียกใช้ ทุกฟังก์ชั่นที่สร้างขึ้นจะมีวัตถุฐานข้อมูลที่ซ่อนอยู่ของตัวเอง อีกตัวอย่างการใช้งานของการปิดคือเมื่อเราไม่ส่งคืนฟังก์ชั่น แต่วัตถุที่มีหลายฟังก์ชั่นเพื่อวัตถุประสงค์ที่แตกต่างกันแต่ละฟังก์ชั่นเหล่านั้นมีการเข้าถึงข้อมูลเดียวกัน


2
นี่คือคำอธิบายที่ดีที่สุดสำหรับการปิด JavaScript ควรเป็นคำตอบที่เลือก ส่วนที่เหลือให้ความบันเทิงเพียงพอ แต่อันนี้มีประโยชน์ในทางปฏิบัติสำหรับผู้เขียนโค้ด JavaScript ที่ใช้งานจริง
geoidesic

136

ฉันรวบรวมการสอน JavaScript เชิงโต้ตอบเพื่ออธิบายวิธีการปิดการทำงาน การปิดคืออะไร

นี่คือหนึ่งในตัวอย่าง:

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

เด็ก ๆ จะจดจำความลับที่พวกเขาแบ่งปันกับผู้ปกครองเสมอแม้หลังจากที่พ่อแม่ของพวกเขาจากไปแล้ว นี่คือสิ่งที่ปิดสำหรับฟังก์ชั่น

ความลับสำหรับฟังก์ชั่น JavaScript เป็นตัวแปรส่วนตัว

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

ทุกครั้งที่คุณเรียกมันว่า "ชื่อ" ตัวแปรท้องถิ่นจะถูกสร้างและให้ชื่อ "แมรี่" และทุกครั้งที่ฟังก์ชันออกจากตัวแปรจะหายไปและชื่อจะถูกลืม

อย่างที่คุณอาจเดาได้เพราะตัวแปรถูกสร้างขึ้นใหม่ทุกครั้งที่มีการเรียกใช้ฟังก์ชันและไม่มีใครรู้ว่าจะต้องมีสถานที่ลับที่เก็บไว้ อาจเรียกว่าChamber of SecretsหรือStackหรือขอบเขตท้องถิ่นแต่มันก็ไม่สำคัญ เรารู้ว่าพวกเขาอยู่ที่นั่นที่ไหนสักแห่งซ่อนอยู่ในความทรงจำ

แต่ในจาวาสคริปต์มีสิ่งที่พิเศษมากซึ่งฟังก์ชั่นที่สร้างขึ้นในฟังก์ชั่นอื่น ๆ ยังสามารถรู้ตัวแปรท้องถิ่นของพ่อแม่ของพวกเขาและทำให้พวกเขาตราบเท่าที่พวกเขามีชีวิตอยู่

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

ดังนั้นตราบใดที่เรายังอยู่ใน parent -function มันสามารถสร้างฟังก์ชั่นลูกหนึ่งอันหรือมากกว่านั้นซึ่งแชร์ตัวแปรลับจากที่ลับ

แต่สิ่งที่น่าเศร้าก็คือถ้าเด็กเป็นตัวแปรส่วนตัวของหน้าที่ผู้ปกครองมันจะตายเมื่อผู้ปกครองจบลงและความลับก็จะตายไปกับพวกเขา

ดังนั้นเพื่อมีชีวิตอยู่เด็กต้องออกไปก่อนที่จะสายเกินไป

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 

และตอนนี้แม้ว่าแมรี่จะ "ไม่ทำงานอีกต่อไป" แต่ความทรงจำของเธอจะไม่สูญหายไปและลูก ๆ ของเธอจะจดจำชื่อและความลับอื่น ๆ ที่พวกเขาแบ่งปันในช่วงเวลาที่อยู่ด้วยกันเสมอ

ดังนั้นถ้าคุณเรียกเด็กว่า "อลิซ" เธอจะตอบกลับ

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

นั่นคือทั้งหมดที่มีให้บอก


15
นี่คือคำอธิบายที่สมเหตุสมผลที่สุดสำหรับฉันเพราะมันไม่ได้ถือว่าความรู้เดิม ๆ เกี่ยวกับคำศัพท์ทางเทคนิคอย่างมีนัยสำคัญ คำอธิบายที่ได้รับการโหวตสูงสุดที่นี่ถือว่าคนที่ไม่เข้าใจการปิดมีความเข้าใจที่สมบูรณ์และสมบูรณ์ของคำเช่น 'ขอบเขตคำศัพท์' และ 'บริบทการดำเนินการ' - ในขณะที่ฉันสามารถเข้าใจแนวคิดเหล่านี้ฉันไม่คิดว่าฉันเป็น สะดวกสบายกับรายละเอียดของพวกเขาอย่างที่ฉันควรจะเป็นและคำอธิบายที่ไม่มีศัพท์แสงในนั้นเป็นสิ่งที่ทำให้การปิดในที่สุดก็คลิกเพื่อฉันขอบคุณ เป็นโบนัสฉันคิดว่ามันยังอธิบายถึงขอบเขตที่ชัดเจนมาก
Emma W

103

ฉันไม่เข้าใจว่าทำไมคำตอบจึงซับซ้อนที่นี่

นี่คือการปิด:

var a = 42;

function b() { return a; }

ใช่. คุณอาจใช้หลายครั้งต่อวัน


ไม่มีเหตุผลที่จะเชื่อว่าการปิดเป็นแฮ็คการออกแบบที่ซับซ้อนเพื่อแก้ไขปัญหาเฉพาะ ไม่การปิดเป็นเพียงการใช้ตัวแปรที่มาจากขอบเขตที่สูงกว่าจากมุมมองของที่ฟังก์ชันถูกประกาศ (ไม่ทำงาน)(ไม่ทำงาน)

ตอนนี้สิ่งที่ช่วยให้คุณสามารถทำได้น่าตื่นเต้นยิ่งขึ้นดูคำตอบอื่น ๆ


5
ดูเหมือนว่าคำตอบนี้จะไม่ช่วยให้ผู้อื่นคลายความสงสัย เทียบเท่าหยาบในภาษาการเขียนโปรแกรมแบบดั้งเดิมอาจจะมีการสร้างข () เป็นวิธีการบนวัตถุที่ยังaมีอย่างต่อเนื่องเอกชนหรือสถานที่ให้บริการ สำหรับความคิดของฉันสิ่งที่น่าประหลาดใจก็คือวัตถุขอบเขตของ JS มีaคุณสมบัติเป็นคุณสมบัติมากกว่าค่าคงที่อย่างมีประสิทธิภาพ และคุณจะสังเกตเห็นว่าพฤติกรรมที่สำคัญนั้นคือถ้าคุณปรับเปลี่ยนมันเช่นเดียวกับในreturn a++;
Jon Coombs

1
สิ่งที่จอนพูด ก่อนที่ฉันจะปิดตัวลงในที่สุดฉันก็ลำบากในการหาตัวอย่างที่ใช้งานได้จริง ใช่ floribon สร้างการปิด แต่เพื่อให้การศึกษาแก่ฉันสิ่งนี้จะไม่สอนอะไรฉันอย่างแน่นอน
Chev

3
นี่ไม่ได้กำหนดว่าการปิดคืออะไร - มันเป็นเพียงตัวอย่างที่ใช้ และไม่ได้บอกถึงความแตกต่างของสิ่งที่เกิดขึ้นเมื่อขอบเขตสิ้นสุดลง ฉันไม่คิดว่าใครมีคำถามเกี่ยวกับการกำหนดขอบเขตศัพท์เมื่อขอบเขตทั้งหมดยังคงอยู่และโดยเฉพาะอย่างยิ่งในกรณีของตัวแปรทั่วโลก
เจอราร์ดโอนีล

91

ตัวอย่างสำหรับจุดแรกโดย dlaliberte:

การปิดไม่ได้สร้างขึ้นเฉพาะเมื่อคุณส่งคืนฟังก์ชันภายใน ในความเป็นจริงฟังก์ชั่นการปิดล้อมไม่จำเป็นต้องกลับมาเลย คุณอาจกำหนดฟังก์ชันภายในของคุณให้กับตัวแปรในขอบเขตภายนอกหรือส่งผ่านเป็นอาร์กิวเมนต์ให้กับฟังก์ชันอื่นซึ่งสามารถใช้งานได้ทันที ดังนั้นการปิดฟังก์ชั่นการปิดล้อมอาจมีอยู่แล้วในเวลาที่เรียกใช้ฟังก์ชั่นการปิดล้อมเนื่องจากฟังก์ชั่นภายในใด ๆ สามารถเข้าใช้งานได้ทันทีที่มีการเรียกใช้

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

ความกระจ่างเล็กน้อยเกี่ยวกับความกำกวมที่เป็นไปได้ เมื่อฉันพูดว่า "อันที่จริงฟังก์ชั่นการปิดล้อมไม่จำเป็นต้องกลับเลย" ฉันไม่ได้แปลว่า "คืนค่าใด ๆ " แต่ "ยังทำงานอยู่" ดังนั้นตัวอย่างจะไม่แสดงภาพนั้นแม้ว่ามันจะแสดงอีกวิธีหนึ่งที่ฟังก์ชันภายในสามารถส่งผ่านไปยังขอบเขตด้านนอกได้ ประเด็นหลักที่ฉันพยายามทำคือเวลาของการสร้างการปิด (สำหรับฟังก์ชั่นการปิดล้อม) เนื่องจากบางคนดูเหมือนจะคิดว่ามันเกิดขึ้นเมื่อฟังก์ชั่นการล้อมรอบกลับมา ตัวอย่างที่แตกต่างกันจะต้องแสดงให้เห็นว่าการปิดจะถูกสร้างขึ้นเมื่อมีฟังก์ชั่นที่เรียกว่า
dlaliberte

88

การปิดคือที่ฟังก์ชันภายในมีการเข้าถึงตัวแปรในฟังก์ชันภายนอก นั่นอาจเป็นคำอธิบายแบบบรรทัดเดียวที่ง่ายที่สุดที่คุณจะได้รับเมื่อปิด


35
นั่นเป็นเพียงครึ่งหนึ่งของคำอธิบาย สิ่งสำคัญที่ควรทราบเกี่ยวกับการปิดคือถ้าฟังก์ชั่นด้านในยังคงถูกอ้างถึงหลังจากที่ฟังก์ชั่นด้านนอกได้ออกแล้วค่าเก่าของฟังก์ชั่นด้านนอกยังคงมีอยู่ในด้านใน
pcorcoran

22
ที่จริงแล้วมันไม่ได้เป็นค่าเก่าของฟังก์ชั่นด้านนอกที่มีให้กับฟังก์ชั่นภายใน แต่ตัวแปรเก่าซึ่งอาจมีค่าใหม่ถ้าฟังก์ชั่นบางอย่างสามารถเปลี่ยนพวกเขา
dlaliberte

86

ฉันรู้ว่ามีวิธีแก้ปัญหามากมายอยู่แล้ว แต่ฉันคิดว่าสคริปต์ขนาดเล็กและเรียบง่ายนี้มีประโยชน์ในการแสดงแนวคิด:

// 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

คุณนอนหลับแล้วและเชิญแดน คุณบอก Dan ให้นำ XBox คอนโทรลเลอร์มาหนึ่งตัว

แดนชวนพอล Dan ขอให้ Paul นำคอนโทรลเลอร์มาหนึ่งตัว มีผู้ควบคุมงานกี่คน?

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

ผู้เขียนClosuresได้อธิบายถึงการปิดเป็นอย่างดีอธิบายเหตุผลที่เราต้องการมันและอธิบาย LexicalEnvironment ซึ่งจำเป็นสำหรับการทำความเข้าใจการปิด
นี่คือบทสรุป:

เกิดอะไรขึ้นถ้าตัวแปรเข้าถึง แต่มันไม่ใช่แบบโลคอล? ชอบที่นี่:

ป้อนคำอธิบายภาพที่นี่

ในกรณีนี้ล่ามจะค้นหาตัวแปรในLexicalEnvironmentวัตถุด้านนอก

กระบวนการประกอบด้วยสองขั้นตอน:

  1. ก่อนอื่นเมื่อสร้างฟังก์ชัน f ฟังก์ชันนั้นจะไม่ถูกสร้างในพื้นที่ว่าง มีวัตถุ LexicalEnvironment ปัจจุบัน ในกรณีข้างต้นเป็นหน้าต่าง (a ไม่ได้ถูกกำหนดในเวลาที่สร้างฟังก์ชัน)

ป้อนคำอธิบายภาพที่นี่

เมื่อสร้างฟังก์ชั่นแล้วมันจะได้รับคุณสมบัติที่ซ่อนชื่อ [ขอบเขต] ซึ่งอ้างอิงถึง LexicalEnvironment ปัจจุบัน

ป้อนคำอธิบายภาพที่นี่

หากอ่านตัวแปร แต่ไม่สามารถพบได้ทุกที่จะเกิดข้อผิดพลาดขึ้น

ฟังก์ชั่นที่ซ้อนกัน

ฟังก์ชันสามารถซ้อนกันในอีกรูปแบบหนึ่งซึ่งก่อให้เกิดห่วงโซ่ของสภาพแวดล้อมของ LexicalEn ซึ่งสามารถเรียกว่าขอบเขตลูกโซ่

ป้อนคำอธิบายภาพที่นี่

ดังนั้นฟังก์ชั่น g สามารถเข้าถึง g, a และ f

ปิด

ฟังก์ชั่นที่ซ้อนกันอาจยังคงอยู่หลังจากฟังก์ชั่นด้านนอกเสร็จสิ้น:

ป้อนคำอธิบายภาพที่นี่

การทำเครื่องหมายสภาพแวดล้อมของ LexicalEn:

ป้อนคำอธิบายภาพที่นี่

อย่างที่เราเห็น this.sayเป็นคุณสมบัติในวัตถุผู้ใช้ดังนั้นมันจะยังคงอยู่หลังจากผู้ใช้เสร็จสมบูรณ์

และถ้าคุณจำเมื่อthis.sayถูกสร้างขึ้นมัน (เป็นทุกฟังก์ชั่น) ได้รับการอ้างอิงภายในthis.say.[[Scope]]กับ LexicalEnvironment ปัจจุบัน ดังนั้น LexicalEnvironment ของการดำเนินการของผู้ใช้ปัจจุบันยังคงอยู่ในหน่วยความจำ ตัวแปรทั้งหมดของผู้ใช้ยังเป็นคุณสมบัติของพวกเขาดังนั้นพวกเขาจึงถูกเก็บไว้อย่างระมัดระวังไม่ใช่ขยะปกติ

จุดทั้งหมดคือการตรวจสอบให้แน่ใจว่าหากฟังก์ชันภายในต้องการเข้าถึงตัวแปรด้านนอกในอนาคตจะสามารถทำได้

เพื่อสรุป:

  1. ฟังก์ชั่นด้านในใช้อ้างอิงถึง LexicalEnvironment ด้านนอก
  2. ฟังก์ชั่นภายในอาจเข้าถึงตัวแปรจากมันได้ตลอดเวลาแม้ว่าฟังก์ชั่นด้านนอกจะเสร็จสิ้น
  3. เบราว์เซอร์จะเก็บ LexicalEnvironment และคุณสมบัติทั้งหมด (ตัวแปร) ไว้ในหน่วยความจำจนกว่าจะมีฟังก์ชันภายในซึ่งอ้างอิงอยู่

นี่เรียกว่าการปิด


78

ฟังก์ชั่น JavaScript สามารถเข้าถึง:

  1. ข้อโต้แย้ง
  2. ชาวบ้าน (นั่นคือตัวแปรท้องถิ่นและฟังก์ชั่นท้องถิ่น)
  3. สภาพแวดล้อมซึ่งรวมถึง:
    • กลมรวมถึง DOM
    • ทุกอย่างในฟังก์ชั่นด้านนอก

หากฟังก์ชันเข้าถึงสภาพแวดล้อมของฟังก์ชันนั้นจะเป็นการปิด

โปรดทราบว่าไม่จำเป็นต้องใช้ฟังก์ชั่นภายนอกแม้ว่าจะให้ประโยชน์ที่ฉันไม่ได้กล่าวถึงที่นี่ โดยการเข้าถึงข้อมูลในสภาพแวดล้อมของมันการปิดทำให้ข้อมูลนั้นยังคงอยู่ ในส่วนย่อยของฟังก์ชั่นด้านนอก / ด้านในฟังก์ชั่นด้านนอกสามารถสร้างข้อมูลในท้องถิ่นและในที่สุดก็ออก แต่ถ้าฟังก์ชั่นภายในใด ๆ อยู่รอดหลังจากฟังก์ชั่นด้านนอกออกจากนั้นฟังก์ชั่นภายในเก็บข้อมูลท้องถิ่นของฟังก์ชั่นด้านนอก มีชีวิตอยู่

ตัวอย่างของการปิดที่ใช้สภาพแวดล้อมแบบโกลบอล:

ลองจินตนาการว่าเหตุการณ์ปุ่มสแต็ก Overflow Vote-Up และ Vote-Down ถูกนำไปใช้เป็นการปิด, voteUp_click และ voteDown_click ที่มีการเข้าถึงตัวแปรภายนอกคือ isVotedUp และ isVotedDown ซึ่งกำหนดไว้ทั่วโลก (เพราะความเรียบง่ายฉันหมายถึงปุ่มโหวตคำถามของ StackOverflow ไม่ใช่อาร์เรย์ของปุ่มตอบรับโหวต)

เมื่อผู้ใช้คลิกปุ่ม VoteUp ฟังก์ชั่นโหวตUp_clickจะตรวจสอบว่า isVotedDown == จริงเพื่อพิจารณาว่าจะลงคะแนนหรือยกเลิกการลงคะแนน ฟังก์ชัน voteUp_click เป็นการปิดเพราะกำลังเข้าถึงสภาพแวดล้อม

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
}

ฟังก์ชั่นทั้งสี่นี้จะปิดเมื่อพวกเขาเข้าถึงสภาพแวดล้อมของพวกเขา


59

ในฐานะพ่อของเด็กอายุ 6 ขวบที่กำลังสอนเด็กเล็ก (และผู้เริ่มหัดให้รหัสโดยไม่มีการศึกษาอย่างเป็นทางการดังนั้นจึงจำเป็นต้องมีการแก้ไข) ฉันคิดว่าบทเรียนจะดีที่สุดเมื่อเล่นด้วยมือ หากเด็กอายุ 6 ขวบพร้อมที่จะเข้าใจว่าการปิดนั้นคืออะไรพวกเขาก็อายุมากพอที่จะไปได้ด้วยตนเอง ฉันขอแนะนำให้วางรหัสลงใน jsfiddle.net อธิบายเล็กน้อยแล้วปล่อยให้พวกเขาอยู่คนเดียวเพื่อแต่งเพลงที่ไม่เหมือนใคร ข้อความอธิบายด้านล่างอาจเหมาะสำหรับเด็กอายุ 10 ปีขึ้นไป

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);

คำแนะนำ

DATA: Data เป็นการรวบรวมข้อเท็จจริง มันอาจเป็นตัวเลขคำพูดการวัดการสังเกตการณ์หรือแม้แต่การอธิบายสิ่งต่าง ๆ คุณไม่สามารถสัมผัสได้กลิ่นหรือลิ้มรส คุณสามารถจดบันทึกพูดและฟังได้ คุณสามารถใช้มันเพื่อสร้างกลิ่นสัมผัสและรสชาติโดยใช้คอมพิวเตอร์ คอมพิวเตอร์สามารถใช้ประโยชน์ได้โดยใช้รหัส

รหัสสินค้า: ทั้งหมดเขียนข้างต้นเรียกว่ารหัส มันเขียนใน JavaScript

JAVASCRIPT: JavaScript เป็นภาษา เช่นเดียวกับภาษาอังกฤษหรือภาษาฝรั่งเศสหรือภาษาจีน มีภาษามากมายที่คอมพิวเตอร์และหน่วยประมวลผลอิเล็กทรอนิกส์อื่นเข้าใจ สำหรับคอมพิวเตอร์ที่จะเข้าใจ JavaScript จะต้องมีล่าม ลองคิดดูว่าครูที่พูดภาษารัสเซียมาสอนในชั้นเรียนที่โรงเรียนหรือไม่ เมื่อครูพูดว่า "всесадятся" ชั้นเรียนจะไม่เข้าใจ แต่โชคดีที่คุณมีลูกศิษย์ชาวรัสเซียในชั้นเรียนของคุณที่บอกทุกคนว่านี่หมายความว่า "ทุกคนนั่งลง" - ดังนั้นคุณทุกคนทำ ชั้นเรียนเป็นเหมือนคอมพิวเตอร์และนักเรียนรัสเซียเป็นล่าม สำหรับ JavaScript ล่ามที่พบบ่อยที่สุดเรียกว่าเบราว์เซอร์

เบราว์เซอร์: เมื่อคุณเชื่อมต่ออินเทอร์เน็ตบนคอมพิวเตอร์แท็บเล็ตหรือโทรศัพท์เพื่อเยี่ยมชมเว็บไซต์คุณใช้เบราว์เซอร์ ตัวอย่างที่คุณอาจรู้จัก ได้แก่ Internet Explorer, Chrome, Firefox และ Safari เบราว์เซอร์สามารถเข้าใจ JavaScript และบอกคอมพิวเตอร์ว่าต้องทำอะไร คำสั่ง JavaScript เรียกว่าฟังก์ชั่น

ฟังก์ชั่น: ฟังก์ชั่นใน JavaScript เป็นเหมือนโรงงาน อาจเป็นโรงงานเล็ก ๆ ที่มีเครื่องจักรเพียงเครื่องเดียว หรืออาจมีโรงงานเล็ก ๆ หลายแห่งแต่ละแห่งมีเครื่องจักรจำนวนมากทำงานต่างกัน ในโรงงานผลิตเสื้อผ้าในชีวิตจริงคุณอาจมีรีมผ้าและกระสวยด้ายติดอยู่รวมทั้งเสื้อยืดและกางเกงยีนส์ โรงงาน JavaScript ของเราประมวลผลข้อมูลเท่านั้นไม่สามารถเย็บเจาะรูหรือละลายโลหะได้ ในข้อมูลโรงงาน JavaScript ของเราจะเข้าและออกมา

ข้อมูลทั้งหมดนี้ฟังดูน่าเบื่อ แต่มันเจ๋งมากจริงๆ เราอาจมีฟังก์ชั่นที่บอกหุ่นยนต์ว่าจะทำอะไรสำหรับอาหารค่ำ สมมติว่าฉันเชิญคุณและเพื่อนของคุณมาที่บ้าน คุณชอบขาไก่ที่ดีที่สุดฉันชอบไส้กรอกเพื่อนของคุณต้องการสิ่งที่คุณต้องการเสมอและเพื่อนของฉันไม่กินเนื้อสัตว์

ฉันไม่มีเวลาไปซื้อของดังนั้นฟังก์ชั่นต้องรู้ว่าเรามีอะไรในตู้เย็นเพื่อการตัดสินใจ ส่วนผสมแต่ละอย่างมีเวลาทำอาหารที่แตกต่างกันและเราต้องการให้ทุกอย่างเสิร์ฟร้อนโดยหุ่นยนต์ในเวลาเดียวกัน เราต้องให้ข้อมูลเกี่ยวกับสิ่งที่เราต้องการฟังก์ชั่นฟังก์ชั่นสามารถ 'พูดคุย' กับตู้เย็นและฟังก์ชั่นสามารถควบคุมหุ่นยนต์ได้

โดยปกติฟังก์ชั่นจะมีชื่อวงเล็บและเครื่องหมายปีกกา แบบนี้:

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

โปรดทราบว่า/*...*/และ//รหัสหยุดกำลังถูกอ่านโดยเบราว์เซอร์

ชื่อ: คุณสามารถเรียกใช้ฟังก์ชันเกี่ยวกับคำที่คุณต้องการ ตัวอย่าง "cookMeal" เป็นเรื่องปกติในการรวมคำสองคำเข้าด้วยกันและให้ตัวอักษรตัวที่สองที่จุดเริ่มต้น - แต่ไม่จำเป็น มันไม่มีที่ว่างในนั้นและมันก็ไม่สามารถเป็นตัวเลขได้ด้วยตัวเอง

PARENTHESES: "วงเล็บ" หรือ()เป็นกล่องจดหมายที่ประตูโรงงานของฟังก์ชัน JavaScript หรือกล่องไปรษณีย์บนถนนเพื่อส่งแพ็กเก็ตข้อมูลไปยังโรงงาน บางครั้งตู้ไปรษณีย์อาจถูกทำเครื่องหมายตัวอย่างเช่น cookMeal(you, me, yourFriend, myFriend, fridge, dinnerTime)ในกรณีนี้คุณรู้ว่าคุณต้องให้ข้อมูลอะไร

พื้น: "เครื่องมือจัดฟัน" ซึ่งมีลักษณะเช่นนี้{}เป็นหน้าต่างย้อมสีของโรงงานของเรา จากภายในโรงงานคุณสามารถมองเห็นได้ แต่จากภายนอกคุณไม่สามารถมองเห็นได้

ตัวอย่างโค้ดยาว ๆ ข้างบน

รหัสของเราเริ่มต้นด้วยฟังก์ชั่นคำดังนั้นเราจึงรู้ว่ามันเป็นหนึ่ง! จากนั้นชื่อของฟังก์ชั่นร้องเพลง - นั่นคือคำอธิบายของฉันเองเกี่ยวกับฟังก์ชั่นนี้ วงเล็บแล้ว() วงเล็บจะมีอยู่เสมอสำหรับฟังก์ชัน (person)บางครั้งพวกเขาจะว่างเปล่าและบางครั้งพวกเขามีสิ่งหนึ่งในนี้มีคำใน. {หลังจากนี้จะมีการรั้งเช่นนี้ เครื่องหมายนี้เริ่มต้นของการทำงานร้องเพลง () มีพันธมิตรที่ทำเครื่องหมายจุดจบของการร้องเพลง ()เช่นนี้}

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

ดังนั้นฟังก์ชั่นนี้อาจมีบางอย่างเกี่ยวกับการร้องเพลงและอาจต้องการข้อมูลบางอย่างเกี่ยวกับบุคคล มีคำแนะนำในการทำบางสิ่งกับข้อมูลนั้น

ตอนนี้หลังจากฟังก์ชั่นร้องเพลง ()ใกล้ถึงจุดสิ้นสุดของโค้ดคือบรรทัด

var person="an old lady";

ตัวแปร: ตัวอักษรvar หมายถึง "variable" ตัวแปรเป็นเหมือนซองจดหมาย ด้านนอกซองจดหมายนี้มีการทำเครื่องหมาย "บุคคล" ด้านในประกอบด้วยกระดาษที่มีข้อมูลที่เราต้องการฟังก์ชั่นตัวอักษรและช่องว่างบางตัวเข้าด้วยกันเหมือนสตริง (เรียกว่าสตริง) ซึ่งทำให้วลีที่อ่านว่า ซองจดหมายของเราอาจมีสิ่งอื่น ๆ เช่นตัวเลข (เรียกว่าจำนวนเต็ม) คำแนะนำ (เรียกว่าฟังก์ชั่น) รายการ (เรียกว่าอาร์เรย์ ) เนื่องจากตัวแปรนี้ถูกเขียนภายนอกวงเล็บปีกกาทั้งหมด{}และเนื่องจากคุณสามารถมองเห็นผ่านหน้าต่างสีเมื่อคุณอยู่ในวงเล็บปีกกาตัวแปรนี้สามารถมองเห็นได้จากที่ใดก็ได้ในรหัส เราเรียกสิ่งนี้ว่า 'ตัวแปรทั่วโลก'

ตัวแปรทั่วโลก: คนเป็นตัวแปรทั่วโลกซึ่งหมายความว่าถ้าคุณเปลี่ยนค่าจาก "หญิงชรา" เป็น "ชายหนุ่ม" คนจะยังคงเป็นชายหนุ่มจนกว่าคุณจะตัดสินใจเปลี่ยนอีกครั้งและฟังก์ชั่นอื่น ๆ ใน รหัสสามารถเห็นได้ว่ามันเป็นชายหนุ่ม กดF12ปุ่มหรือดูการตั้งค่าตัวเลือกเพื่อเปิดคอนโซลนักพัฒนาซอฟต์แวร์ของเบราว์เซอร์และพิมพ์ "คน" เพื่อดูว่าค่านี้คืออะไร พิมพ์person="a young man"เพื่อเปลี่ยนแล้วพิมพ์ "person" อีกครั้งเพื่อดูว่ามีการเปลี่ยนแปลง

หลังจากนี้เรามีสาย

sing(person);

บรรทัดนี้กำลังเรียกใช้ฟังก์ชั่นราวกับว่ามันกำลังเรียกสุนัข

"มาร้องเพลงมารับคน !"

เมื่อเบราว์เซอร์โหลดรหัส JavaScript ถึงบรรทัดนี้มันจะเริ่มทำงาน ฉันวางสายในตอนท้ายเพื่อให้แน่ใจว่าเบราว์เซอร์มีข้อมูลทั้งหมดที่จำเป็นในการเรียกใช้

ฟังก์ชั่นกำหนดการกระทำ - ฟังก์ชั่นหลักคือเกี่ยวกับการร้องเพลง มันมีตัวแปรที่เรียกว่าfirstPartซึ่งใช้กับการร้องเพลงเกี่ยวกับบุคคลที่ใช้กับแต่ละบทของเพลง: "มี" + คน + "ที่กลืนกิน" หากคุณพิมพ์firstPartลงในคอนโซลคุณจะไม่ได้รับคำตอบเนื่องจากตัวแปรนั้นถูกล็อคไว้ในฟังก์ชั่นเพราะเบราว์เซอร์จะไม่สามารถมองเห็นหน้าต่างด้านในของวงเล็บปีกกาได้

การปิด: การปิดคือฟังก์ชั่นขนาดเล็กที่อยู่ภายในฟังก์ชั่นร้องเพลงใหญ่() โรงงานเล็ก ๆ ในโรงงานขนาดใหญ่ พวกเขาแต่ละคนมีเครื่องหมายปีกกาของตัวเองซึ่งหมายความว่าตัวแปรภายในพวกเขาไม่สามารถมองเห็นได้จากภายนอก นั่นเป็นเหตุผลที่ชื่อของตัวแปร ( สิ่งมีชีวิตและผลลัพธ์ ) สามารถทำซ้ำได้ในการปิด แต่มีค่าต่างกัน หากคุณพิมพ์ชื่อตัวแปรเหล่านี้ในหน้าต่างคอนโซลคุณจะไม่ได้รับค่าเนื่องจากถูกซ่อนโดยหน้าต่างสองสี

การปิดทุกคนรู้ว่าตัวแปรของฟังก์ชั่นsing () ที่เรียกว่าfirstPartคืออะไรเพราะพวกเขาสามารถมองเห็นได้จากหน้าต่างสีอ่อนของพวกเขา

หลังจากปิดมาบรรทัด

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

ฟังก์ชั่น sing () จะเรียกแต่ละฟังก์ชั่นเหล่านี้ตามลำดับที่ได้รับ จากนั้นงานของ sing () จะเสร็จสิ้น


56

โอเคพูดกับเด็กอายุ 6 ขวบฉันอาจจะใช้ความสัมพันธ์ดังต่อไปนี้

ลองนึกภาพ - คุณกำลังเล่นกับน้องชายและน้องสาวของคุณในบ้านทั้งหมดและคุณกำลังเคลื่อนไปรอบ ๆ กับของเล่นของคุณและนำพวกเขาบางส่วนเข้าไปในห้องพี่ชายของคุณ หลังจากนั้นครู่หนึ่งพี่ชายของคุณกลับมาจากโรงเรียนและไปที่ห้องของเขาและเขาขังอยู่ข้างในดังนั้นตอนนี้คุณไม่สามารถเข้าถึงของเล่นที่เหลืออยู่ในทางตรง แต่คุณสามารถเคาะประตูและขอของเล่นจากพี่ชายของคุณ นี้เรียกว่าของเล่นปิด ; พี่ชายของคุณทำมันขึ้นมาสำหรับคุณและเขาอยู่ในขณะนี้เป็นนอกขอบเขต

เปรียบเทียบกับสถานการณ์เมื่อประตูถูกล็อคโดยร่างและไม่มีใครอยู่ข้างใน (การใช้งานฟังก์ชั่นทั่วไป) จากนั้นมีไฟในท้องถิ่นเกิดขึ้นและเผาห้อง (ผู้เก็บขยะ: D) จากนั้นห้องใหม่ก็ถูกสร้างขึ้น ของเล่นอื่นที่นั่น (อินสแตนซ์ของฟังก์ชันใหม่) แต่ไม่เคยได้ของเล่นเดิมที่เหลืออยู่ในห้องแรก

สำหรับเด็กขั้นสูงฉันจะใส่อะไรต่อไปนี้ มันไม่สมบูรณ์แบบ แต่มันทำให้คุณรู้สึกว่ามันคืออะไร:

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

อย่างที่คุณเห็นของเล่นที่เหลืออยู่ในห้องนั้นยังสามารถเข้าถึงได้ผ่านทางพี่ชายและไม่ว่าห้องจะถูกล็อคหรือไม่ นี่คือjsbinเพื่อเล่นกับมัน


49

คำตอบสำหรับเด็กอายุหกขวบ (สมมติว่าเขารู้ว่าฟังก์ชันคืออะไรและตัวแปรคืออะไรและข้อมูลอะไร):

ฟังก์ชั่นสามารถส่งคืนข้อมูล ข้อมูลประเภทหนึ่งที่คุณสามารถส่งคืนจากฟังก์ชั่นคือฟังก์ชั่นอื่น เมื่อฟังก์ชั่นใหม่นั้นได้รับคืนมาตัวแปรและอาร์กิวเมนต์ทั้งหมดที่ใช้ในฟังก์ชั่นที่สร้างมันจะไม่หายไป ฟังก์ชันหลักนั้น "ปิด" แทน กล่าวอีกนัยหนึ่งไม่มีอะไรที่สามารถดูได้จากภายในและดูตัวแปรที่ใช้ยกเว้นฟังก์ชั่นที่ส่งคืน ฟังก์ชั่นใหม่นั้นมีความสามารถพิเศษในการมองย้อนกลับไปข้างในฟังก์ชั่นที่สร้างขึ้นและดูข้อมูลที่อยู่ภายใน

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

อีกวิธีที่ง่ายมากที่จะอธิบายในแง่ของขอบเขต:

ทุกครั้งที่คุณสร้างขอบเขตที่เล็กกว่าภายในขอบเขตที่ใหญ่กว่าขอบเขตที่เล็กกว่าจะสามารถมองเห็นสิ่งที่อยู่ในขอบเขตที่ใหญ่กว่าเสมอ


49

ฟังก์ชั่นใน JavaScript ไม่ได้เป็นเพียงการอ้างอิงไปยังชุดคำสั่ง (เช่นเดียวกับในภาษา C) แต่ยังรวมถึงโครงสร้างข้อมูลที่ซ่อนอยู่ซึ่งประกอบด้วยการอ้างอิงถึงตัวแปรที่ไม่ใช่ท้องถิ่นทั้งหมดที่ใช้ (ตัวแปรที่ถูกจับ) ฟังก์ชั่นสองชิ้นดังกล่าวเรียกว่าการปิด ทุกฟังก์ชั่นใน JavaScript ถือได้ว่าเป็นการปิด

การปิดเป็นฟังก์ชันที่มีสถานะ มันค่อนข้างคล้ายกับ "นี่" ในแง่ที่ว่า "นี่" ยังมีสถานะสำหรับฟังก์ชั่น แต่ฟังก์ชั่นและ "นี้" เป็นวัตถุที่แยกจากกัน ("นี่" เป็นเพียงพารามิเตอร์แฟนซีและวิธีเดียวที่จะผูกมันถาวรกับ ฟังก์ชั่นคือการสร้างการปิด) ในขณะที่ "นี่" และฟังก์ชั่นอยู่เสมอแยกจากกันฟังก์ชั่นไม่สามารถแยกออกจากการปิดของมันและภาษาไม่ได้หมายถึงการเข้าถึงตัวแปรที่บันทึกไว้

เนื่องจากตัวแปรภายนอกทั้งหมดเหล่านี้ที่อ้างอิงโดยฟังก์ชันที่ซ้อนกัน lexically เป็นตัวแปรท้องถิ่นในห่วงโซ่ของฟังก์ชั่นที่ล้อมรอบ lexically (ตัวแปรทั่วโลกสามารถสันนิษฐานว่าเป็นตัวแปรท้องถิ่นของฟังก์ชั่นรากบางส่วน) และการดำเนินการเดียวทุกครั้ง ตัวแปรในตัวของมันจะตามมาด้วยการประมวลผลของฟังก์ชั่นที่ส่งคืน (หรือถ่ายโอนออกไปเช่นการลงทะเบียนเป็นการโทรกลับ) ฟังก์ชั่นที่ซ้อนกันสร้างการปิดใหม่ (ด้วยชุดตัวแปร nonlocal ที่อ้างอิงเฉพาะ บริบท).

นอกจากนี้จะต้องเข้าใจว่าตัวแปรโลคอลใน JavaScript ไม่ได้ถูกสร้างขึ้นบนสแต็กเฟรม แต่บนฮีปและทำลายเฉพาะเมื่อไม่มีใครอ้างถึง เมื่อฟังก์ชันส่งคืนการอ้างอิงถึงตัวแปรโลคัลของมันจะลดลง แต่พวกเขายังคงไม่เป็นโมฆะหากในระหว่างการดำเนินการปัจจุบันพวกเขากลายเป็นส่วนหนึ่งของการปิดและยังคงถูกอ้างอิงโดยฟังก์ชันที่ซ้อนกันของ lexically (ซึ่งสามารถเกิดขึ้นได้ ฟังก์ชั่นที่ซ้อนกันเหล่านี้ถูกส่งคืนหรือโอนไปยังรหัสภายนอกบางส่วน)

ตัวอย่าง:

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

บางทีอาจจะเกินกว่าทุกสิ่งทุกอย่าง แต่เป็นเรื่องของวัยหกขวบที่สุด แต่เป็นตัวอย่างเล็ก ๆ น้อย ๆ ที่ช่วยทำให้แนวคิดของการปิดใน JavaScript คลิกสำหรับฉัน

การปิดคือฟังก์ชันที่เข้าถึงขอบเขตของฟังก์ชันอื่น (ตัวแปรและฟังก์ชัน) วิธีที่ง่ายที่สุดในการสร้างการปิดคือด้วยฟังก์ชั่นภายในฟังก์ชั่น; เหตุผลที่ใน JavaScript ฟังก์ชันจะสามารถเข้าถึงขอบเขตของฟังก์ชันที่มีอยู่ได้เสมอ

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

outerFunction();

ALERT: ลิง

ในตัวอย่างข้างต้น outerFunction เรียกว่าซึ่งจะเรียก InnerFunction โปรดสังเกตว่า outerVar พร้อมใช้งานกับ InnerFunction ซึ่งพิสูจน์ได้ด้วยการแจ้งเตือนค่าของ outerVar อย่างถูกต้อง

ตอนนี้ให้พิจารณาสิ่งต่อไปนี้:

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

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

ALERT: ลิง

referenceToInnerFunction ถูกตั้งค่าเป็น outerFunction () ซึ่งจะส่งคืนการอ้างอิงไปยัง InnerFunction เพียงอย่างเดียว เมื่อ referenceToInnerFunction ถูกเรียกมันจะส่งคืน outerVar อีกครั้งดังกล่าวข้างต้นนี้แสดงให้เห็นว่า innerFunction มีการเข้าถึง outerVar ซึ่งเป็นตัวแปรของ outerFunction ยิ่งไปกว่านั้นมันเป็นเรื่องที่น่าสนใจที่จะทราบว่ามันยังคงสามารถเข้าถึงได้แม้หลังจาก outerFunction ดำเนินการเสร็จสิ้นแล้ว

และนี่คือสิ่งที่น่าสนใจมาก ๆ หากเราต้องกำจัด outerFunction ให้ตั้งค่าเป็น null คุณอาจคิดว่าการอ้างอิง ToInnerFunction จะทำให้การเข้าถึงค่า outerVar หลวม แต่นี่ไม่ใช่กรณี

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

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

outerFunction = null;
alert(referenceToInnerFunction());

ALERT: Monkey ALERT: Monkey

แต่สิ่งนี้เป็นอย่างไร ReferenceToInnerFunction จะยังคงทราบค่าของ outerVar ได้อย่างไรในขณะที่ outerFunction ได้ถูกตั้งค่าเป็น null?

เหตุผลที่ ReferenceToInnerFunction ยังคงสามารถเข้าถึงค่าของ outerVar ได้เพราะเมื่อการปิดถูกสร้างขึ้นครั้งแรกโดยการวาง InnerFunction ภายในของ outerFunction, InnerFunction เพิ่มการอ้างอิงถึงขอบเขตของ outerFunction (ตัวแปรและฟังก์ชั่น) ไปยังห่วงโซ่ขอบเขต สิ่งนี้หมายความว่า innerFunction มีตัวชี้หรือการอ้างอิงถึงตัวแปรของ outerFunction ทั้งหมดรวมถึง outerVar ดังนั้นเมื่อ outerFunction เสร็จสิ้นการดำเนินการหรือแม้ว่าจะถูกลบหรือตั้งค่าเป็นโมฆะตัวแปรในขอบเขตของมันเช่น outerVar ติดรอบในหน่วยความจำเพราะการอ้างอิงที่โดดเด่นพวกเขาในส่วนของ InnerFunction ที่ได้กลับไป referenceToInnerFunction ในการปล่อย outerVar และตัวแปรส่วนที่เหลือของ outerFunction ออกจากหน่วยความจำอย่างแท้จริงคุณจะต้องกำจัดการอ้างอิงที่โดดเด่นนี้ให้กับพวกเขา

//////////

อีกสองเรื่องเกี่ยวกับการปิดที่จะต้องทราบ ก่อนอื่นการปิดจะสามารถเข้าถึงค่าสุดท้ายของฟังก์ชันที่มีอยู่ได้เสมอ

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

    innerFunction();
}

outerFunction();

ALERT: กอริลลา

ประการที่สองเมื่อมีการสร้างการปิดระบบจะเก็บการอ้างอิงไปยังตัวแปรและฟังก์ชั่นการปิดล้อมทั้งหมด มันไม่ได้เลือกและเลือก และอย่างนั้นการปิดควรใช้เท่าที่จำเป็นหรืออย่างน้อยที่สุดอย่างระมัดระวังเนื่องจากสามารถใช้หน่วยความจำมาก ตัวแปรจำนวนมากสามารถเก็บไว้ในหน่วยความจำได้นานหลังจากฟังก์ชั่นที่บรรจุเสร็จสิ้นการดำเนินการ


45

ฉันก็จะชี้ไปยังหน้า Mozilla ปิด มันเป็นคำอธิบายที่ดีที่สุดกระชับและเรียบง่ายที่สุดเกี่ยวกับพื้นฐานการปิดและการใช้งานจริงที่ฉันได้พบ ขอแนะนำสำหรับทุกคนที่เรียนรู้ JavaScript

และใช่ฉันยังแนะนำให้กับเด็กอายุ 6 ปีด้วยถ้าเด็กอายุ 6 ขวบกำลังเรียนรู้เกี่ยวกับการปิดประตูมันก็มีเหตุผลที่พวกเขาพร้อมที่จะเข้าใจคำอธิบายที่กระชับและง่ายในบทความ


ฉันเห็นด้วย: หน้า Mozilla กล่าวว่าง่ายและกระชับเป็นพิเศษ โพสต์ของคุณน่าแปลกใจมากพอที่คนอื่น ๆ
Brice Coustillas
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.