Псевдоним функции JavaScript не работает - PullRequest
80 голосов
/ 17 июня 2009

Я просто читал этот вопрос и хотел попробовать метод псевдонима, а не метод функции-обертки, но я не мог заставить его работать в Firefox 3 или 3.5beta4, или Google Chrome как в окнах отладки, так и на тестовой веб-странице.

Firebug:

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

Если я помещу это на веб-страницу, вызов myAlias ​​выдаст мне эту ошибку:

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 (для ясности вставлено >>>):

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

И на тестовой странице я получаю тот же «незаконный вызов».

Я что-то не так делаю? Кто-нибудь еще может воспроизвести это?

Также, как ни странно, я только что попробовал, и это работает в IE8.

Ответы [ 6 ]

184 голосов
/ 22 июля 2009

Я глубоко копался, чтобы понять это конкретное поведение, и я думаю, что нашел хорошее объяснение.

Прежде чем я выясню, почему вы не можете использовать псевдоним document.getElementById, я попытаюсь объяснить, как работают функции / объекты JavaScript.

Каждый раз, когда вы вызываете функцию JavaScript, интерпретатор JavaScript определяет область действия и передает ее функции.

Рассмотрим следующую функцию:

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

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

Эта функция объявлена ​​в области действия Window, и при ее вызове значение this внутри функции sum будет глобальным Window объектом.

Для функции «sum» не имеет значения, каково значение «this», поскольку она не использует его.


Рассмотрим следующую функцию:

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.

Когда вы вызываете функцию dave.getAge, интерпретатор JavaScript видит, что вы вызываете функцию getAge для объекта dave, поэтому он устанавливает this в dave и вызывает функцию getAge. getAge() вернет правильно 100.


Возможно, вы знаете, что в JavaScript вы можете указать область видимости, используя метод apply. Давайте попробуем это.

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.

В приведенной выше строке вместо того, чтобы позволить JavaScript определять область действия, вы передаете область вручную как объект bob. getAge теперь будет возвращать 200, даже если вы «думали», что вызвали getAge для объекта dave.


Какой смысл всего вышеперечисленного? Функции «слабо» привязаны к вашим объектам JavaScript. Например. Вы можете сделать

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

Давайте сделаем следующий шаг.

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

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

ageMethod выполнение выдает ошибку! Что случилось?

Если вы внимательно прочитаете мои пункты выше, вы заметите, что метод dave.getAge был вызван с dave как this объект, тогда как JavaScript не смог определить «область действия» для выполнения ageMethod. Таким образом, оно прошло глобальное «Окно» как «это». Теперь, когда window не имеет свойства birthDate, выполнение ageMethod завершится неудачно.

Как это исправить? Простой,

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

Имеет ли все вышесказанное смысл? Если да, то вы сможете объяснить, почему вы не можете использовать псевдоним document.getElementById:

var $ = document.getElementById;

$('someElement'); 

$ вызывается с window как this, и если реализация getElementById ожидает, что this будет document, произойдет сбой.

Снова, чтобы исправить это, вы можете сделать

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

Так почему же он работает в Internet Explorer?

Я не знаю внутреннюю реализацию getElementById в IE, но комментарий в источнике jQuery (реализация метода inArray) говорит, что в IE window == document. Если это так, то псевдоним document.getElementById должен работать в IE.

Чтобы проиллюстрировать это далее, я создал сложный пример. Посмотрите на функцию Person ниже.

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

}

Для функции Person, описанной выше, вот как будут вести себя различные методы getAge.

Давайте создадим два объекта, используя функцию Person.

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.

Прямо, метод getAge получает yogi объект как this и выводит 100.


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

JavaScript интерпретатор устанавливает window объект как this, и наш метод getAge вернет -1.


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

Если мы установим правильную область, вы можете использовать метод ageAlias.


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

Если мы передадим объект другого человека, он все равно будет правильно вычислять возраст.

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

Функция ageSmarter захватила оригинальный объект this, так что теперь вам не нужно беспокоиться о предоставлении правильной области видимости.


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

Проблема с ageSmarter заключается в том, что вы никогда не можете установить область видимости для какого-либо другого объекта.


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

Функция ageSmartest будет использовать исходную область, если указана недопустимая область.


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

Вы по-прежнему сможете передавать другой Person объект getAgeSmartest. :)

37 голосов
/ 17 июня 2009

Вы должны привязать этот метод к объекту документа. Посмотрите:

>>> $ = 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();">

Когда вы делаете простой псевдоним, функция вызывается для глобального объекта, а не для объекта документа. Используйте метод, называемый замыканиями, чтобы исправить это:

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

Таким образом, вы не потеряете ссылку на исходный объект.

В 2012 году появился новый метод bind от ES5, который позволяет нам сделать это более изящным способом:

>>> $ = document.getElementById.bind(document)
>>> $('bn_home')
<body id="bn_home" onload="init();">
3 голосов
/ 03 февраля 2011

Это короткий ответ.

Следующее делает копию (ссылку) на функцию. Проблема в том, что теперь функция находится на объекте window, когда она была разработана для жизни на объекте document.

window.myAlias = document.getElementById

Альтернативы

  • использовать обертку (уже упоминавшуюся Фабьен Менагер)
  • или вы можете использовать два псевдонима.

    window.d = document // A renamed reference to the object
    window.d.myAlias = window.d.getElementById
    
2 голосов
/ 03 февраля 2012

Еще один короткий ответ, только для обтекания / псевдонимов console.log и подобных методов ведения журнала. Все они ожидают, что окажутся в контексте console.

Это можно использовать при переносе console.log с некоторыми запасными вариантами, в случае, если вы или ваши пользователи столкнулись с проблемами, когда использует браузер, который (всегда) не поддерживает его . Тем не менее, это не полное решение этой проблемы, поскольку необходимо расширить проверки и использовать запасной вариант - ваш пробег может отличаться.

Пример использования предупреждений

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

Тогда используйте как обычно

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

Если вы предпочитаете, чтобы аргументы журналирования были заключены в массив (чаще всего так и есть), замените .apply(...) на .call(...).

Должно работать с console.log(), console.debug(), console.info(), console.warn(), console.error(). См. Также console в MDN .

1 голос
/ 17 августа 2012

В дополнение к другим замечательным ответам, существует простой метод jQuery $. Proxy .

Вы можете использовать псевдоним так:

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

Или

myAlias = $.proxy(document.getElementById, document);
0 голосов
/ 29 июня 2009

Вы на самом деле не можете"чистый псевдоним" функции для предопределенного объекта. Поэтому наиболее близким к псевдониму, который вы можете получить без переноса, является пребывание внутри одного и того же объекта:

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