Bí danh hàm JavaScript dường như không hoạt động


85

Tôi vừa mới đọc câu hỏi này và muốn thử phương pháp bí danh thay vì phương thức trình bao bọc hàm, nhưng dường như tôi không thể làm cho nó hoạt động trong Firefox 3 hoặc 3.5beta4 hoặc Google Chrome, cả trong cửa sổ gỡ lỗi và trong một trang web thử nghiệm.

Firebug:

>>> window.myAlias = document.getElementById
function()
>>> myAlias('item1')
>>> window.myAlias('item1')
>>> document.getElementById('item1')
<div id="item1">

Nếu tôi đặt nó trong một trang web, cuộc gọi đến myAlias ​​sẽ cho tôi lỗi sau:

uncaught exception: [Exception... "Illegal operation on WrappedNative prototype object" nsresult: "0x8057000c (NS_ERROR_XPC_BAD_OP_ON_WN_PROTO)" location: "JS frame :: file:///[...snip...]/test.html :: <TOP_LEVEL> :: line 7" data: no]

Chrome (có chèn >>> để rõ ràng):

>>> window.myAlias = document.getElementById
function getElementById() { [native code] }
>>> window.myAlias('item1')
TypeError: Illegal invocation
>>> document.getElementById('item1')
<div id=?"item1">?

Và trong trang thử nghiệm, tôi nhận được cùng một "Lệnh gọi bất hợp pháp".

Tôi có làm điều gì sai? Bất cứ ai khác có thể tái tạo điều này?

Ngoài ra, thật kỳ lạ, tôi vừa thử và nó hoạt động trong IE8.


1
trong IE8 - nó có thể là một loại chức năng kỳ diệu hoặc một cái gì đó.
Maciej Łebkowski

Tôi đã thêm nhận xét cho các câu trả lời không chính xác tại stackoverflow.com/questions/954417 và hướng dẫn chúng ở đây.
Grant Wagner

1
Đối với các trình duyệt hỗ trợ phương thức Function.prototype.bind: window.myAlias ​​= document.getElementById.bind (document); Tìm kiếm tài liệu MDN, sẽ có một miếng đệm ràng buộc.
Ben Fleming

Câu trả lời:


39

Bạn phải liên kết phương thức đó với đối tượng tài liệu. Nhìn:

>>> $ = document.getElementById
getElementById()
>>> $('bn_home')
[Exception... "Cannot modify properties of a WrappedNative" ... anonymous :: line 72 data: no]
>>> $.call(document, 'bn_home')
<body id="bn_home" onload="init();">

Khi bạn đang thực hiện một bí danh đơn giản, hàm được gọi trên đối tượng toàn cục, không phải trên đối tượng tài liệu. Sử dụng một kỹ thuật được gọi là bao đóng để khắc phục điều này:

function makeAlias(object, name) {
    var fn = object ? object[name] : null;
    if (typeof fn == 'undefined') return function () {}
    return function () {
        return fn.apply(object, arguments)
    }
}
$ = makeAlias(document, 'getElementById');

>>> $('bn_home')
<body id="bn_home" onload="init();">

Bằng cách này, bạn không mất tham chiếu đến đối tượng ban đầu.

Vào năm 2012, có một bindphương pháp mới từ ES5 cho phép chúng tôi thực hiện điều này theo một cách tuyệt vời hơn:

>>> $ = document.getElementById.bind(document)
>>> $('bn_home')
<body id="bn_home" onload="init();">

4
Đó thực sự là một trình bao bọc bởi vì nó trả về một chức năng mới. Tôi nghĩ câu trả lời cho câu hỏi của Kev là không thể vì những lý do bạn mô tả ở trên. Phương pháp bạn mô tả ở đây có lẽ là lựa chọn tốt nhất.
jiggy

Cảm ơn bình luận của bạn, tôi đã không nhìn đủ kỹ để nhận ra điều đó. Vì vậy, cú pháp trong các câu trả lời trên câu hỏi khác là hoàn toàn không có thật? Thú vị ...
Kev

188

Tôi đã đào sâu để hiểu hành vi cụ thể này và tôi nghĩ rằng tôi đã tìm thấy một lời giải thích tốt.

Trước khi tìm hiểu lý do tại sao bạn không thể đặt bí danh document.getElementById, tôi sẽ cố gắng giải thích cách hoạt động của các hàm / đối tượng JavaScript.

Bất cứ khi nào bạn gọi một hàm JavaScript, trình thông dịch JavaScript sẽ xác định một phạm vi và chuyển nó cho hàm.

Hãy xem xét chức năng sau:

function sum(a, b)
{
    return a + b;
}

sum(10, 20); // returns 30;

Hàm này được khai báo trong phạm vi Window và khi bạn gọi nó, giá trị thisbên trong hàm sum sẽ là Windowđối tượng toàn cục .

Đối với hàm 'sum', không quan trọng giá trị của 'this' vì nó không sử dụng nó.


Hãy xem xét chức năng sau:

