← Курс/Как работает this в JavaScript?#131 из 257+40 XP

Как работает this в JavaScript?

Краткий ответ

this в JavaScript определяется не местом определения функции, а **способом её вызова**. Четыре основных правила: по умолчанию this — глобальный объект (или undefined в strict mode); при вызове метода объекта — объект перед точкой; при явном задании через call/apply/bind; при вызове через new — новый объект. Стрелочные функции не имеют собственного this — они захватывают его из лексического окружения при создании.

Полный разбор

Правило 1: Default binding (по умолчанию)

function show() {
  console.log(this)
}

show()  // window (браузер) или global (Node.js)
        // undefined в strict mode ('use strict')

В строгом режиме this по умолчанию — undefined, а не глобальный объект. Это сделано специально чтобы предотвращать случайное загрязнение глобальных переменных.

Правило 2: Implicit binding (метод объекта)

this — объект **перед последней точкой** при вызове:

const user = {
  name: 'Alice',
  greet() {
    console.log(`Привет, я ${this.name}`)
  }
}

user.greet()  // 'Привет, я Alice' — this = user

// Вложенный объект: this = объект перед точкой
const app = {
  user: user,
  run() { this.user.greet() }  // this.user = {name: 'Alice', greet: fn}
}

// Потеря контекста — классический баг!
const fn = user.greet  // функция без привязки к объекту
fn()  // 'Привет, я undefined' — this = global/undefined

Правило 3: Explicit binding (call/apply/bind)

function greet(greeting, punctuation) {
  console.log(`${greeting}, ${this.name}${punctuation}`)
}

const alice = { name: 'Alice' }
const bob   = { name: 'Bob' }

// call — аргументы через запятую, вызывает сразу
greet.call(alice, 'Привет', '!')     // 'Привет, Alice!'
greet.call(bob, 'Здравствуй', '.')   // 'Здравствуй, Bob.'

// apply — аргументы в массиве, вызывает сразу
greet.apply(alice, ['Хай', '?'])     // 'Хай, Alice?'

// bind — возвращает новую функцию с привязанным this (НЕ вызывает)
const greetAlice = greet.bind(alice, 'Добрый день')
greetAlice('!')  // 'Добрый день, Alice!'
greetAlice('.')  // 'Добрый день, Alice.'

Правило 4: new binding

При вызове функции через new:

1. Создаётся новый пустой объект

2. this указывает на этот объект

3. Выполняется тело функции

4. Новый объект возвращается автоматически

function Person(name, age) {
  this.name = name  // this = новый объект
  this.age = age
  // return this — неявно
}

const alice = new Person('Alice', 30)
console.log(alice.name)  // 'Alice'
console.log(alice.age)   // 30

Приоритет правил

new binding       > (высший приоритет)
explicit binding  >
implicit binding  >
default binding     (низший приоритет)

Стрелочные функции: нет собственного this

Стрелочная функция **не имеет** собственного this. Она захватывает this из окружающего лексического контекста при создании — и этот this никогда не изменяется.

const timer = {
  seconds: 0,

  // ПРОБЛЕМА: обычная функция теряет this
  startBroken() {
    setInterval(function() {
      this.seconds++  // this = global/undefined (не timer!)
      console.log(this.seconds)  // NaN или ошибка
    }, 1000)
  },

  // РЕШЕНИЕ 1: стрелочная функция захватывает this из startCorrect
  startArrow() {
    setInterval(() => {
      this.seconds++  // this = timer (захвачен из startArrow)
      console.log(this.seconds)  // 1, 2, 3...
    }, 1000)
  },

  // РЕШЕНИЕ 2: bind
  startBind() {
    setInterval(function() {
      this.seconds++
      console.log(this.seconds)
    }.bind(this), 1000)
  }
}

Стрелочные функции: когда НЕ использовать

// НЕ используй стрелочную функцию как метод объекта
const obj = {
  name: 'Тест',
  greet: () => {
    // this здесь = лексическое this снаружи (обычно global/undefined)
    console.log(this.name)  // undefined!
  }
}
obj.greet()  // undefined — потеря контекста!

// Используй обычную функцию для методов
const obj2 = {
  name: 'Тест',
  greet() {
    console.log(this.name)  // 'Тест' — правильно!
  }
}
obj2.greet()  // 'Тест'

Жёсткая привязка с bind — нельзя перебить

function greet() { return this.name }

const alice = { name: 'Alice' }
const bound = greet.bind(alice)

const bob = { name: 'Bob' }
bound.call(bob)    // 'Alice' — bind сильнее call!
bound.apply(bob)   // 'Alice' — bind сильнее apply!

// Единственное исключение — new (для полифиллов)

