คำหลัก“ นี้” ทำงานอย่างไรภายในฟังก์ชั่น


248

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

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

// inside class definition, itself an object literal, we have this function:
onRender: function() {

    this.menuItems = this.menuItems.concat([
        {
            text: 'Group by Module',
            rptletdiv: this
        },
        {
            text: 'Group by Status',
            rptletdiv: this
        }]);
    // etc
}

มันจะเกิดขึ้นเมื่อฉันทำสิ่งนี้ด้วย var signup = { onLoadHandler:function(){ console.log(this); return Type.createDelegate(this,this._onLoad); }, _onLoad: function (s, a) { console.log("this",this); }};
Deeptechtons


ตรวจสอบโพสต์นี้ มีคำอธิบายที่ดีเกี่ยวกับการใช้งานและพฤติกรรมที่หลากหลายของคำหลักนี้
ความรัก Hasija

ชำระเงินลิงค์นี้scotch.io/@alZami/understanding-this-in-javascript
AL-zami

คำตอบ:


558

แผงโซลาร์เซลล์จากโพสต์อื่น ๆ ของฉันที่นี่มากกว่าที่คุณเคยอยากรู้เกี่ยวกับเรื่องนี้

ก่อนที่ฉันจะเริ่มนี่คือสิ่งที่สำคัญที่สุดที่ต้องจำไว้เกี่ยวกับ Javascript และทำซ้ำเพื่อตัวเองเมื่อมันไม่สมเหตุสมผล Javascript ไม่มีคลาส (ES6 classคือsyntactic sugar ) หากสิ่งที่ดูเหมือนว่าเป็นชั้นเรียนก็เป็นเคล็ดลับที่ชาญฉลาด จาวาสคริปต์ที่มีวัตถุ และฟังก์ชั่น (นั่นไม่ถูกต้อง 100% ฟังก์ชั่นเป็นเพียงวัตถุ แต่บางครั้งมันก็มีประโยชน์ที่จะคิดว่ามันเป็นสิ่งที่แยกต่างหาก)

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

มีสี่วิธีในการเรียกใช้ฟังก์ชันใน javascript คุณสามารถเรียกฟังก์ชั่นเป็นวิธีการที่เป็นฟังก์ชั่นเป็นตัวสร้างและมีผลบังคับใช้

เป็นวิธีการ

วิธีการคือฟังก์ชั่นที่แนบมากับวัตถุ

var foo = {};
foo.someMethod = function(){
    alert(this);
}

เมื่อเรียกใช้เป็นวิธีการนี้จะถูกผูกไว้กับวัตถุที่ฟังก์ชั่น / วิธีการเป็นส่วนหนึ่งของ ในตัวอย่างนี้สิ่งนี้จะถูกผูกไว้กับ foo

เป็นฟังก์ชั่น

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

 var foo = function(){
    alert(this);
 }
 foo();

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

หลายคนประสบปัญหาด้วยการทำอะไรแบบนี้อืม

var foo = {};
foo.someMethod = function (){
    var that=this;
    function bar(){
        alert(that);
    }
}

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

หมายเหตุ: ในuse strictโหมดหากใช้เป็นฟังก์ชั่นthisจะไม่ถูกผูกไว้กับ global (เป็นundefined)

ในฐานะผู้สร้าง

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

คุณเรียกใช้ฟังก์ชันเป็นตัวสร้างด้วยคำหลักใหม่

function Foo(){
    this.confusing = 'hell yeah';
}
var myObject = new Foo();

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

บางคนคิดว่าตัวสร้าง / คำหลักใหม่เป็นกระดูกที่ถูกโยนไปยังโปรแกรมเมอร์ Java / OOP แบบดั้งเดิมเพื่อสร้างสิ่งที่คล้ายกับคลาส

ด้วยวิธีการสมัคร

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

function foo(a,b){
    alert(a);
    alert(b);
    alert(this);
}
var args = ['ah','be'];
foo.apply('omg',args);

8
หมายเหตุ: ในโหมดที่เข้มงวด , thisจะเป็นundefinedสำหรับการเรียกฟังก์ชั่น
สารเลว

1
ประกาศฟังก์ชั่นเช่น function myfunction () {} เป็นกรณีพิเศษของ "as a method" โดยที่ "this" เป็นขอบเขตทั่วโลก (หน้าต่าง)
richard

1
@ Richard: ยกเว้นในโหมดเข้มงวดและthisไม่มีส่วนเกี่ยวข้องกับขอบเขต คุณหมายถึงโลกวัตถุ
TJ Crowder

@ alan-storm ในกรณีของ "ในฐานะผู้สร้าง" จะthis.confusing = 'hell yeah';เหมือนกับvar confusing = 'hell yeah';หรือไม่? ดังนั้นทั้งสองจะอนุญาตmyObject.confusingหรือไม่ มันจะดีถ้าไม่ใช่เพื่อให้คุณสามารถใช้thisเพื่อสร้างคุณสมบัติและตัวแปรอื่น ๆ สำหรับงานภายใน
wunth

แต่แล้วอีกครั้งผมคิดว่าสิ่งที่ทำงานสามารถทำได้นอกการทำงานและค่าที่ส่งผ่านไปยังตัวสร้าง: function Foo(thought){ this.confusing = thought; }แล้วvar myObject = new Foo("hell yeah");
wunth

35

ฟังก์ชั่นการโทร

ฟังก์ชั่นเป็นเพียงประเภทของวัตถุ

