«this» не работает, когда привязка функций к событиям внутри класса Javascript - PullRequest
3 голосов
/ 29 июля 2010

Во-первых, я знаю, что могу скопировать "this" при создании экземпляра, но здесь это не работает.

В основном я пишу что-то, чтобы отслеживать людей, взаимодействующих с видео на Youtube.

Я получил это работает нормально для одного видео за раз.Но я хочу, чтобы он работал и на страницах с несколькими видео на Youtube, поэтому я преобразовал код в класс, чтобы я мог создать новый его экземпляр для каждого видео на странице.

Проблема заключается в попыткепривязать к слушателю событий Youtube для изменений состояния.Для «не классового» кода это выглядит следующим образом:

var o = document.getElementById( id );
o.addEventListener("onStateChange", "onPlayerStateChange" );

(onPlayerStateChange - функция, которую я написал для отслеживания изменений состояния в видео)

(я также знаю, чтоaddEventListener не будет работать с MSIE, но я пока не беспокоюсь об этом)

Но когда я нахожусь в классе, я должен использовать «this» для ссылки на другую функцию в этом классе.Вот как выглядит код:

this.o = document.getElementById( id );
this.o.addEventListener("onStateChange", "this.onPlayerStateChange" );

Когда он написан так, this.onPlayerStateChange никогда не вызывается.Я пытался скопировать «this» в другую переменную, например «me», но это тоже не работает.Функция onPlayerStateChange определена в области действия «this», прежде чем я сделаю это:

var me = this;
this.o = document.getElementById( id );
this.o.addEventListener("onStateChange", "me.onPlayerStateChange" );

Есть идеи?

Просматривая другие подобные вопросы, все они используют jQuery, и ядумаю, что так будет работать, если я так сделаю.Но я не хочу использовать jQuery, потому что это будет развернуто на случайных сторонних сайтах.Мне нравится jQuery, но я не хочу, чтобы это требовалось.

Ответы [ 6 ]

3 голосов
/ 29 июля 2010

Вы можете использовать технику под названием curry для достижения этой цели. Для этого вам нужна функция карри. Вот тот, который я написал некоторое время назад

     /**
      * Changes the scope of function "fn" to the "scope" parameter specified or
      * if not, defaults to window scope. The scope of the function determines what
      * "this" inside "fn" evaluates to, inside the function "fn". Any additional arguments
      * specified in this are passed to the underlying "curried" function. If the underlying
      * function is already passed some arguments, the optional arguments are appended
      * to the argument array of the underlying function. To explain this lets take
      * the example below:
      *
      * You can pass any number of arguments that are passed to the underlying (curried)
      * function
      * @param {Function} fn The function to curry
      * @param {Object} scope The scope to be set inside the curried function, if
      * not specified, defaults to window
      * @param arguments {...} Any other optional arguments ot be passed to the curried function
      *
      */
     var curry = function(fn, scope /*, arguments */) {
        scope = scope || window;
        var actualArgs = arguments;

        return function() {
           var args = [];
           for(var j = 0; j < arguments.length; j++) {
              args.push(arguments[j]);
           }

           for(var i = 2; i < actualArgs.length; i++) {
              args.push(actualArgs[i]);
           }

           return fn.apply(scope, args);
        };
     };

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

     this.o.addEventListener("onStateChange", curry(onPlayerStateChange, this));

Edit:

var curriedFunc = curry(onPlayerStateChange, this);
this.o.addEventListener("onStateChange", "curriedFunc");

Edit: Хорошо, скажем, это ваш пользовательский класс, который вы создаете:

function MyCustomClass() {
   var privateVar = "x"; // some variables;
   this.onPlayerStateChange = function() {  //instance method on your custom class
       // do something important
   }
}

На глобальном уровне вы создаете экземпляр MyCustomClass

   var myCustom = new MyCustomClass(); // create a new instance of your custom class
   var curriedFunc = curry(myCustom.onplayerStageChange, myCustom); // curry its onplayerstateChange
   // now add it to your event handler
   o.addEventListener("onStateChange", "curriedFunc");
3 голосов
/ 29 июля 2010

Вам нужен глобальный способ доступа к методу onPlayerStateChange вашего объекта.Когда вы присваиваете me как var me = this;, переменная me действительна только внутри метода объекта, в котором она создана.Однако для API проигрывателя Youtube требуется функция, доступная в глобальном масштабе, поскольку фактический вызов поступает из Flash и не имеет прямой ссылки на вашу функцию JavaScript.

