Это мои заметки о this
Теперь все имеет смысл!, второй главе Кайла Симпсона в книге Вы не знаете Javascript: это и прототипы объектов.
Call-сайт
сайт вызова – это место в коде, где вызывается функция (а не там, где она объявлена).
Стек вызовов – это стек функций, которые были вызваны для перехода к текущему моменту выполнения.
При изучении того, на что ссылается this
, вы должны смотреть на сайт вызова до текущей выполняемой функции. Это можно найти в стеке вызовов.
Вы можете визуализировать стек вызовов в уме как цепочку вызовов функций по порядку. Например:
function baz() { // call-stack is: `baz` // so, our call-site is in the global scope console.log( "baz" ); bar(); // <-- call-site for `bar` } function bar() { // call-stack is: `baz` -> `bar` // so, our call-site is in `baz` console.log( "bar"); } baz(); // <-- call-site for `baz`
Вы можете легко найти сайт вызова, поместив debugger;
в первую строку рассматриваемой функции, а затем проверив встроенные инструменты разработчика вашего браузера. Отладчик JS покажет сайт вызова во втором элементе сверху стека вызовов.
Ничего, кроме правил
Привязка по умолчанию
Привязка по умолчанию для this
указывает на глобальный объект (если содержимое функции с this
не выполняется в strict mode
).
Неявная привязка
Когда функция с this
принадлежит объекту, правило неявной привязки говорит, что объект — это то, что используется для привязки this
вызова функции. Этот объект обычно называют «контекстным объектом».
Только верхний/последний уровень цепочки ссылок на свойства объекта имеет значение для call-site. Например:
function foo() { console.log( this.a ); } var obj2 = { a: 42, foo: foo }; var obj1 = { a: 2, obj2: obj2 }; obj1.obj2.foo(); // 42
Одна из распространенных проблем с привязкой this
возникает, когда неявно связанная функция теряет эту привязку и возвращается к привязке по умолчанию. Например:
function foo() { console.log( this.a ); } var obj = { a: 2, foo: foo }; var bar = obj.foo; // function reference/alias! var a = "oops, global"; // `a` also property on global object bar(); // "oops, global"
Важно помнить, что obj.foo
— это ссылка на функцию foo
. Назначение obj.foo
функции bar
— это просто присвоение ссылки функции foo
функции bar
. Неявная привязка foo
к obj
теряется, и поэтому глобальное свойство a
регистрируется в консоли.
Это также часто происходит при передаче функции обратного вызова. Если бы у нас было:
function doFoo(fn) { fn(); // <-- call-site! }
И прошел obj.foo
так:
dooFoo( obj.foo );
"oops, global"
будет зарегистрирован в консоли. fn
— это еще одна ссылка на foo
.
То же самое происходит при использовании функции, встроенной в язык;
setTimeout( obj.foo, 100 ); // "oops, global"
Одним из распространенных реальных примеров неявно потерянной привязки является среда ReactJS. Обработчик событий в теле класса, который передается в JSX, потеряет свою неявную привязку, если вы не привяжете обработчик жестко в конструкторе класса или не определите обработчик с помощью функции стрелки. Более подробно вы можете прочитать об этом в моих заметках в туториале по ReactJS на сайте egghead.io.
Явная привязка
Вы можете прямо указать, что вы хотите, чтобы ваш this
был с помощью методов call(..)
и apply(..)
. Они берут объект для использования в качестве this
в первом параметре, а затем вызывают функцию с указанным this
. Это называется явной привязкой.
Например:
function foo() { console.log( this.a ); } var obj = { a: 2 }; foo.call(obj); // 2
Жесткая привязка — это разновидность явной привязки, при которой вы присваиваете явную привязку (с помощью call(..)
или apply(..)
) ссылке на функцию.
Например:
function foo() { console.log( this.a ); } var obj = { a: 2 }; var bar = function() { foo.call( obj ); }; bar(); // 2 setTimeout( bar, 100 ); // 2
bar
жестко связывает this
foo
с obj
.
Жесткая привязка — настолько распространенный шаблон, что он предоставляется как встроенная утилита начиная с ES5: Function.prototype.bind
.
bind(..)
возвращает новую функцию, которая жестко запрограммирована для вызова исходной функции с указанным контекстом this
.
Например:
function foo(something) { console.log( this.a, something ); return this.a + something; } var obj = { a: 2 }; var bar = foo.bind( obj ); var b = bar( 3 ); // 2 3 console.log( b ); // 5
new
Привязка
В JS конструкторы — это просто функции, которые вызываются с оператором new
перед ними. Они не привязаны к классам, не создают экземпляры классов и не являются специальными типами функций. Это обычные функции, которые перехватываются использованием new
в их вызове. Любые функции могут быть вызваны с new
перед ним, что делает его вызовом конструктора.
Когда функция вызывается с new
перед ней, автоматически выполняются следующие действия:
- совершенно новый объект создается (иначе, конструируется) из воздуха
- новый созданный объект является [[Prototype]]-связанным
- вновь созданный объект устанавливается как привязка
this
для этого вызова функции - если функция не вернет свой собственный альтернативный объект,
new
вызванный вызов функции автоматически вернет вновь созданный объект
Привязка new
— это когда мы вызываем конструктор, а новый объект является привязкой для this
. Например:
function foo(a) { this.a = a; } var bar = new foo( 2 ); console.log( bar.a ); // 2
Запустив bar;
в консоли, вы увидите Object { a: 2 }
.
Все по порядку
Это правила определения this
по порядку старшинства.
- Если функция вызывается с
new
(новая привязка),this
— это вновь созданный объект.
var bar = new foo()
2. Если функция вызывается с call
или apply
(явная привязка), даже внутри bind
жесткой привязки this
является явно указанным объектом.
var bar = foo.call( obj2 )
3. Если функция вызывается с «владеющим» или содержащим (контекстным) объектом, this
является этим объектом контекста.
var bar = obj1.foo()
4. Если ничего из вышеперечисленного не применимо, this
является объектом global
или undefined
в строгом режиме (привязка по умолчанию).
var bar = foo()
Исключения привязки
Если вы передаете null
или undefined
в качестве параметра привязки this
для call
, apply
или bind
, то к вызову применяется правило привязки по умолчанию.
Обычно apply(..)
используется для распространения массивов значений в качестве параметров вызова функции. bind(..)
также можно использовать для каррирования параметров. В ES6 есть оператор распространения ...
, который делает то же самое без использования apply(..)
, но bind(..)
по-прежнему необходим для каррирования.
Одним из способов избежать случайного изменения объекта global
с правилом привязки по умолчанию является использование пустого объекта-заполнителя, такого как ø = Object.create(null)
.
Будьте осторожны с «косвенными ссылками» на функции. В этом случае применяется правило привязки по умолчанию. Например:
function foo() { console.log( this.a ); } var a = 2; var o = { a: 3, foo: foo }; var p = { a: 4 }; o.foo(); // 3 (p.foo = o.foo)(); // 2
o.foo
— это просто ссылка на функцию foo
в глобальной области видимости, поэтому глобальная переменная a
записывается в консоль.
Лексический this
Стрелочные функции принимают привязку this
из объемлющей (функциональной или глобальной) области. Например:
function foo() { return (a) => { console.log( this.a ); }; } var obj1 = { a: 2 }; var obj2 = { a: 3 }; var bar = foo.call( obj1 ); bar.call( obj2 ); // 2, not 3!
Стрелочная функция лексически захватывает все foo()
s this
во время вызова. bar
this
связан с obj1
, так как foo()
был this
связан с obj1
с call(..)
. Лексическая привязка стрелочной функции не может быть переопределена.
Программа может эффективно использовать как лексический, так и this
стиль кода, но смешивание двух механизмов внутри одной и той же функции усложняет сопровождение кода.