Làm thế nào để từ khóa này làm việc trong một chức năng?


248

Tôi vừa gặp một tình huống thú vị trong JavaScript. Tôi có một lớp với một phương thức xác định một số đối tượng bằng cách sử dụng ký hiệu đối tượng. Bên trong những đối tượng đó, thiscon trỏ đang được sử dụng. Từ hành vi của chương trình, tôi đã suy luận rằng thiscon trỏ đang tham chiếu đến lớp mà phương thức được gọi và không phải là đối tượng được tạo bởi nghĩa đen.

Điều này có vẻ tùy tiện, mặc dù đó là cách tôi mong đợi nó hoạt động. Đây có phải là hành vi được xác định? Có phải trình duyệt chéo an toàn? Có bất kỳ lý do cơ bản tại sao nó là cách nó vượt ra ngoài "thông số kỹ thuật nói như vậy" (ví dụ, nó là hậu quả của một số quyết định / triết lý thiết kế rộng hơn)? Ví dụ mã giảm xuống:

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

nó cũng xảy ra khi tôi làm điều này nữa var signup = { onLoadHandler:function(){ console.log(this); return Type.createDelegate(this,this._onLoad); }, _onLoad: function (s, a) { console.log("this",this); }};
Deeptechtons


kiểm tra bài này . Có một số giải thích tốt về cách sử dụng và hành vi khác nhau của từ khóa này.
Yêu Hasija

Câu trả lời:


558

Bị ăn cắp từ một bài đăng khác của tôi, đây là nhiều hơn bạn muốn biết về điều này .

Trước khi tôi bắt đầu, đây là điều quan trọng nhất cần ghi nhớ về Javascript và lặp lại với chính mình khi nó không có ý nghĩa. Javascript không có các lớp (ES6 classcú pháp đường ). Nếu một cái gì đó trông giống như một lớp học, đó là một mẹo thông minh. Javascript có các đối tượngchức năng . (điều đó không chính xác 100%, các chức năng chỉ là đối tượng, nhưng đôi khi có thể hữu ích khi nghĩ về chúng như những thứ riêng biệt)

Biến này được gắn vào các chức năng. Bất cứ khi nào bạn gọi hàm, giá trị này sẽ được cung cấp một giá trị nhất định, tùy thuộc vào cách bạn gọi hàm. Điều này thường được gọi là mô hình gọi.

Có bốn cách để gọi các hàm trong javascript. Bạn có thể gọi hàm như một phương thức , như một hàm , như là một hàm tạo và với ứng dụng .

Như một phương pháp

Phương thức là một hàm gắn liền với một đối tượng

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

Khi được gọi như một phương thức, điều này sẽ được liên kết với đối tượng mà hàm / phương thức là một phần của. Trong ví dụ này, điều này sẽ bị ràng buộc với foo.

Là một chức năng

Nếu bạn có một độc lập chức năng, này biến sẽ được ràng buộc với đối tượng "toàn cầu", gần như lúc nào cũng là cửa sổ đối tượng trong bối cảnh của một trình duyệt.

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

Đây có thể là những gì khiến bạn vấp ngã , nhưng đừng cảm thấy tồi tệ. Nhiều người coi đây là một quyết định thiết kế tồi. Vì một cuộc gọi lại được gọi là một chức năng chứ không phải là một phương thức, đó là lý do tại sao bạn thấy những gì dường như là hành vi không nhất quán.

Nhiều người giải quyết vấn đề bằng cách làm một cái gì đó như, ừm, cái này

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

Bạn định nghĩa một biến đó mà điểm đến này . Việc đóng (một chủ đề hoàn toàn thuộc về nó) giữ điều đó xung quanh, vì vậy nếu bạn gọi thanh dưới dạng gọi lại, nó vẫn có một tham chiếu.

GHI CHÚ: Trong use strictchế độ nếu được sử dụng làm chức năng, thiskhông bị ràng buộc với toàn cầu. (Đó là undefined).

Là một nhà xây dựng

Bạn cũng có thể gọi một hàm như là một hàm tạo. Dựa trên quy ước đặt tên bạn đang sử dụng (TestObject), đây cũng có thể là những gì bạn đang làm và là điều khiến bạn vấp ngã .

Bạn gọi một hàm là Trình xây dựng với từ khóa mới.

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

Khi được gọi như là một constructor, một Object mới sẽ được tạo và điều này sẽ được ràng buộc với đối tượng đó. Một lần nữa, nếu bạn có các chức năng bên trong và chúng được sử dụng làm cuộc gọi lại, bạn sẽ gọi chúng là các chức năng và điều này sẽ bị ràng buộc với đối tượng toàn cầu. Sử dụng var đó = thủ thuật / mẫu này.