Я нашел очень полезную запись в блоге Джеймса Коглана (James Coglan), в котором он обсудил хороший способ взаимодействия с JavaScript API JavaScript и управления событиями для нескольких видео.

Я выпустил библиотеку JavaScript-оболочки, используя его идеи на http://github.com/AnuragMishra/YoutubePlayer. Почувствуйтебесплатно проверить код.Основная идея проста - хранить все экземпляры объекта player в конструкторе.Например:

function Player(id) {
    // id of the placeholder div that gets replaced
    // the <object> element in which the flash video resides will
    // replace the placeholder div and take over its id
    this.id = id;

    Player.instances.push(this);
}

Player.instances = [];

При передаче строки в качестве обратного вызова используйте строку вида:

"Player.dispatchEvent('playerId')"

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

Player.dispatchEvent = function(id) {
    var player = ..; // search player object using id in "instances"
    return function(eventId) { // this is the callback that Flash talks to
        player.notify(eventId);
    };
};

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

function onYouTubePlayerReady(id) {
    var player = ..; // find player in "instances"

    // replace <id> with player.id
    var callback = "YoutubePlayer.dispatchEvent({id})";
    callback = callback.replace("{id}", player.id);

    player.addEventListener('onStateChange', callback);
}

См. рабочий пример здесь. .

2 голосов
/ 29 июля 2010

Для добавления события следует использовать следующее:

this.o.addEventListener("statechange", this.onPlayerStateChange);

Для addEventListener вам не нужно добавлять префикс on.

То, что я выложил выше, правильно для стандартного javascript, но поскольку он передает его объекту YT flash, он ожидает onStateChange, что правильно.1016 * РЕДАКТИРОВАТЬ : попробуйте метод в этот пост , чтобы помочь.

1 голос
/ 29 июля 2010

После просмотра API-интерфейса Youtube выглядит, что addEventListener принимает только String для функции обработчика событий. Это означает, что нет чистого способа зарегистрировать уникальный обработчик событий для каждого объекта.

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

function globalOnPlayerStateChange() {
    for (tracker in myTrackerObjects) {
        tracker.playerStateChange();
    }
}

Каждый объект трекера может затем самостоятельно определить, действительно ли произошло изменение состояния (с помощью функции API getPlayerState):

function MyYoutubeTracker() {
    this.currentState = ...

    // Determine if state changed happened or not
    this.playerStateChange = function() {
        var newState = this.o.getPlayerState();
        if (newState != this.currentState) {
            // State has changed
            this.currentState = newState;
        }
    }

    // Register global event handler for this youtube object
    this.o.addEventListener("onStateChange", "globalOnPlayerStateChange");
}
1 голос
/ 29 июля 2010

TheCloudlessSky был отчасти прав, а Шон был отчасти прав.Вы можете продолжать использовать «onStateChange» в качестве имени события, но не помещайте this.onPlayerStateChange в кавычки - это удаляет специальное значение this, и javascript будет искать функцию с именем «this.onPlayerStateChange», а не искатьдля функции "onPlayerStateChange" внутри this объекта.

this.o.addEventListener("onStateChange", this.onPlayerStateChange);
0 голосов
/ 29 июля 2010

Хорошо, у меня все это работает.Это немного уродливый хак, но это работает.По сути, я сохраняю каждый новый экземпляр класса в массиве и передаю ключ массива (1, 2 и т. Д.) В класс, чтобы он мог ссылаться на себя извне по мере необходимости в нескольких ключевых местах.