function Person(birthDate)
{
    this.birthDate = birthDate;    
    this.getAge = function() { return new Date().getFullYear() - this.birthDate.getFullYear(); };
}

var dave = new Person(new Date(1909, 1, 1)); 
dave.getAge(); //returns 100.

Khi bạn gọi hàm dave.getAge, trình thông dịch JavaScript thấy rằng bạn đang gọi hàm getAge trên daveđối tượng, vì vậy nó đặt thisthành davevà gọi getAgehàm. getAge()chính xác sẽ trở lại 100.


Bạn có thể biết rằng trong JavaScript, bạn có thể chỉ định phạm vi bằng applyphương pháp này. Hãy thử điều đó.

var dave = new Person(new Date(1909, 1, 1)); //Age 100 in 2009
var bob = new Person(new Date(1809, 1, 1)); //Age 200 in 2009

dave.getAge.apply(bob); //returns 200.

Trong dòng trên, thay vì để JavaScript quyết định phạm vi, bạn đang chuyển phạm vi theo cách thủ công dưới dạng bobđối tượng. getAgebây giờ sẽ trở lại 200ngay cả khi bạn 'nghĩ' bạn gọi getAgetrên daveđối tượng.


Điểm của tất cả những điều trên là gì? Các hàm được gắn 'lỏng lẻo' với các đối tượng JavaScript của bạn. Vd bạn có thể làm

var dave = new Person(new Date(1909, 1, 1));
var bob = new Person(new Date(1809, 1, 1));

bob.getAge = function() { return -1; };

bob.getAge(); //returns -1
dave.getAge(); //returns 100

Hãy thực hiện bước tiếp theo.

var dave = new Person(new Date(1909, 1, 1));
var ageMethod = dave.getAge;

dave.getAge(); //returns 100;
ageMethod(); //returns ?????

ageMethodthực thi ném một lỗi! Chuyện gì đã xảy ra?

Nếu bạn đọc kỹ những điểm trên của tôi, bạn sẽ lưu ý rằng dave.getAgephương thức được gọi davethisđối tượng trong khi JavaScript không thể xác định 'phạm vi' để ageMethodthực thi. Vì vậy, nó đã chuyển toàn cầu 'Cửa sổ' là 'cái này'. Bây giờ vì windowkhông có thuộc birthDatetính, ageMethodviệc thực thi sẽ không thành công.

Làm thế nào để khắc phục điều này? Đơn giản,

ageMethod.apply(dave); //returns 100.

Tất cả những điều trên có hợp lý không? Nếu có, thì bạn sẽ có thể giải thích tại sao bạn không thể đặt bí danh document.getElementById:

var $ = document.getElementById;

$('someElement'); 

$được gọi với windowtheo thisvà nếu getElementByIdthực hiện dự kiến thisdocument, nó sẽ thất bại.

Một lần nữa để khắc phục điều này, bạn có thể làm

$.apply(document, ['someElement']);

Vậy tại sao nó hoạt động trong Internet Explorer?

Tôi không biết việc triển khai nội bộ của getElementByIdtrong IE, nhưng một nhận xét trong nguồn jQuery ( inArraytriển khai phương thức) nói rằng trong IE , window == document. Nếu đúng như vậy, thì răng cưa document.getElementByIdsẽ hoạt động trong IE.

Để minh họa thêm điều này, tôi đã tạo ra một ví dụ phức tạp. Hãy xem Personchức năng bên dưới.

function Person(birthDate)
{
    var self = this;

    this.birthDate = birthDate;

    this.getAge = function()
    {
        //Let's make sure that getAge method was invoked 
        //with an object which was constructed from our Person function.
        if(this.constructor == Person)
            return new Date().getFullYear() - this.birthDate.getFullYear();
        else
            return -1;
    };

    //Smarter version of getAge function, it will always refer to the object
    //it was created with.
    this.getAgeSmarter = function()
    {
        return self.getAge();
    };

    //Smartest version of getAge function.
    //It will try to use the most appropriate scope.
    this.getAgeSmartest = function()
    {
        var scope = this.constructor == Person ? this : self;
        return scope.getAge();
    };

}

Đối với Personhàm trên, đây là cách các getAgephương thức khác nhau sẽ hoạt động.

Hãy tạo hai đối tượng bằng Personhàm.

var yogi = new Person(new Date(1909, 1,1)); //Age is 100
var anotherYogi = new Person(new Date(1809, 1, 1)); //Age is 200

console.log(yogi.getAge()); //Output: 100.

Về phía trước, phương thức getAge nhận yogiđối tượng là thisvà xuất ra 100.


var ageAlias = yogi.getAge;
console.log(ageAlias()); //Output: -1

Công cụ hỗ trợ JavaScript đặt windowđối tượng là đối tượng thisgetAgephương thức của chúng tôi sẽ trả về -1.


console.log(ageAlias.apply(yogi)); //Output: 100

Nếu chúng tôi đặt phạm vi chính xác, bạn có thể sử dụng ageAliasphương pháp.