Một số người nghĩ rằng hàm tạo / từ khóa mới là xương cốt được lập trình viên Java / OOP truyền thống như một cách để tạo ra thứ gì đó tương tự như các lớp.

Với phương pháp áp dụng

Cuối cùng, mọi hàm đều có một phương thức (vâng, các hàm là các đối tượng trong Javascript) có tên là "áp dụng". Áp dụng cho phép bạn xác định giá trị của giá trị này sẽ là bao nhiêu và cũng cho phép bạn vượt qua trong một loạt các đối số. Đây là một ví dụ vô dụng.

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

8
Lưu ý: Trong chế độ nghiêm ngặt , thissẽ undefineddành cho các yêu cầu chức năng.
Linh tinh

1
Một khai báo hàm, ví dụ. function myfunction () {}, là trường hợp đặc biệt của "như một phương thức" trong đó "this" là phạm vi toàn cầu (cửa sổ).
Richard

1
@richard: Ngoại trừ trong chế độ nghiêm ngặt, và thiskhông liên quan gì đến phạm vi. Bạn có nghĩa là đối tượng toàn cầu .
TJ Crowder

@ bão alan. Trong trường hợp "Là một nhà xây dựng" có this.confusing = 'hell yeah';giống như var confusing = 'hell yeah';? Vậy cả hai sẽ cho phép myObject.confusing? Sẽ thật tuyệt nếu không chỉ để bạn có thể sử dụng thisđể tạo các thuộc tính và các biến khác cho công việc nội bộ.
wunth

Nhưng sau đó, một lần nữa tôi đoán rằng các công cụ làm việc có thể được thực hiện bên ngoài hàm và giá trị được truyền cho hàm tạo: function Foo(thought){ this.confusing = thought; }và sau đóvar myObject = new Foo("hell yeah");
wunth

35

Chức năng gọi

Các chức năng chỉ là một loại đối tượng.

Tất cả các đối tượng Hàm có lệnh gọiáp dụng các phương thức thực thi đối tượng Hàm mà chúng được gọi.

Khi được gọi, đối số đầu tiên cho các phương thức này chỉ định đối tượng sẽ được tham chiếu bởi thistừ khóa trong khi thực thi Hàm - nếu nó nullhoặc undefined, đối tượng toàn cục window, được sử dụng cho this.

Do đó, gọi một Hàm ...

whereAmI = "window";

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

... với dấu ngoặc đơn - foo()- tương đương với foo.call(undefined)hoặc foo.apply(undefined), có hiệu quả tương tự như foo.call(window)hoặc foo.apply(window).

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

Các đối số bổ sung callđược truyền dưới dạng đối số cho lệnh gọi hàm, trong khi một đối số bổ sung duy nhất applycó thể chỉ định các đối số cho lệnh gọi hàm là một đối tượng giống như mảng.

Như vậy, foo(1, 2, 3)tương đương với foo.call(null, 1, 2, 3)hoặc 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"

Nếu một chức năng là một thuộc tính của một đối tượng ...

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

... truy cập một tham chiếu đến Hàm thông qua đối tượng và gọi nó bằng dấu ngoặc đơn - obj.foo()- tương đương với foo.call(obj)hoặc foo.apply(obj).

Tuy nhiên, các hàm được giữ làm thuộc tính của các đối tượng không bị "ràng buộc" với các đối tượng đó. Như bạn có thể thấy trong định nghĩa objở trên, vì Hàm chỉ là một loại Đối tượng, chúng có thể được tham chiếu (và do đó có thể được chuyển qua tham chiếu đến lệnh gọi Hàm hoặc được trả về bởi tham chiếu từ lệnh gọi Hàm). Khi tham chiếu đến Hàm được truyền, không có thông tin bổ sung nào về nơi nó được truyền từ đó, đó là lý do tại sao điều sau đây xảy ra:

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

Cuộc gọi đến tham chiếu Chức năng của chúng tôi baz, không cung cấp bất kỳ ngữ cảnh nào cho cuộc gọi, vì vậy nó thực sự giống như baz.call(undefined)vậy, vì vậy thiskết thúc việc tham chiếu window. Nếu chúng ta muốn bazbiết rằng nó thuộc về obj, chúng ta cần bằng cách nào đó cung cấp thông tin đó khi bazđược gọi, đó là nơi tranh luận đầu tiên callhoặc applyvà đóng cửa phát huy tác dụng.

Chuỗi phạm vi

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