วัตถุฟังก์ชั่นทั้งหมดมีการโทรและใช้วิธีการที่รันวัตถุฟังก์ชั่นที่พวกเขาเรียกว่า

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

ดังนั้นการเรียกฟังก์ชั่น ...

whereAmI = "window";

function foo()
{
    return "this is " + this.whereAmI + " with " + arguments.length + " + arguments";
}

... ที่มีวงเล็บ - foo()- เทียบเท่ากับfoo.call(undefined)หรือfoo.apply(undefined)ซึ่งเป็นได้อย่างมีประสิทธิภาพเช่นเดียวกับหรือfoo.call(window)foo.apply(window)

>>> foo()
"this is window with 0 arguments"
>>> foo.call()
"this is window with 0 arguments"

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

ดังนั้นfoo(1, 2, 3)จะเทียบเท่ากับหรือfoo.call(null, 1, 2, 3)foo.apply(null, [1, 2, 3])

>>> foo(1, 2, 3)
"this is window with 3 arguments"
>>> foo.apply(null, [1, 2, 3])
"this is window with 3 arguments"

ถ้าฟังก์ชั่นเป็นคุณสมบัติของวัตถุ ...

var obj =
{
    whereAmI: "obj",
    foo: foo
};

... การเข้าถึงการอ้างอิงถึงฟังก์ชั่นผ่านทางวัตถุและเรียกมันว่าด้วยวงเล็บ - obj.foo()- เทียบเท่ากับหรือfoo.call(obj)foo.apply(obj)

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

>>> baz = obj.foo;
>>> baz();
"this is window with 0 arguments"

เรียกร้องให้การอ้างอิงฟังก์ชั่นของเราbazไม่ได้ให้บริบทใด ๆ สำหรับการโทรเพื่อให้มันได้อย่างมีประสิทธิภาพเช่นเดียวกับbaz.call(undefined)เพื่อให้ปลายขึ้นอ้างอิงthis windowหากเราต้องการbazรู้ว่ามันเป็นของobjเราจำเป็นต้องให้ข้อมูลที่เมื่อbazมีการเรียกซึ่งเป็นที่ที่อาร์กิวเมนต์แรกcallหรือapplyปิดและเข้ามาเล่น

ขอบเขตโซ่

function bind(func, context)
{
    return function()
    {
        func.apply(context, arguments);
    };
}

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

[global scope (window)] - whereAmI, foo, obj, baz
    |
    [bind scope] - func, context
        |
        [anonymous scope]

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

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

>>> baz = bind(obj.foo, obj);
>>> baz(1, 2);
"this is obj with 2 arguments"

"เมื่อผ่านการอ้างอิงไปยังฟังก์ชั่นจะไม่มีการส่งข้อมูลเพิ่มเติมเกี่ยวกับตำแหน่งที่ถูกส่งผ่าน"ขอบคุณ @insin สำหรับสิ่งนี้
Alex Marandon

9

นี่เป็นพฤติกรรมที่กำหนดไว้หรือไม่? เบราว์เซอร์ปลอดภัยหรือไม่?

ใช่. และใช่.

มีเหตุผลใดบ้างที่แฝงอยู่ว่าทำไมมันถึงเป็นอย่างนั้น ...

ความหมายของthisมันค่อนข้างง่ายที่จะอนุมาน:

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

กรณีที่สองคือข้อบกพร่องในการออกแบบ แต่มันง่ายที่จะแก้ไขโดยการปิด


4

ในกรณีนี้ด้านในthisจะถูกผูกไว้กับวัตถุทั่วโลกแทนที่จะเป็นthisตัวแปรของฟังก์ชั่นด้านนอก มันเป็นวิธีการออกแบบภาษา

ดู "JavaScript: The Good Parts" โดย Douglas Crockford สำหรับคำอธิบายที่ดี


4

ฉันพบการสอนที่ดีเกี่ยวกับECMAScript นี้

ค่านี้เป็นวัตถุพิเศษที่เกี่ยวข้องกับบริบทการดำเนินการ ดังนั้นมันอาจถูกตั้งชื่อเป็นวัตถุบริบท (เช่นวัตถุที่บริบทเปิดใช้งานบริบทการดำเนินการ)

วัตถุใด ๆ ที่อาจใช้เป็นค่าของบริบทนี้

ค่านี้เป็นคุณสมบัติของบริบทการดำเนินการ แต่ไม่ใช่คุณสมบัติของวัตถุตัวแปร

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

ในบริบทโลกค่านี้เป็นวัตถุทั่วโลกตัวเอง (ซึ่งหมายความว่าค่านี้ที่นี่เท่ากับวัตถุตัวแปร)

ในกรณีของบริบทฟังก์ชันค่านี้ในการเรียกใช้ฟังก์ชันทุกครั้งอาจแตกต่างกัน

อ้างอิงJavascript-the-coreและบทที่ 3-this


" ในบริบทโลกค่านี้คือวัตถุทั่วโลก (นั่นหมายถึงค่านี้ตรงนี้เท่ากับวัตถุตัวแปร) " วัตถุทั่วโลกเป็นส่วนหนึ่งของบริบทการดำเนินการทั่วโลกเช่นเดียวกับ (ES4) วัตถุ "ตัวแปร" และบันทึกสภาพแวดล้อม ES5 แต่มันเป็นเอนทิตีที่แตกต่างกันไปทั่วโลกวัตถุ (เช่นบันทึกสภาพแวดล้อมที่ไม่สามารถอ้างอิงโดยตรงมันเป็นสิ่งต้องห้ามโดย spec แต่วัตถุทั่วโลกสามารถ)
RobG
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.