console.log(ageAlias.apply(anotherYogi)); //Output: 200

Nếu chúng ta truyền vào một số đối tượng người khác, nó vẫn sẽ tính tuổi một cách chính xác.

var ageSmarterAlias = yogi.getAgeSmarter;    
console.log(ageSmarterAlias()); //Output: 100

Các ageSmarterchức năng chụp bản gốc thisđối tượng vì vậy bây giờ bạn không cần phải lo lắng về cung cấp phạm vi chính xác.


console.log(ageSmarterAlias.apply(anotherYogi)); //Output: 100 !!!

Vấn đề ageSmarterlà bạn không bao giờ có thể đặt phạm vi cho một số đối tượng khác.


var ageSmartestAlias = yogi.getAgeSmartest;
console.log(ageSmartestAlias()); //Output: 100
console.log(ageSmartestAlias.apply(document)); //Output: 100

Các ageSmartestchức năng sẽ sử dụng phạm vi ban đầu nếu một phạm vi không hợp lệ được cung cấp.


console.log(ageSmartestAlias.apply(anotherYogi)); //Output: 200

Bạn vẫn có thể chuyển một Personđối tượng khác tới getAgeSmartest. :)


7
+1 cho lý do IE hoạt động. Phần còn lại hơi lạc đề nhưng nhìn chung vẫn hữu ích. :)
Kev

44
Câu trả lời xuất sắc. Tôi không thấy làm thế nào bất kỳ điều nào trong số đó là lạc đề.
Justin Johnson

Câu trả lời rất hay, thực sự. Một phiên bản ngắn hơn được viết ở đây .
Marcel Korpel,

8
Một trong những câu trả lời hay nhất mà tôi đã thấy trên stackoverflow.
warbaker:

tại sao nó hoạt động trong IE? bởi vì: stackoverflow.com/questions/6994139/... - vì vậy thực hiện phương pháp này trong thực tế có thể là {cửa sổ trở lại [id]}, vì vậy nó có thể làm việc trong mọi hoàn cảnh
Maciej Łebkowski

3

Đây là một câu trả lời ngắn gọn.

Phần sau tạo một bản sao của (tham chiếu đến) hàm. Vấn đề là bây giờ hàm nằm trên windowđối tượng khi nó được thiết kế để sống trên documentđối tượng.

window.myAlias = document.getElementById

Các lựa chọn thay thế là

  • sử dụng trình bao bọc (đã được đề cập bởi Fabien Ménager)
  • hoặc bạn có thể sử dụng hai bí danh.

    window.d = document // A renamed reference to the object
    window.d.myAlias = window.d.getElementById
    

2

Một câu trả lời ngắn gọn khác, chỉ dành cho gói / răng cưa console.logvà các phương pháp ghi nhật ký tương tự. Tất cả đều mong đợi được trong consolebối cảnh.

Điều này có thể sử dụng được khi kết console.loghợp với một số dự phòng, trong trường hợp bạn hoặc người dùng của bạn gặp sự cố khi sử dụng một trình duyệt không (luôn) hỗ trợ nó . Tuy nhiên, đây không phải là một giải pháp đầy đủ cho vấn đề đó, vì nó cần được kiểm tra mở rộng và dự phòng - số dặm của bạn có thể thay đổi.

Ví dụ sử dụng cảnh báo

var warn = function(){ console.warn.apply(console, arguments); }

Sau đó sử dụng nó như bình thường

warn("I need to debug a number and an object", 9999, { "user" : "Joel" });

Nếu bạn muốn xem các đối số ghi nhật ký của mình được bao bọc trong một mảng (tôi thường làm vậy), hãy thay thế .apply(...)bằng .call(...).

Nên làm việc với console.log(), console.debug(), console.info(), console.warn(), console.error(). Xem thêm consoletrên MDN .


1

Ngoài các câu trả lời tuyệt vời khác, còn có phương pháp jQuery đơn giản $ .proxy .

Bạn có thể đặt bí danh như sau:

myAlias = $.proxy(document, 'getElementById');

Hoặc là

myAlias = $.proxy(document.getElementById, document);

+1 vì đó là một giải pháp khả thi. Nhưng, nói đúng ra, đằng sau hậu trường $.proxythực sự cũng là một lớp bao bọc.
pimvdb

-6

Bạn thực sự không thể "bí danh thuần túy" một hàm trên một đối tượng được xác định trước. Do đó, cách gần nhất với răng cưa mà bạn có thể nhận được mà không cần bao bọc là ở trong cùng một đối tượng:

>>> document.s = document.getElementById;
>>> document.s('myid');
<div id="myid">

5
Điều này hơi không chính xác. Bạn có thể 'bí danh thuần túy' một hàm với điều kiện là việc triển khai hàm sử dụng 'this'. Vì vậy, nó phụ thuộc.
SolutionYogi

Xin lỗi, ý tôi là trong ngữ cảnh của getElementById. Bạn đúng. Tôi đã thay đổi câu trả lời một chút để phản ánh điều này ...
Kev
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.