Сохранение ссылки на "this" в функциях прототипа JavaScript

Я просто использую прототипный JavaScript, и мне трудно понять, как сохранить ссылку this на главный объект изнутри функции прототипа при изменении области действия. Позвольте мне проиллюстрировать, что я имею в виду (я использую jQuery здесь):

MyClass = function() {
  this.element = $('#element');
  this.myValue = 'something';

  // some more code
}

MyClass.prototype.myfunc = function() {
  // at this point, "this" refers to the instance of MyClass

  this.element.click(function() {
    // at this point, "this" refers to the DOM element
    // but what if I want to access the original "this.myValue"?
  });
}

new MyClass();

Я знаю, что я могу сохранить ссылку на основной объект, выполнив это в начале myfunc:

var myThis = this;

а затем используя myThis.myValue для доступа к основному объекту. Но что происходит, когда у меня есть целая группа прототипов на MyClass? Должен ли я сохранить ссылку на this в начале каждого из них? Похоже, что должен быть более чистый путь. А как насчет ситуации вроде этого:

MyClass = function() {
  this.elements $('.elements');
  this.myValue = 'something';

  this.elements.each(this.doSomething);
}

MyClass.prototype.doSomething = function() {
  // operate on the element
}

new MyClass();

В этом случае я не могу создать ссылку на основной объект с помощью var myThis = this;, потому что даже исходное значение this в контексте doSomething является объектом jQuery, а не MyClass объект.

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

Любые предложения? Есть ли чистый способ сделать то, что мне нужно? Или мой дизайн дизайна ошибочен?

Ответы

Ответ 1

Для сохранения контекста метод bind действительно полезен, теперь он является частью недавно выпущенного ECMAScript 5th Edition Спецификация, реализация этой функции прост (всего 8 строк):

// The .bind method from Prototype.js 
if (!Function.prototype.bind) { // check if native implementation available
  Function.prototype.bind = function(){ 
    var fn = this, args = Array.prototype.slice.call(arguments),
        object = args.shift(); 
    return function(){ 
      return fn.apply(object, 
        args.concat(Array.prototype.slice.call(arguments))); 
    }; 
  };
}

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

MyClass.prototype.myfunc = function() {

  this.element.click((function() {
    // ...
  }).bind(this));
};

Другой пример:

var obj = {
  test: 'obj test',
  fx: function() {
    alert(this.test + '\n' + Array.prototype.slice.call(arguments).join());
  }
};

var test = "Global test";
var fx1 = obj.fx;
var fx2 = obj.fx.bind(obj, 1, 2, 3);

fx1(1,2);
fx2(4, 5);

В этом втором примере мы можем наблюдать больше о поведении bind.

Он в основном генерирует новую функцию, которая будет ответственна за вызов нашей функции, сохраняя контекст функции (this value), который определяется как первый аргумент bind.

Остальные аргументы просто передаются нашей функции.

Обратите внимание, что в этом примере функция fx1 вызывается без какого-либо контекста объекта (obj.method()), так же как и простой вызов функции, в этом типе интрокации ключевое слово this внутри будет ссылаться на Global объект, он будет предупреждать "глобальный тест".

Теперь fx2 - это новая функция, сгенерированная методом bind, она вызовет нашу функцию, сохраняя контекст и правильно передавая аргументы, будет предупреждать "obj test 1, 2, 3, 4, 5" потому что мы вызывают его добавление двух дополнительных аргументов, он уже привязал первые три.

Ответ 2

Для вашего последнего примера MyClass вы можете сделать это:

var myThis=this;
this.elements.each(function() { myThis.doSomething.apply(myThis, arguments); });

В функции, которая передается в each, this ссылается на объект jQuery, как вы уже знаете. Если внутри этой функции вы получаете функцию doSomething из myThis, а затем вызываете метод apply для этой функции с массивом аргументов (см. apply функция и arguments variable), тогда this будет установлен на myThis в doSomething.

Ответ 3

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

Рассмотрим следующее:

var f=function(){
    var context=this;
}    

f.prototype.test=function(){
    return context;
}    

var fn=new f();
fn.test(); 
// should return undefined because the prototype definition
// took place outside the scope where 'context' is available

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

Чтобы создать методы, находящиеся в области локальной переменной, нам необходимо прямо определить их как членов функции и избавиться от прототипической ссылки:

var f=function(){
    var context=this;    

    this.test=function(){
        console.log(context);
        return context;
    };
}    

var fn=new(f);
fn.test(); 
//should return an object that correctly references 'this'
//in the context of that function;    

fn.test().test().test(); 
//proving that 'this' is the correct reference;

Вы можете быть обеспокоены тем, что, поскольку методы не создаются прототипом, разные экземпляры могут не разделяться данными. Чтобы продемонстрировать это, рассмотрим следующее:

var f=function(val){
    var self=this;
    this.chain=function(){
        return self;
    };    

    this.checkval=function(){
        return val;
    };
}    

var fn1=new f('first value');
var fn2=new f('second value');    

fn1.checkval();
fn1.chain().chain().checkval();
// returns 'first value' indicating that not only does the initiated value remain untouched,
// one can use the internally stored context reference rigorously without losing sight of local variables.     

fn2.checkval();
fn2.chain().chain().checkval();
// the fact that this set of tests returns 'second value'
// proves that they are really referencing separate instances

Другим способом использования этого метода является создание синглетонов. Чаще всего наши функции javascript не создаются более одного раза. Если вы знаете, что вам никогда не понадобится второй экземпляр одной и той же функции, тогда существует сокращенный способ их создания. Однако будьте осторожны: lint будет жаловаться, что это странная конструкция, и задайте вопрос о том, что вы используете ключевое слово "новый":

fn=new function(val){
    var self=this;
    this.chain=function(){
        return self;
    };        

    this.checkval=function(){
        return val;
    };  
}    

fn.checkval();
fn.chain().chain().checkval();

Pro-х: Преимущества использования этого метода для создания объектов функции многочисленны.

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

Con-х: Есть некоторые недостатки в использовании этого метода. Я не претендую на полноту:)

  • Поскольку методы определяются как члены объекта, а не прототипы - наследование может быть достигнуто с использованием определения члена, но не прототипических определений. Это фактически неверно. Такое же прототипическое наследование может быть достигнуто, действуя на f.constructor.prototype.

Ответ 4

Вы можете установить область действия, используя функции call() и apply()

Ответ 5

Поскольку вы используете jQuery, стоит отметить, что this уже поддерживается jQuery:

$("li").each(function(j,o){
  $("span", o).each(function(x,y){
    alert(o + " " + y);
  });
});

В этом примере o представляет li, тогда как y представляет дочерний элемент span. И с $.click() вы можете получить область видимости с объекта event:

$("li").click(function(e){
  $("span", this).each(function(i,o){
    alert(e.target + " " + o);
  });
});

Где e.target представляет li, а o представляет дочерний элемент span.

Ответ 6

Другим решением (и моим любимым способом в jQuery) является использование jQuery, предоставленного 'e.data' для передачи 'this'. Затем вы можете сделать это:

this.element.bind('click', this, function(e) {
    e.data.myValue; //e.data now references the 'this' that you want
});

Ответ 7

Вы можете создать ссылку на этот объект или использовать метод with (this). Более поздняя версия чрезвычайно полезна, когда вы используете обработчики событий, и у вас нет способа передать ссылку.

MyClass = function() {
    // More code here ...
}

MyClass.prototype.myfunc = function() {       
    // Create a reference
    var obj = this;
    this.element.click(function() {
        // "obj" refers to the original class instance            
        with (this){
            // "this" now also refers to the original class instance
        }
    });

}