Наложение псевдонимов JavaScript, похоже, не работает

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

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

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

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

Ответ 2

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

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

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

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

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

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

Эта функция объявлена ​​в области "Окно", и при ее вызове значение 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 не мог определить выполнение "scope" для ageMethod. Поэтому он передал глобальное "окно" как 'this'. Теперь, когда 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. Если это так, то в IE будет работать псевдоним document.getElementById.

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

function Person(birthDate)
{
    var self = this;

    this.birthDate = birthDate;

    this.getAge = function()
    {
        //Let 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 intercreter устанавливает 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.:)

Ответ 3

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

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

window.myAlias = document.getElementById

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

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

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

Ответ 4

Другой короткий ответ, просто для обертывания/сглаживания 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.

Ответ 5

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

Вы можете использовать псевдоним следующим образом:

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

или

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

Ответ 6

На самом деле вы не можете использовать "чистый псевдоним" на предопределенном объекте. Поэтому ближайший к aliasing, который вы можете получить без обертывания, - это находиться внутри одного и того же объекта:

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