Связанные уроки курса

  • this — базовое понимание
  • bind, call, apply
  • Стрелочные функции
  • Как отвечать на собеседовании

    **Сразу скажи главное**: "this определяется не где написана функция, а как она вызвана."

    **Объясни 4 правила по порядку**: default → implicit (метод) → explicit (call/apply/bind) → new. Для каждого — 1-2 предложения и короткий пример.

    **Стрелочные функции** — отдельный пункт: "Стрелочные функции не имеют собственного this. Они захватывают this из лексического окружения при создании."

    **Покажи классический баг**: потеря this в setTimeout/setInterval и два способа исправить (bind и стрелка).

    **Время ответа**: 3-4 минуты.

    Красные флаги ответа

    1. **"this — это сама функция"** или **"this — это объект, в котором определена функция"** — оба варианта неверны. this зависит от вызова, не от определения.

    2. **Не знать про потерю контекста** — это самый частый баг с this в реальном коде. "Передал метод в setTimeout и сломалось" — классика. Незнание этого выдаёт джуниора.

    3. **Не понимать разницу call/apply/bind** — call и apply вызывают сразу, bind возвращает функцию. Путаница — признак поверхностного знания.

    Примеры

    Все четыре правила this + стрелочные функции + решения для потери контекста

    'use strict'
    // В strict mode this по умолчанию = undefined (безопаснее)
    
    // ===== ПРАВИЛО 1: Default binding =====
    function defaultThis() {
      return typeof this  // 'undefined' в strict mode
    }
    console.log('Default this:', defaultThis())  // 'undefined'
    
    // ===== ПРАВИЛО 2: Implicit binding =====
    const user = {
      name: 'Alice',
      role: 'admin',
    
      greet() {
        return `Привет, ${this.name} (${this.role})`
      },
    
      getInfo() {
        return { name: this.name, role: this.role }
      }
    }
    
    console.log('\nImplicit binding:')
    console.log(user.greet())  // 'Привет, Alice (admin)'
    
    // Потеря контекста
    const greetFn = user.greet
    try {
      console.log(greetFn())  // TypeError в strict mode
    } catch(e) {
      console.log('Потеря контекста:', e.constructor.name)  // TypeError
    }
    
    // ===== ПРАВИЛО 3: Explicit binding =====
    console.log('\nExplicit binding:')
    
    function introduce(greeting, lang) {
      return `[${lang}] ${greeting}, я ${this.name}!`
    }
    
    const alice = { name: 'Alice' }
    const bob   = { name: 'Bob' }
    
    // call — аргументы через запятую
    console.log(introduce.call(alice, 'Привет', 'RU'))     // [RU] Привет, я Alice!
    console.log(introduce.call(bob, 'Hello', 'EN'))        // [EN] Hello, я Bob!
    
    // apply — аргументы в массиве
    console.log(introduce.apply(alice, ['Хай', 'RU']))     // [RU] Хай, я Alice!
    
    // bind — возвращает новую функцию
    const aliceGreeter = introduce.bind(alice, 'Добрый день')
    console.log(aliceGreeter('RU'))   // [RU] Добрый день, я Alice!
    console.log(aliceGreeter('EN'))   // [EN] Добрый день, я Alice!
    
    // bind нельзя перебить
    console.log(aliceGreeter.call(bob, 'RU'))   // [RU] Добрый день, я Alice! (не Bob!)
    
    // ===== ПРАВИЛО 4: new binding =====
    console.log('\nnew binding:')
    
    function Animal(name, sound) {
      this.name = name
      this.sound = sound
      this.speak = function() {
        return `${this.name} говорит: ${this.sound}!`
      }
    }
    
    const cat = new Animal('Кот', 'Мяу')
    const dog = new Animal('Пёс', 'Гав')
    console.log(cat.speak())  // Кот говорит: Мяу!
    console.log(dog.speak())  // Пёс говорит: Гав!
    
    // ===== СТРЕЛОЧНЫЕ ФУНКЦИИ =====
    console.log('\nСтрелочные функции:')
    
    const timer = {
      name: 'Timer',
      ticks: 0,
    
      // ПРОБЛЕМА: обычная функция в колбэке теряет this
      // startBroken() { setTimeout(function() { this.ticks++ }, 0) }
    
      // РЕШЕНИЕ 1: стрелочная функция
      startArrow() {
        // this здесь = timer (из лексического контекста startArrow)
        const tick = () => {
          this.ticks++
          return `${this.name}: tick #${this.ticks}`
        }
        return tick()
      },
    
      // РЕШЕНИЕ 2: bind
      startBind() {
        const tick = function() {
          this.ticks++
          return `${this.name}: tick #${this.ticks}`
        }.bind(this)
        return tick()
      }
    }
    
    console.log(timer.startArrow())  // Timer: tick #1
    console.log(timer.startBind())   // Timer: tick #2
    
    // Стрелочная функция как метод — ПЛОХО
    const broken = {
      name: 'broken',
      greet: () => {
        // this здесь = внешнее this (undefined в strict mode или global)
        return `this.name = ${typeof this === 'undefined' ? 'undefined' : String(this)}`
      }
    }
    console.log('\nСтрелка как метод (плохо):', broken.greet())