What a hell is this?

JS walks into a bar. Bartender says:
— What a hell is this?
— undefined.

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

В прошлый раз я остановился на том, что такое this. В серии You don’t know JavaScript есть даже книжка, посвященная тому, что такое this, и на что она (переменная, или все таки оно?) ссылается внутри функции.

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

Само правило в конце поста, но чтобы показать что оно работает, заново вспомним What a hell is this.

Если вкратце, то вариантов всего четыре (перечисляются в порядке приоритета).

4 Варианта на что ссылается this

  1. При вызове функции как конструктора (с ключевым словом new), this ссылается на создаваемый объект.
function foo () {
this.a = 1;
}
var x = new foo();
console.log(x); // {a: 1}
console.log(x.a); // 1
console.log(a); // Uncaught ReferenceError: a is not defined(…)

При этом не имеет значения является ли вызываемая функция глобальной или методом уже существующего объекта (хотя бы потому что все глобальные функции, это методы объекта window) — при вызове с ключевым словом new, this будет ссылаться на создаваемый объект.

y = {
foo: function bar() {
this.a = 1;
}
};
var x = new y.foo();
console.log(x); // {a: 1}
console.log(x.a); // 1
console.log(a); // Uncaught ReferenceError: a is not defined(…)
console.log(y); // Object{}
console.log(y.a); // undefined

2. В том случае, если мы делаем явную привязку функции к методу при помощи методов call(), apply() или bind(), this указывает на тот объект, который передается как первый аргумент в метод call(), вызываемой функции.

function foo () {
this.a = 1;
}
var x = {};
foo.call(x);
console.log(x); // {a: 1}
console.log(x.a); // 1
console.log(a); // Uncaught ReferenceError: a is not defined(…)

То же самое с методами других объектов:

var y = {
foo: function bar() {
this.a = 1;
}
}
var x = {};
y.foo.call(x)
console.log(x); // {a: 1}
console.log(x.a); // 1
console.log(a); // Uncaught ReferenceError: a is not defined(…)
console.log(y); // Object{}
console.log(y.a); // undefined

Кстати, именно из-за такого поведения функции call(), те функции, которые мы передаем в setTimeOut, вызываются в глобальном контексте. Внутри setTimeOut, после работы таймера, функция вызывается при помощи call(), а первым параметром в call() передается undefined — в этом случае контекст вызова становится глобальным.

3. Кроме явной, у нас еще бывает и неявная привязка, в тех случаях, когда функция вызывается в контексте какого-то объекта, в качестве его метода. Тогда this указывает именно на этот объект.

function foo () {
this.a = 1;
}
var x = {bar: foo};
console.log(x.a); // undefined
x.bar();
console.log(x); // Object{a: 1, bar: foo () {...}}
console.log(x.a); // 1
console.log(a); // Uncaught ReferenceError: a is not defined(…)

В примере x.a принимает значение 1 только после вызова функции x.bar. Это работает даже если функция участвует в вычислениях.

function foo () {
this.a = 1;
return 2;
}
var y = {
bar: foo
}
var x = y.bar() + 1;
console.log(x); // 3
console.log(y.a); // 1

Здесь тоже действует правило с методами других объектов.

var y = {
foo: function bar() {
this.a = 1;
}
}
var x = {
f: y.foo
};
x.f();
console.log(x); // Object{a: 1, f: function bar () {...}}
console.log(x.a); // 1
console.log(a); // Uncaught ReferenceError: a is not defined(…)
console.log(y); // Object{}
console.log(y.a); // undefined

4. А когда ни одно из перечисленных выше правил не выполняется, this ссылается на глобальный объект window. Ну или становится undefined, если используется sctrict mode.

function foo () {
this.a = 1;
}
var x = foo(); // Мы присваиваем переменной x результат выполнения функции, которая не возвращает никакой результат;
console.log(x); // undefined
console.log(x.a); // Uncaught TypeError: Cannot read property 'a' of undefined(…)
console.log(a); // 1

sctrict mode

function foo () {
‘use strict’;
this.a = 1;
}
x = foo(); //Uncaught TypeError: Cannot set property ‘a’ of undefined(…)

А теперь про правило:

Внутри функции this ссылается на тот объект, который стоит слева перед вызовом функции, кроме тех случаев, когда используется call/apply/bind — в этих случаях this ссылается на тот объект, который стоит слева внутри скобок.

Вспоминая 4 правила выше:

  • При вызове функции как конструктора, слева от функции стоит ключевое слово new, this ссылается на новый, создаваемый объект.
  • Первый аргумент функции call так и называется в документации: thisArg, а раз он первый, то он будет стоять слева.
  • Какое бы значение мы ни давали методу объекта, если при вызове функции, слева от нее стоит какой-то объект — this будет ссылаться на него.
  • Если слева от функции не стоит никакого объекта, то значит это глобальная функция, а глобальные функции это методы глобального объекта window. И несмотря на то, что мы не видим объекта слева от точки — он есть.

Вернемся к предыдущему посту

Почему мы не могли включить функцию ParentClass в цепочку прототипов? Разве тогда ChildClass не видел бы свойство a, поднимаясь по цепочке прототипов?

function extend(child, parent) {
var emptyCtor = function() {};
emptyCtor.prototype = parent.prototype;
child.prototype = new emptyCtor;
child._super = parent;
return child;
};
var ParentClass = function() {
this.a = 1;
};
var ChildClass = extend(function() {}, ParentClass);
document.body.innerHTML = new ChildClass().a;

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

Вот вроде и все, что хотел сказать. Если еще не совсем надоело, то вопрос на понимание.

Что выведет следующий код и почему?

function foo() {
this.a;
}
var x;
foo.call(x);
console.log(x);
console.log(a);
One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.