Если вы приходите в мир JavaScript из классических объектно-ориентированных языков, таких как Java или C ++, вы были бы очень рады увидеть 'class' и 'this 'на языке. Но прежде чем вы перейдете к написанию кода JavaScript, похожего на Java, вы должны знать, что класс JavaScript - это не тот класс, который вы знали раньше. Значение this в JavaScript отличается.

Сначала давайте посмотрим на пример кода:

class Timer{
  constructor(second){
    this._second = second;
    document.getElementById(‘timer’).innerHTML = second;
  }
  _process(){
    this._second -= 1;
    document.getElementById(‘timer’).innerHTML = this._second;
    if (this._second <= 0){
      this.stop();
    }
  }
  start() {
    this._interval = setInterval(function doSomething(){
      this._process();
    }, 1000);
  }
  stop() {
    clearInterval(this._interval);
  }
}
var t1 = new Timer(10);
t1.start();

Выше кода объявлен класс Timer. Класс Timer имеет конструктор и 2 открытых метода: start () и stop (). Конструктор принимает число в качестве параметра.

И он создает объект Timer и вызывает метод start (). Метод start () устанавливает интервал для вызова функции _process () каждые 1000 миллисекунд.

Но если вы запустите этот код, вместо этого вы не увидите, что в браузере ничего не происходит, а в консоли разработки появится сообщение «Ошибка неперехваченного типа: this_process не является функцией».

Эта ошибка вызвана методом start (). Строка с this ._process () является проблемой. этот объект не имеет метода _process. Если вы используете отладчик или добавите console.log (this); прямо над this._process ();, вы увидите, что this - глобальный объект.

Почему так происходит? Это должен быть объект Timer? Чтобы понять это явление, вам необходимо понять, как this работает в JavaScript.

Как определить значение слова "это"

В отличие от классических объектно-ориентированных языков, this не означает сам объект. this в JavaScript - это скорее сайт вызова. Я объясню, что означает сайт вызова позже. Когда JavaScript видит это ключевое слово, он задает эти три вопроса, чтобы определить его значение, в том же порядке, как показано ниже.

  1. Функция вызывается с помощью нового ключевого слова?
  2. Вызывается ли функция с явной привязкой?
  3. Вызывается ли функция с содержащим объектом (неявная привязка)?
  4. Тогда это значение по умолчанию: global или undefined в строгом режиме.

Давайте рассмотрим каждый случай.

По умолчанию: глобальный объект или неопределенный

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

function whatIsThis(){
  console.log(this);
}
whatIsThis();

Приведенный выше код отобразит объект окна на консоли.

Но если ваш код работает в строгом режиме, this будет неопределенным.

Неявная привязка

Если вы вызываете функцию как метод объекта, this привязывается к объекту, содержащему функцию.

function sayName(){
  console.log(this._name);
}
var foo = {
  _name: "Foo",
  sayName: sayName
}
var bar = {
  _name: "Bar",
  sayName: sayName
}
foo.sayName();
// Foo
bar.sayName();
// Bar
sayName();
// undefined

Как видно из приведенного выше кода, значение this определяется динамически. Когда функция sayName () вызывается внутри объекта foo, this связывается с объектом foo. Но значение this становится объектом bar, когда функция вызывается внутри объекта bar. Таким образом, значение this зависит от того, откуда оно было вызвано. Вы можете думать об этом как о сайте для звонков.

Когда sayName () вызывается без какого-либо объекта, this становится глобальным объектом (или неопределенным, если это строгий режим).

Явная привязка

Вы можете принудительно установить значение this с помощью функции. Call () или. Apply (). Или вы можете создать функцию, которая всегда привязывается к определенному объекту, с помощью функции. Bind (). Когда вы вызываете функцию с помощью .call (thisObj) или .apply (thisObj), значение this внутри функции будет thisObj. Давайте посмотрим на код:

function sayName(){
  console.log(this._name);
}
var foo = {
  _name: "Foo",
  sayName: sayName
}
var bar = {
  _name: "Bar",
  sayName: sayName
}
sayName.call(bar);
// Bar
sayName.call(foo);
// Foo
foo.sayName.call(bar);
// Bar
bar.sayName.call(foo);
// Foo

Обратите внимание, что в последней строке кода, даже когда sayName вызывается внутри объекта bar,. call (foo) переопределяет значение this быть foo.

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

function sayName(){
 console.log(this._name);
}
var foo = {
 _name: “Foo”,
}
var sayFoo = sayName.bind(foo);
var bar = {
 _name: “Bar”,
 sayName: sayName,
 sayFoo: sayFoo
}
sayFoo();
// Foo
bar.sayFoo();
// Foo
sayFoo.call(bar);
// Foo
bar.sayName();
// Bar