Khi một Hàm được thực thi, nó tạo ra một phạm vi mới và có một tham chiếu đến bất kỳ phạm vi kèm theo nào. Khi hàm ẩn danh được tạo trong ví dụ trên, nó có tham chiếu đến phạm vi mà nó được tạo, đó là bindphạm vi. Điều này được gọi là "đóng cửa."

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

Khi bạn cố gắng truy cập vào một biến, "chuỗi phạm vi" này sẽ được tìm thấy một biến có tên đã cho - nếu phạm vi hiện tại không chứa biến đó, bạn hãy xem phạm vi tiếp theo trong chuỗi, và cứ thế cho đến khi bạn đạt được phạm vi toàn cầu. Khi hàm ẩn danh được trả về và bindkết thúc thực thi, hàm ẩn danh vẫn có tham chiếu đến bindphạm vi, vì vậy bindphạm vi không "biến mất".

Với tất cả những điều trên, giờ đây bạn có thể hiểu phạm vi hoạt động như thế nào trong ví dụ sau và tại sao kỹ thuật chuyển một hàm xung quanh "giới hạn trước" với một giá trị cụ thể của thisnó sẽ có khi nó được gọi là hoạt động:

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

"Khi tham chiếu đến Chức năng được thông qua, không có thông tin bổ sung nào về nơi nó được truyền đi từ đó" cảm ơn bạn @insin vì điều này.
Alex Marandon

9

Đây có phải là hành vi được xác định? Có phải trình duyệt chéo an toàn?

Đúng. Và vâng.

Có bất kỳ lý do cơ bản tại sao nó là như vậy ...

Ý nghĩa của thiskhá đơn giản để suy luận:

  1. Nếu thisđược sử dụng bên trong hàm xây dựng và hàm được gọi với newtừ khóa, thistham chiếu đến đối tượng sẽ được tạo. thissẽ tiếp tục có nghĩa là đối tượng ngay cả trong các phương thức công cộng.
  2. Nếu thisđược sử dụng ở bất kỳ nơi nào khác, bao gồm các hàm được bảo vệ lồng nhau , thì nó đề cập đến phạm vi toàn cầu (trong trường hợp trình duyệt là đối tượng cửa sổ).

Trường hợp thứ hai rõ ràng là một lỗ hổng thiết kế, nhưng nó khá dễ dàng để xử lý xung quanh nó bằng cách sử dụng các bao đóng.


4

Trong trường hợp này, bên trong thisđược liên kết với đối tượng toàn cầu thay vì thisbiến của hàm ngoài. Đó là cách ngôn ngữ được thiết kế.

Xem "JavaScript: Các bộ phận tốt" của Douglas Crockford để được giải thích tốt.


4

Tôi tìm thấy một hướng dẫn tốt đẹp về ECMAScript này

Giá trị này là một đối tượng đặc biệt có liên quan đến bối cảnh thực hiện. Do đó, nó có thể được đặt tên là một đối tượng bối cảnh (tức là một đối tượng trong đó bối cảnh thực thi được kích hoạt).

Bất kỳ đối tượng có thể được sử dụng như giá trị này của bối cảnh.

một giá trị này là một thuộc tính của bối cảnh thực thi, nhưng không phải là một thuộc tính của đối tượng biến.

Tính năng này rất quan trọng, vì trái với các biến, giá trị này không bao giờ tham gia vào quá trình phân giải định danh. Tức là khi truy cập mã này trong một mã, giá trị của nó được lấy trực tiếp từ bối cảnh thực thi và không có bất kỳ tra cứu chuỗi phạm vi nào. Giá trị của điều này chỉ được xác định một lần khi vào ngữ cảnh.

Trong bối cảnh toàn cầu, một giá trị này là chính đối tượng toàn cầu (có nghĩa là, giá trị này ở đây bằng với đối tượng biến)

Trong trường hợp bối cảnh hàm, giá trị này trong mỗi lệnh gọi hàm có thể khác nhau

Tham khảo Javascript-the-coreChương-3-this


" Trong bối cảnh toàn cầu, một giá trị này là chính đối tượng toàn cầu (có nghĩa là, giá trị này ở đây bằng với đối tượng biến) ". Các đối tượng trên toàn cầu là một phần của bối cảnh thực hiện toàn cầu, như là (ES4) "biến đối tượng" và kỷ lục môi trường ES5. Nhưng chúng là các thực thể khác nhau đối với đối tượng toàn cầu (ví dụ: bản ghi môi trường không thể được tham chiếu trực tiếp, nó bị cấm bởi thông số kỹ thuật, nhưng đối tượng toàn cầu có thể).
RobG
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.