เมื่อส่งเมธอด object เป็นการเรียกกลับ เช่น setTimeout
มีปัญหาที่ทราบ: “losing this
”
ในบทนี้เราจะดูวิธีการแก้ไข
เราได้เห็นตัวอย่างของการสูญเสีย this
แล้ว เมื่อส่งเมธอดไปที่ใดที่หนึ่งแยกจากอ็อบเจ็กต์ this
จะหายไป
นี่คือวิธีการที่อาจเกิดขึ้นกับ setTimeout
:
ให้ผู้ใช้ = { ชื่อจริง: "จอห์น", พูดสวัสดี() { alert(`สวัสดี ${this.firstName}!`); - - setTimeout(user.sayHi, 1,000); // สวัสดี ไม่ได้กำหนด!
ดังที่เราเห็น ผลลัพธ์ไม่ได้แสดง "John" เป็น this.firstName
แต่ undefined
!
นั่นเป็นเพราะว่า setTimeout
มีฟังก์ชัน user.sayHi
แยกจากอ็อบเจ็กต์ บรรทัดสุดท้ายสามารถเขียนใหม่เป็น:
ให้ f = user.sayHi; setTimeout(f, 1,000); // สูญเสียบริบทของผู้ใช้
เมธอด setTimeout
ในเบราว์เซอร์นั้นมีความพิเศษเล็กน้อย โดยจะตั้งค่า this=window
สำหรับการเรียกใช้ฟังก์ชัน (สำหรับ Node.js this
จะกลายเป็นอ็อบเจ็กต์ตัวจับเวลา แต่ไม่สำคัญที่นี่) ดังนั้นสำหรับ this.firstName
มันจะพยายามรับ window.firstName
ซึ่งไม่มีอยู่จริง ในกรณีอื่นที่คล้ายคลึงกัน โดยปกติจะ this
เป็น undefined
งานค่อนข้างปกติ - เราต้องการส่งวิธีวัตถุไปที่อื่น (ที่นี่ - ไปยังตัวกำหนดเวลา) ซึ่งจะถูกเรียก จะแน่ใจได้อย่างไรว่าจะถูกเรียกในบริบทที่ถูกต้อง?
วิธีแก้ปัญหาที่ง่ายที่สุดคือการใช้ฟังก์ชันการห่อ:
ให้ผู้ใช้ = { ชื่อจริง: "จอห์น", พูดสวัสดี() { alert(`สวัสดี ${this.firstName}!`); - - setTimeout (ฟังก์ชัน () { user.sayHi(); // สวัสดีจอห์น! }, 1,000);
ตอนนี้ใช้งานได้แล้ว เนื่องจากได้รับ user
จากสภาพแวดล้อมคำศัพท์ภายนอก แล้วจึงเรียกใช้เมธอดตามปกติ
เหมือนกัน แต่สั้นกว่า:
setTimeout(() => user.sayHi(), 1,000); // สวัสดีจอห์น!
ดูดี แต่มีช่องโหว่เล็กน้อยปรากฏในโครงสร้างโค้ดของเรา
จะเกิดอะไรขึ้นถ้าก่อนที่ setTimeout
ทริกเกอร์ (มีความล่าช้าหนึ่งวินาที!) user
เปลี่ยนค่า แล้วจู่ๆ มันก็จะเรียกวัตถุผิด!
ให้ผู้ใช้ = { ชื่อจริง: "จอห์น", พูดสวัสดี() { alert(`สวัสดี ${this.firstName}!`); - - setTimeout(() => user.sayHi(), 1,000); // ...ค่าของผู้ใช้เปลี่ยนแปลงภายใน 1 วินาที ผู้ใช้ = { sayHi() { alert("ผู้ใช้รายอื่นใน setTimeout!"); - - // ผู้ใช้รายอื่นใน setTimeout!
แนวทางแก้ไขถัดไปรับประกันได้ว่าสิ่งนั้นจะไม่เกิดขึ้น
ฟังก์ชั่นจัดให้มีการผูกวิธีการในตัวที่ช่วยให้สามารถแก้ไข this
ได้
ไวยากรณ์พื้นฐานคือ:
// ไวยากรณ์ที่ซับซ้อนมากขึ้นจะมาในภายหลังเล็กน้อย ให้ boundFunc = func.bind(บริบท);
ผลลัพธ์ของ func.bind(context)
คือ "วัตถุแปลกใหม่" ที่มีลักษณะคล้ายฟังก์ชันพิเศษ ซึ่งสามารถเรียกใช้เป็นฟังก์ชันได้ และผ่านการเรียกไปยังการตั้งค่า func
this=context
อย่างโปร่งใส
กล่าวอีกนัยหนึ่งการเรียก boundFunc
ก็เหมือนกับ func
ที่แก้ไข this
ตัวอย่างเช่น ที่นี่ funcUser
ผ่านการเรียกไปยัง func
ด้วย this=user
:
ให้ผู้ใช้ = { ชื่อจริง: "จอห์น" - ฟังก์ชั่น func() { การแจ้งเตือน (this.firstName); - ให้ funcUser = func.bind(ผู้ใช้); funcUser(); // จอห์น
ที่นี่ func.bind(user)
เป็น "ตัวแปรที่ถูกผูกไว้" ของ func
พร้อมแก้ไข this=user
อาร์กิวเมนต์ทั้งหมดจะถูกส่งผ่านไปยัง func
ดั้งเดิม "ตามสภาพ" เช่น:
ให้ผู้ใช้ = { ชื่อจริง: "จอห์น" - ฟังก์ชั่น func (วลี) { การแจ้งเตือน (วลี + ', ' + this.firstName); - // ผูกสิ่งนี้กับผู้ใช้ ให้ funcUser = func.bind(ผู้ใช้); funcUser("สวัสดี"); // สวัสดี จอห์น (ผ่านการโต้แย้ง "สวัสดี" และนี่คือ=ผู้ใช้)
ตอนนี้เรามาลองใช้วิธี object กัน:
ให้ผู้ใช้ = { ชื่อจริง: "จอห์น", พูดสวัสดี() { alert(`สวัสดี ${this.firstName}!`); - - ให้ sayHi = user.sayHi.bind(user); - // สามารถรันได้โดยไม่ต้องมีวัตถุ พูดว่าสวัสดี(); // สวัสดีจอห์น! setTimeout (สวัสดี 1,000); // สวัสดีจอห์น! // แม้ว่ามูลค่าของผู้ใช้จะเปลี่ยนภายใน 1 วินาทีก็ตาม // sayHi ใช้ค่าที่ถูกผูกไว้ล่วงหน้าซึ่งอ้างอิงถึงอ็อบเจ็กต์ผู้ใช้เก่า ผู้ใช้ = { sayHi() { alert("ผู้ใช้รายอื่นใน setTimeout!"); - -
ในบรรทัด (*)
เราใช้วิธี user.sayHi
และผูกไว้กับ user
sayHi
เป็นฟังก์ชัน "ผูกมัด" ซึ่งสามารถเรียกเพียงอย่างเดียวหรือส่งผ่านไปยัง setTimeout
ได้ ไม่สำคัญหรอก บริบทจะถูกต้อง
ที่นี่เราจะเห็นว่าอาร์กิวเมนต์ถูกส่งผ่าน "ตามสภาพ" เฉพาะ this
เท่านั้นที่ได้รับการแก้ไขโดย bind
:
ให้ผู้ใช้ = { ชื่อจริง: "จอห์น", พูด (วลี) { alert(`${phrase}, ${this.firstName}!`); - - สมมติว่า = user.say.bind (ผู้ใช้); พูด ("สวัสดี"); // สวัสดีจอห์น! (อาร์กิวเมนต์ "สวัสดี" ถูกส่งผ่านเพื่อพูด) พูด ("ลาก่อน"); // บาย จอห์น! ("ลาก่อน" เป็นการส่งผ่านเพื่อพูด)
วิธีที่สะดวก: bindAll
หาก Object มีหลายวิธีและเราวางแผนที่จะส่งต่อมันไปเรื่อยๆ เราก็สามารถผูกมันทั้งหมดไว้ในลูปได้:
สำหรับ (ให้ป้อนผู้ใช้) { ถ้า (ประเภทของผู้ใช้ [คีย์] == 'ฟังก์ชั่น') { ผู้ใช้ [คีย์] = ผู้ใช้ [คีย์] .bind (ผู้ใช้); - -
ไลบรารี JavaScript ยังมีฟังก์ชันสำหรับการรวมมวลที่สะดวก เช่น _.bindAll(object, methodNames) ใน lodash
จนถึงตอนนี้เราแค่พูดถึงการผูกมัด this
เท่านั้น ก้าวไปอีกขั้นหนึ่ง
เราไม่เพียงแต่สามารถผูก this
แต่ยังรวมถึงข้อโต้แย้งด้วย ไม่ค่อยได้ทำ แต่บางครั้งก็มีประโยชน์
ไวยากรณ์แบบเต็มของ bind
:
ให้ผูกไว้ = func.bind(บริบท, [arg1], [arg2], ...);
อนุญาตให้ผูกบริบทเช่น this
และอาร์กิวเมนต์เริ่มต้นของฟังก์ชัน
ตัวอย่างเช่น เรามีฟังก์ชันการคูณ mul(a, b)
:
ฟังก์ชั่น mul(a, b) { ส่งคืน * b; -
ลองใช้ bind
เพื่อสร้างฟังก์ชัน double
บนฐานของมัน:
ฟังก์ชั่น mul(a, b) { ส่งคืน * b; - ให้ double = mul.bind(null, 2); การแจ้งเตือน ( สองเท่า(3) ); // = มูล(2, 3) = 6 การแจ้งเตือน ( สองเท่า(4) ); // = มูล(2, 4) = 8 การแจ้งเตือน( สองเท่า(5) ); // = มูล(2, 5) = 10
การเรียกไปยัง mul.bind(null, 2)
จะสร้างฟังก์ชันใหม่ double
ที่ส่งผ่านการโทรไปยัง mul
โดยแก้ไข null
เป็นบริบทและ 2
เป็นอาร์กิวเมนต์แรก ข้อโต้แย้งเพิ่มเติมจะถูกส่งผ่าน "ตามสภาพ"
นั่นเรียกว่าแอปพลิเคชันฟังก์ชันบางส่วน – เราสร้างฟังก์ชันใหม่โดยแก้ไขพารามิเตอร์บางตัวของฟังก์ชันที่มีอยู่
โปรดทราบว่าจริงๆ แล้วเราไม่ได้ใช้ this
ที่นี่ แต่ bind
ต้องการมัน ดังนั้นเราต้องใส่สิ่งที่ต้องการ null
ฟังก์ชัน triple
ในโค้ดด้านล่างจะเพิ่มค่าเป็นสามเท่า:
ฟังก์ชั่น mul(a, b) { ส่งคืน * b; - ให้ triple = mul.bind(null, 3); การแจ้งเตือน ( สามเท่า(3) ); // = มูล(3, 3) = 9 การแจ้งเตือน ( สามเท่า(4) ); // = มูล(3, 4) = 12 การแจ้งเตือน ( สามเท่า(5) ); // = มูล(3, 5) = 15
ทำไมเราถึงสร้างฟังก์ชันบางส่วน?
ข้อดีคือเราสามารถสร้างฟังก์ชันอิสระที่มีชื่อที่อ่านได้ ( double
, triple
) เราสามารถใช้ได้และไม่ต้องระบุอาร์กิวเมนต์แรกทุกครั้งเนื่องจากได้รับการแก้ไขด้วย bind
ในกรณีอื่นๆ แอปพลิเคชันบางส่วนจะมีประโยชน์เมื่อเรามีฟังก์ชันทั่วไปและต้องการฟังก์ชันที่เป็นสากลน้อยกว่าเพื่อความสะดวก
ตัวอย่างเช่น เรามีฟังก์ชัน send(from, to, text)
จากนั้น ภายในวัตถุ user
เราอาจต้องการใช้ตัวแปรบางส่วน: sendTo(to, text)
ที่ส่งจากผู้ใช้ปัจจุบัน
จะเป็นอย่างไรหากเราต้องการแก้ไขข้อโต้แย้งบางอย่าง แต่ไม่ใช่บริบท this
ตัวอย่างเช่น สำหรับวิธีการวัตถุ
bind
เนทิฟไม่อนุญาตให้ทำเช่นนั้น เราไม่สามารถละเว้นบริบทและข้ามไปยังข้อโต้แย้งได้
โชคดีที่ฟังก์ชัน partial
สำหรับการผูกอาร์กิวเมนต์เท่านั้นสามารถนำไปใช้ได้อย่างง่ายดาย
แบบนี้:
ฟังก์ชั่นบางส่วน (func, ...argsBound) { ฟังก์ชั่นส่งคืน (...args) { // (*) return func.call(นี่, ...argsBound, ...args); - - // การใช้งาน: ให้ผู้ใช้ = { ชื่อจริง: "จอห์น", พูด (เวลา, วลี) { alert(`[${time}] ${this.firstName}: ${phrase}!`); - - // เพิ่มวิธีการบางส่วนพร้อมเวลาที่กำหนด user.sayNow = บางส่วน (user.say, วันที่ใหม่ ().getHours() + ':' + วันที่ใหม่ ().getMinutes()); user.sayNow("สวัสดี"); // บางอย่างเช่น: // [10:00] จอห์น: สวัสดี!
ผลลัพธ์ของการเรียก partial(func[, arg1, arg2...])
คือ wrapper (*)
ที่เรียก func
ด้วย:
เช่น this
กับที่ได้รับ (สำหรับ user.sayNow
เรียกว่าเป็น user
)
จากนั้นให้ ...argsBound
– อาร์กิวเมนต์จากการโทร partial
( "10:00"
)
จากนั้นให้ ...args
– อาร์กิวเมนต์ที่กำหนดให้กับ wrapper ( "Hello"
)
มันง่ายมากที่จะทำด้วยไวยากรณ์การแพร่กระจายใช่ไหม?
นอกจากนี้ยังมี _.การใช้งานบางส่วนที่พร้อมจากไลบรารี Lodash
เมธอด func.bind(context, ...args)
ส่งคืน "ตัวแปรที่ถูกผูกไว้" ของฟังก์ชัน func
ที่แก้ไขบริบท this
และอาร์กิวเมนต์แรกหากได้รับ
โดยปกติแล้วเราจะใช้ bind
เพื่อแก้ไข this
สำหรับวิธีอ็อบเจ็กต์ เพื่อให้เราสามารถส่งต่อมันไปที่ใดที่หนึ่งได้ ตัวอย่างเช่น เพื่อ setTimeout
เมื่อเราแก้ไขข้อโต้แย้งบางส่วนของฟังก์ชันที่มีอยู่ ผลลัพธ์ที่ได้ (สากลน้อยกว่า) เรียกว่า ฟังก์ชันบางส่วน หรือ บางส่วน
การแบ่งบางส่วนเป็นวิธีที่สะดวกเมื่อเราไม่ต้องการโต้แย้งเรื่องเดิมซ้ำแล้วซ้ำอีก เช่นถ้าเรามีฟังก์ชัน send(from, to)
และ from
ควรจะเหมือนกันเสมอสำหรับงานของเรา เราก็สามารถได้รับฟังก์ชันบางส่วนและดำเนินการต่อไปได้
ความสำคัญ: 5
ผลลัพธ์จะเป็นอย่างไร?
ฟังก์ชัน ฉ() { การแจ้งเตือน (นี้); - - ให้ผู้ใช้ = { g: f.bind(null) - ผู้ใช้.g();
คำตอบ: null
ฟังก์ชัน ฉ() { การแจ้งเตือน (นี้); // โมฆะ - ให้ผู้ใช้ = { g: f.bind(null) - ผู้ใช้.g();
บริบทของฟังก์ชันที่ถูกผูกไว้นั้นได้รับการแก้ไขยาก ไม่มีทางที่จะเปลี่ยนแปลงมันได้อีก
ดังนั้นแม้ในขณะที่เรารัน user.g()
ฟังก์ชันดั้งเดิมก็ถูกเรียกด้วย this=null
ความสำคัญ: 5
เราสามารถเปลี่ยน this
โดยการผูกมัดเพิ่มเติมได้หรือไม่?
ผลลัพธ์จะเป็นอย่างไร?
ฟังก์ชัน ฉ() { การแจ้งเตือน (this.name); - f = f.bind( {ชื่อ: "จอห์น"} ).bind( {ชื่อ: "แอน" } ); ฉ();
คำตอบ: จอห์น .
ฟังก์ชัน ฉ() { การแจ้งเตือน (this.name); - f = f.bind( {ชื่อ: "จอห์น"} ).bind( {ชื่อ: "พีท"} ); ฉ(); // จอห์น
ออบเจ็กต์ฟังก์ชันผูกที่แปลกใหม่ที่ส่งคืนโดย f.bind(...)
จะจดจำบริบท (และอาร์กิวเมนต์หากมีให้) ในเวลาที่สร้างเท่านั้น
ฟังก์ชันไม่สามารถผูกใหม่ได้
ความสำคัญ: 5
มีค่าอยู่ในคุณสมบัติของฟังก์ชัน หลังจาก bind
แล้วจะเปลี่ยนไปไหม? ทำไมหรือทำไมไม่?
ฟังก์ชั่น sayHi() { การแจ้งเตือน( this.name ); - sayHi.test = 5; ให้ผูกไว้ = sayHi.bind({ ชื่อ: "จอห์น" - การแจ้งเตือน ( bound.test ); // ผลลัพธ์จะเป็นอย่างไร? ทำไม
คำตอบ: undefined
.
ผลลัพธ์ของ bind
เป็นอีกวัตถุหนึ่ง มันไม่มีคุณสมบัติ test
ความสำคัญ: 5
การเรียก askPassword()
ในโค้ดด้านล่างควรตรวจสอบรหัสผ่าน จากนั้นเรียก user.loginOk/loginFail
ขึ้นอยู่กับคำตอบ
แต่มันนำไปสู่ข้อผิดพลาด ทำไม
แก้ไขบรรทัดที่ไฮไลต์เพื่อให้ทุกอย่างเริ่มทำงานได้อย่างถูกต้อง (บรรทัดอื่นไม่ต้องเปลี่ยน)
ฟังก์ชั่น AskPassword (ตกลง, ล้มเหลว) { ให้รหัสผ่าน = prompt("รหัสผ่าน?", ''); ถ้า (รหัสผ่าน == "rockstar") ตกลง(); อย่างอื่นล้มเหลว(); - ให้ผู้ใช้ = { ชื่อ: 'จอห์น' เข้าสู่ระบบตกลง() { alert(`${this.name} เข้าสู่ระบบแล้ว`); - เข้าสู่ระบบล้มเหลว() { alert(`${this.name} ไม่สามารถเข้าสู่ระบบได้`); - - ถามรหัสผ่าน (user.loginOk, user.loginFail);
ข้อผิดพลาดเกิดขึ้นเนื่องจาก askPassword
ได้รับฟังก์ชัน loginOk/loginFail
โดยไม่มีอ็อบเจ็กต์
เมื่อมันเรียกพวกเขา พวกเขาจะสมมติ this=undefined
ตามธรรมชาติ
มา bind
บริบทกัน:
ฟังก์ชั่น AskPassword (ตกลง, ล้มเหลว) { ให้รหัสผ่าน = prompt("รหัสผ่าน?", ''); ถ้า (รหัสผ่าน == "rockstar") ตกลง(); อย่างอื่นล้มเหลว(); - ให้ผู้ใช้ = { ชื่อ: 'จอห์น' เข้าสู่ระบบตกลง() { alert(`${this.name} เข้าสู่ระบบแล้ว`); - เข้าสู่ระบบล้มเหลว() { alert(`${this.name} ไม่สามารถเข้าสู่ระบบได้`); - - AskPassword(user.loginOk.bind(ผู้ใช้), user.loginFail.bind(ผู้ใช้));
ตอนนี้มันใช้งานได้
ทางเลือกอื่นอาจเป็น:
- AskPassword(() => user.loginOk(), () => user.loginFail());
โดยปกติแล้วจะใช้งานได้และดูดี
มีความน่าเชื่อถือน้อยกว่าเล็กน้อยแม้ว่าในสถานการณ์ที่ซับซ้อนมากขึ้นซึ่งตัวแปร user
อาจเปลี่ยนแปลง หลังจาก การเรียก askPassword
แต่ ก่อนที่ ผู้เยี่ยมชมจะรับสายและโทร () => user.loginOk()
ความสำคัญ: 5
งานนี้เป็นตัวแปรที่ซับซ้อนกว่าเล็กน้อยของฟังก์ชัน Fix ที่สูญเสีย "สิ่งนี้"
วัตถุ user
รับการแก้ไข ตอนนี้แทนที่จะมีสองฟังก์ชัน loginOk/loginFail
มีฟังก์ชันเดียว user.login(true/false)
เราควรส่งอะไร askPassword
ในโค้ดด้านล่าง เพื่อให้เรียก user.login(true)
ว่า ok
และ user.login(false)
ว่า fail
ฟังก์ชั่น AskPassword (ตกลง, ล้มเหลว) { ให้รหัสผ่าน = prompt("รหัสผ่าน?", ''); ถ้า (รหัสผ่าน == "rockstar") ตกลง(); อย่างอื่นล้มเหลว(); - ให้ผู้ใช้ = { ชื่อ: 'จอห์น' เข้าสู่ระบบ (ผลลัพธ์) { การแจ้งเตือน ( this.name + (ผลลัพธ์ ? ' เข้าสู่ระบบ' : ' ไม่สามารถเข้าสู่ระบบ') ); - - ถามรหัสผ่าน(?, ?); -
การเปลี่ยนแปลงของคุณควรแก้ไขเฉพาะส่วนที่ไฮไลต์เท่านั้น
ใช้ฟังก์ชัน wrapper ลูกศรให้กระชับ:
AskPassword(() => user.login(true), () => user.login(false));
ตอนนี้มันรับ user
จากตัวแปรภายนอกและรันตามปกติ
หรือสร้างฟังก์ชันบางส่วนจาก user.login
ที่ใช้ user
เป็นบริบทและมีอาร์กิวเมนต์แรกที่ถูกต้อง:
AskPassword(user.login.bind(ผู้ใช้, จริง), user.login.bind(ผู้ใช้, false));