Функция sayFoo () - это недавно созданная функция, основанная на sayName. Но он был создан с помощью метода .bind (). Итак, this значением в sayFoo всегда будет объект foo.

Когда sayFoo вызывается в объекте bar, значение this остается как foo. объект. Даже когда он вызывается с помощью функции .call (bar), это все равно foo.

Функция или класс с новым ключевым словом

Когда функция вызывается с ключевым словом new, она выполняет следующие действия:

  1. Создает пустой объект {}
  2. Привязывает this к вновь созданному пустому объекту
  3. Запускает функцию или конструктор
  4. Возвращает вновь созданный объект

Давайте посмотрим на пример определения функции и класса.

function Beer(alcohol) {
  this._happy = true;
  this._focus = false;
  this._alcoholContent = alcohol;
}

or

class Beer{
  constructor(alcohol) {
    this._happy = true;
    this._focus = false;
    this._alcoholContent = alcohol;
  }
}

Когда вы вызываете функцию Пиво с новым объектом, подобным этому:

var beer = new Beer(5);

Он (1) создаст пустой объект, (2) заполнит объект свойствами _happy, _focus и _alcoholContent и (3) вернет вновь созданный объект. Возвращенный объект будет выглядеть так:

{
  _happy:          true,
  _focus:          false,
  _alcoholContent: 5
}

Вернемся к нашей проблеме с таймером

class Timer{
  /* ... omitted ... */
start() {
    this._interval = setInterval(function doSomething(){
      this._process();
    }, 1000);
  }
/* ... omitted ... */
var t = new Timer(10);
t.start();

В методе start () есть два ключевых слова this. Похоже, что оба ключевых слова this относятся к одному и тому же объекту Timer, но имеют разное значение.

Когда вы вызываете t.start (), значение this становится t. И функция start () вызовет setInterval () с doSomething (1-й аргумент) и 1000 миллисекунд (2-й аргумент). И возвращаемое значение intervalId будет сохранено в this ._interval или t._interval.

Через секунду, когда вызывается doSomething (), функция больше не привязана к объекту t. Вы можете задать 3 вопроса, чтобы определить ценность this. (1) Вызывается ли он с помощью нового ключевого слова? Нет. (2) Является ли функция явно привязанной к объекту? Нет. (3) Вызывается ли он внутри содержащего объекта? Нет. Ответ на все эти 3 вопроса: Нет. Таким образом, значение this становится глобальным (или неопределенным). А в глобале нет метода _process (). Вот почему мы видим сообщение об ошибке.

Решение проблемы

Как мы можем решить проблему, если this является динамическим? Одно из решений - создать функцию, которая связывает this с объектом t. Для этого вы можете использовать функцию .bind ().

start() {
    var boundFunction = function doSomething(){
      this._process();
    }.bind(this);
    this._interval = setInterval(boundFunction, 1000);
  }

BoundFunction - это новая функция, созданная на основе doSomething () с помощью .bind (this). В boundFunction значение this всегда будет таким же this, что и в функции start (). Итак, когда вы звоните

t.start();

Значение this - t внутри функции start (), и вы заставляете this внутри boundFunction должно быть t. Поэтому всякий раз, когда вызывается boundFunction, this всегда будет ссылаться на t.

Заключение (TL; DR;)

JavaScript имеет другое значение this. В классическом объектно-ориентированном языке, таком как Java или C ++, this означает сам объект. Однако в JavaScript this имеет более близкое значение для вызова сайта (откуда функция была вызвана).

Вы можете определить значение слова это, задав эти 3 вопроса:

  1. Функция вызывается с помощью нового ключевого слова? Тогда this - вновь созданный пустой объект
  2. Вызывается ли функция с явной привязкой? Тогда this является аргументом thisArg из .call (thisArg), .apply (thisArg) или .bind (thisArg)
  3. Вызывается ли функция внутри содержащего объекта? Тогда this - содержащий объект.
  4. Если все ответы отрицательные, по умолчанию используется this: global или undefined в строгом режиме.

И исправленная версия кода таймера такая:

class Timer{
  constructor(second){
    this._second = second;
    document.getElementById(‘timer’).innerHTML = second;
  }
_process(){
    this._second -= 1;
    document.getElementById(‘timer’).innerHTML = this._second;
    if (this._second <= 0){
      this.stop();
    }
  }
start() {
    this._interval = setInterval(function doSomething(){
      this._process();
    }.bind(this), 1000);
  }
stop() {
    clearInterval(this._interval);
  }
}
var t1 = new Timer(10);
t1.start();