Псевдонимы функций JavaScript не работают

85

Я как раз читал этот вопрос и хотел попробовать метод псевдонима, а не метод-оболочку функции, но я не мог заставить его работать ни в 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.

Кев
источник
1
в IE8 - это может быть какая-то волшебная функция или что-то в этом роде.
Maciej ebkowski
Я добавил комментарии к неправильным ответам на stackoverflow.com/questions/954417 и направил их сюда.
Грант Вагнер,
1
Для браузеров, поддерживающих метод Function.prototype.bind: window.myAlias ​​= document.getElementById.bind (document); Найдите в документации MDN, там будет прокладка привязки.
Бен Флеминг,

Ответы:

39

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

>>> $ = 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 году в bindES5 появился новый метод, который позволяет нам делать это более изящным способом:

>>> $ = document.getElementById.bind(document)
>>> $('bn_home')
<body id="bn_home" onload="init();">
Мацей Лебковски
источник
4
Что на самом деле является оберткой, потому что возвращает новую функцию. Я думаю, что ответ на вопрос Кева состоит в том, что это невозможно по причинам, которые вы описали выше. Метод, который вы здесь описываете, вероятно, является лучшим вариантом.
jiggy
Спасибо за ваш комментарий, я не посмотрел достаточно внимательно, чтобы понять это. Значит, синтаксис в ответах на другой вопрос полностью подделан? Интересно ...
Кев
188

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

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

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

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

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

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

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

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


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

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 видит , что вы вызываете функцию СеЬАда на 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. :)

Решение Йоги
источник
7
+1, почему IE работает. Остальное немного не по теме, но в целом полезно. :)
Kev
44
Отличный ответ. Я не понимаю, что это не по теме.
Джастин Джонсон
Действительно, очень хороший ответ. Несколько более короткая версия написана здесь .
Марсель Корпель,
8
Один из лучших ответов, которые я видел по stackoverflow.
warbaker
почему это работает в IE? потому что: stackoverflow.com/questions/6994139/… - так что реализация этого метода на самом деле может быть {return window [id]}, поэтому он будет работать в любом контексте
Maciej ebkowski
3

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

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

window.myAlias = document.getElementById

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

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

    window.d = document // A renamed reference to the object
    window.d.myAlias = window.d.getElementById
    
Брайан Филд
источник
2

Еще один короткий ответ, только для обертывания / псевдонима 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

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

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

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

Или

myAlias = $.proxy(document.getElementById, document);
Санхюн Ли
источник
+1 потому что это реальное решение. Но, собственно говоря, за кадром $.proxyтоже есть обертка.
pimvdb
-6

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

>>> document.s = document.getElementById;
>>> document.s('myid');
<div id="myid">
Кев
источник
5
Это немного неверно. Вы можете использовать «чистый псевдоним» для функции при условии, что реализация функции использует «this». Так что это зависит от обстоятельств.
SolutionYogi
Извините, я имел в виду в контексте getElementById. Ты прав. Я немного изменил ответ, чтобы отразить это ...
Кев