Места, в которых класс должен ссылаться на себя извне, - это строка, которую я передаю в addEventListener, и в нескольких функциях setTimeout, где «this», по-видимому, теряет свой контекст (насколько я могу сказать в любом случае, потому чтоединственный способ заставить их работать - это изменить «this», чтобы вместо этого использовать внешние ссылки.

Вот полный код.

На странице, где есть видео Youtube, они вводятся с использованием swfobject.Объект _ytmeta хранит заголовки для каждого видео. Это необязательно, но это единственный способ записать заголовок видео, потому что API YouTube не дает его вам. Это означает, что вы должны знать заголовок заранее, но дело в том, чтопросто, если вы хотите, чтобы заголовок отображался в наших отчетах, вы должны создать этот объект:

<div id='yt1'></div>

<script src='youtube.js'></script>
<script src='swfobject.js'></script>
<script>
var _ytmeta = {}
_ytmeta.yt1 = { 'title': 'Moonwalking in Walmart' };

var params = { allowScriptAccess: "always" };
swfobject.embedSWF("http://www.youtube.com/v/gE1ZvCnwkYk?enablejsapi=1&playerapiid=yt1", "yt1", "425", "356", "8", null, null, params );
</script>

Итак, мы включили код JavaScript swfobject, а также файл youtube.js, которыйбудет размещен на нашем сервере и включен на страницы, которые вы хотите отслеживать видео.

Вот содержимое youtube.js:

// we're storing each youtube object (video) in an array, and passing the array key into the class, so the class instance can refer to itself externally
// this is necessary for two reasons
// first, the event listener function we pass to Youtube has to be globally accessible, so passing "this.blah" doesn't work
// it has to be passed as a string also, so putting "this" in quotes makes it lose its special meaning
// second, when we create timeout functions, the meaning of "this" inside that function loses its scope, so we have to refer to the class externally from there too.

// _yt is the global youtube array that stores each youtube object. yti is the array key, incremented automatically for each new object created
var _yt = [], _yti = 0;

// this is the function the youtube player calls once it's loaded. 
// each time it's called, it creates a new object in the global array, and passes the array key into the class so the class can refer to itself externally
function onYouTubePlayerReady( id ) {
  _yti++;
  _yt[ _yti ] = new _yta( id, _yti );
}

function _yta( id, i ) {

  if( !id || !i ) return;

  this.id = id;
  this.mytime;
  this.scrubTimer;
  this.startTimer;
  this.last = 'none';
  this.scrubbing = false;

  this.o = document.getElementById( this.id );
  this.o.addEventListener("onStateChange", "_yt["+i+"].onPlayerStateChange" );

  this.onPlayerStateChange = function( newState ) {

    // some events rely on a timer to determine what action was performed, we clear it on every state change.
    if( this.myTime != undefined ) clearTimeout( this.myTime );

    // pause - happens when clicking pause, or seeking
    // that's why a timeout is used, so if we're seeking, once it starts playing again, we log it as a seek and kill the timer that would have logged the pause
    // we're only giving it 2 seconds to start playing again though. that should be enough for most users.
    // if we happen to log a pause during the seek - so be it.
    if( newState == '2' ) {
      this.myTime = setTimeout( function() {
        _yt[i].videoLog('pause');
        _yt[i].last = 'pause';
        _yt[i].scrubbing = false;
        }, 2000 );
      if( this.scrubbing == false ){
        this.last = 'pre-scrub';
        this.scrubbing = true;
      }
    }

    // play
    else if( newState == '1' ) {

      switch( this.last ) {

        case 'none':
          this.killTimers();
          this.startTimer = setInterval( this.startRun, 200 );
          break;

        case 'pause':
          this.myTime = setTimeout( function() {
            _yt[i].videoLog('play');
            _yt[i].last = 'play';
          }, 2000 );
          break;

        case 'pre-scrub':
          this.killTimers();
          this.scrubTimer = setInterval( this.scrubRun, 200 );
          break;
      }
    }

    // end
    else if( newState == '0' ) {
      this.last = 'none';
      this.videoLog('end');
    }
  }


  // have to use external calls here because these are set as timeouts, which makes "this" change context (apparently)
  this.scrubRun = function() {
    _yt[i].videoLog('seek');
    _yt[i].killTimers();
    _yt[i].last = 'scrub';
    _yt[i].scrubbing = false;
  }
  this.startRun = function() {
    _yt[i].videoLog('play');
    _yt[i].killTimers();
    _yt[i].last = 'start';
  }

  this.killTimers = function() {
    if( this.startTimer ) {
      clearInterval( this.startTimer );
      this.startTimer = null;
    }
    if( this.scrubTimer ){
      clearInterval( this.scrubTimer );
      this.scrubTimer = null;
    }
  }

  this.videoLog = function( action ) {
    clicky.video( action, this.videoTime(), this.videoURL(), this.videoTitle());
  }

  this.videoTime = function() {
    return Math.round( this.o.getCurrentTime() );
  }

  this.videoURL = function() {
    return this.o.getVideoUrl().split('&')[0]; // remove any extra parameters - we just want the first one, which is the video ID.
  }

  this.videoTitle = function() {
    // titles have to be defined in an external object
    if( window['_ytmeta'] ) return window['_ytmeta'][ this.id ].title || '';
  }
}

Надеюсь, кто-то в будущем сочтет это полезным, потому что задница была серьезной болью, чтобы заставить его работать!

Спасибо всем, кто разместил свои идеи здесь,:)

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...