79

Объясните, пожалуйста, почему после присвоения var f = obj1.f теряется контекст вызова и выводится undefined?

var obj1 = {

x: 3,

f: function() { return (this.x); } };

alert(obj1.f()); var f = obj1.f; alert(f());

Grundy
  • 81,538
Nick
  • 949
  • 4
    Встречный вопрос, а почему вы решили, что контекст вызова должен сохраниться? – Dmitriy Simushev Jun 14 '16 at 18:12
  • потому что контекст зависит от способа вызова функции и ее создания – Grundy Jun 14 '16 at 18:12
  • 7
    Потому что функции в javascript (в отличие от некоторых других языков) существуют сами по себе, вне привязки к объекту.

    В первом случае Вы вызываете функцию как метод объекта. Во втором случае, достаете функцию из объекта, а после вызываете саму по себе.

    https://ru.wikipedia.org/wiki/%D0%9E%D0%B1%D1%8A%D0%B5%D0%BA%D1%82_%D0%BF%D0%B5%D1%80%D0%B2%D0%BE%D0%B3%D0%BE_%D0%BA%D0%BB%D0%B0%D1%81%D1%81%D0%B0

    – Утка Учится Укрываться Jun 14 '16 at 18:13
  • 7
    Коллеги, вопрос валидный, может быть, кто-нибудь даст развёрнутый ответ? Было бы полезно тем, кто не спец в JS (например, мне). – VladD Jun 14 '16 at 18:46
  • Я еще не спец в JS(пока), но пользователь Утка дал хорошее объяснение, мне стало все понятно. Так, что спасибо) – Nick Jun 14 '16 at 18:51
  • Если вам дан исчерпывающий ответ, отметьте его как верный (галка напротив выбранного ответа). – Nicolas Chabanovsky Jun 15 '16 at 05:55
  • @NicolasChabanovsky исчерпывающий ответ был дан в комментарии, вот в чем проблема :) – Pavel Mayorov Jun 15 '16 at 05:56
  • 3
    @VladD вопрос валидный, но ответ на него есть в каждом первом (ну может в каждом втором) учебнике по JS. – Alexey Ten Jun 15 '16 at 07:35
  • Вынес ответ в ответ – Утка Учится Укрываться Jun 15 '16 at 07:35
  • @AlexeyTen: То же самое я мог бы сказать о половине, если не больше, вопросов по любому тегу :-) – VladD Jun 15 '16 at 07:39
  • 6
    @AlexeyTen, процентов 80 вопросов по js которые появляются здесь (и на других сайтах) есть в каждом втором учебнике по js. И все равно будут появляться. Не отвечать на них чтоли? – Утка Учится Укрываться Jun 15 '16 at 07:39
  • по хорошему надо закрывать дублями. – Alexey Ten Jun 15 '16 at 07:41
  • @AlexeyTen, с радостью бы, но для этого надо сделать эталонный ответ на который можно будет с чистой совестью ссылаться. Сможете?) – Утка Учится Укрываться Jun 15 '16 at 07:47
  • @Утка мне очень не хочется переписывать пол-учебника http://learn.javascript.ru/objects-more – Alexey Ten Jun 15 '16 at 07:50
  • @AlexeyTen к сожалению, насколько я знаю, здесь нельзя закрыть дублем со ссылкой на learn.javascript.ru – Утка Учится Укрываться Jun 15 '16 at 07:53
  • @VladD, попробовал развернуть :) – Grundy Jun 17 '16 at 10:48
  • 1
    Если вам дан исчерпывающий ответ, отметьте его как верный (галка напротив выбранного ответа) – Grundy Jan 17 '17 at 15:37
  • ассоциация: http://stackoverflow.com/questions/3127429/how-does-the-this-keyword-work – Grundy Feb 04 '17 at 09:44
  • пожалуйста, обратите внимание на формулировку вопроса...и сравните с кодом который я привел...на каком вы примере вообще собрались обсуждать потерю контекста??? тут в вопросе грубая синтаксическая ошибка, ребята я тут просто новичёк, и мой самый первый и короткий ответ как раз и выявляет проблему приведенного кода, но когда я вижу обсуждение не по сути, и при этом еще и минусуют, извините но мне кажется, что это не правильно. И вопрос должен, для начала, хотя бы работать...что да действительно потеря контекста, тем более на этом примере... – Evgeniy Zaritskiy Apr 29 '17 at 07:18
  • Cпасибо, что вопрос сделали, все таки, рабочим >> Выполнить код. Теперь видно undefined, который остаётся, по непонятной причине потери контекста вызова. – Evgeniy Zaritskiy May 03 '17 at 18:05
  • @EvgeniyZaritskiy, о том, что функция возвращает undefined при выполнении было написано и раньше, в сниппет я его перенес для того, чтобы показать, что тут нет синтаксической ошибки, о которой ты говорил в комментариях к своему ответу: код абсолютно рабочий. – Grundy May 04 '17 at 07:29
  • 1
    Было бы очень хорошо принять ответ. – Qwertiy Jan 31 '18 at 20:17
  • Все просто, для ответа надо понимать работу интерпретатора. Контекст определяется указателем с которого вызывается. obj.f - указатель obj f - указатель document – Azamat Galiyev Nov 02 '17 at 08:26
  • Не document, а window (и то не всегда). И отформатируй ответ нормально. – Qwertiy Jan 31 '18 at 20:16

5 Answers5

109

Значение this внутри функции зависит от того как вызывается функция и как создана функция.

Как вызывается?

Вызвать функцию можно следующими способами:

Вызов функции

Если есть обычная функция, в большинстве случаев значением this будет глобальный объект (для браузера window). При использовании "strict mode" - undefined.

var f = function (){
  console.log('common:',this.toString());
};
f();

var fStrict = function (){ "use strict"; console.log('strict:', this); }; fStrict();

Обычно так вызываются функции обратного вызова(callback), вот почему значение this в них кажется неожиданным.

Например, при передаче функции в .addEventListener, значением this будет элемент, которому добавлен обработчик.


Вызов метода

Метод - это функция находящаяся в объекте.

var Obj = {toString:function(){ return "[object Obj]";}};
Obj.f = function (){
  console.log('common:',this.toString());
};
Obj.f();

Obj.fStrict = function (){ "use strict"; console.log('strict:', this.toString()); }; Obj.fStrict();

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


Вызов конструктора

Функцию можно вызывать в качестве конструктора, для этого перед вызовом нужно использовать оператор new: new Foo()

function Foo(name){
  this.name = name;
}

var foo = new Foo('foo'); console.log(foo);

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

Особенность: при использовании наследования и классов из ES2015 обращение к this до вызова super в зависимости от браузера вызовет исключение о попытке обратиться к необъявленной/неинициализированной переменной.

class A {}
class B extends A {
  constructor(){
    console.log(this);
  }
}
var b = new B();

Вызов с помощью методов call и apply

При использовании функций call и apply можно задать значение this напрямую, передав его первым параметром.

var f = function (){
  console.log('common:',this);
};
f.call({o:'object'});

var fStrict = function (){ "use strict"; console.log('strict:', this); }; fStrict.apply({o:'object'});

В библиотеках вроде jQuery с помощью этих функций вызываются коллбэки передаваемые в различные функции, например: each, map, on и другие. В качестве this в этом случае устанавливается текущий элемент коллекции, либо html-элемент.


Вызов в качестве коллбэков в функциях обработки массивов

Некоторые встроенные функции для объекта типа Array позволяют так же напрямую указать значение this для передаваемого коллбэка:

var specialMap = {'b':'specialB','d':'specialD'}
var source= ['a','b','c','d','e'];
var mapped = source.map(function(el){
  return this[el] || ('common-'+el);
},specialMap);

console.log('source:',source); console.log('mapped:',mapped);

Как создается?

Объявление функции или функционального выражения

Обычное объявление функции:

function A(){}

var a = function (){};

при обычном объявлении значение this определяется при вызове способами описанными выше.


Создание функции с помощью bind

Функция bind возвращает новую привязанную функцию. Значение this внутри созданной функции всегда то, которое передали при вызове bind.

Важная особенность: при использовании привязанной функции в качестве конструктора, значение this все равно будет указывать на создаваемый объект, как описано выше.

Важная особенность: в НЕ strict mode при передаче в качестве параметра this значений null и undefined - этот параметр будет проигнорирован и this будет установлен в глобальный объект.

function A(){console.log(typeof this,'is window', this === window);}
console.log('execute with null');
A.bind(null)();
console.log('execute with undefined');
A.bind(undefined)();

function A1(){'use strict'; console.log(typeof this, this);} console.log('execute with null'); A1.bind(null)(); console.log('execute with undefined'); A1.bind(undefined)();

Важная особенность: значение this у созданной функции нельзя переопределить используя функции call и apply описанные выше.

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

var B = A.bind({o:'object'}); console.log('execute binded'); B(); console.log('execute with call'); B.call({another: 'some new object'});

console.log('execute as constructor'); new B();


Стрелочные функции

Стрелочные функции появились в ES2015 и при создании привязываются к текущему значению this.

После создания значение this нельзя поменять указанными выше способами.

Кроме того стрелочную функцию нельзя использовать в качестве конструктора.

function A(){
  this.t = (place)=>console.log(place,this);
}

var a = new A() a.t('method:');

var tt = a.t; tt('free function execute:');

tt.call({o:'object'},'using call function');

new tt('constructor');


на основе ответов:
- How does the “this” keyword work?
- How does “this” keyword work within a JavaScript object literal?

Grundy
  • 81,538
  • 2
    Тогда ты ещё не ленился:=:) (+) – Alexandr_TT May 02 '19 at 13:36
  • 2
    @Alexandr_TT, тогда еще не было вопроса, на который можно сослаться как на дубликат :) – Grundy May 02 '19 at 13:37
  • 1
    То что-то не то про null написал: https://i.stack.imgur.com/g7YAZ.png – Qwertiy Jan 16 '20 at 19:10
  • @Qwertiy, ага, в es5 или 6 поменялось поведение :) наверное уберу этот абзац – Grundy Jan 16 '20 at 19:54
  • @Qwertiy, не, все правильно, обновил ответ :-) в не strict mode - будет глобальный объект, в strict mode - что передали, то и получили. В примере это как раз заметно :) – Grundy Jan 16 '20 at 20:06
  • @Grundy, так это ж характеристики самой функции, а не bind. – Qwertiy Jan 16 '20 at 21:27
  • @Qwertiy, это не характеристики функции, а среды выполнения скорее, strict mode же можно в самом начале файла прописать и тогда для любой функции будет работать так – Grundy Jan 16 '20 at 21:35
  • @Grundy, если функция, которую биндим находится в строгом режиме, то работает в соответствии с ним. https://ru.stackoverflow.com/a/435547/178988 - там где ссылка на §10.4.3. – Qwertiy Jan 16 '20 at 21:38
  • Предлагаю добавить или просто упомянуть addEventListener в качестве примера «Как вызывается» чтобы можно было смелее вопросы дубликатами отмечать – andreymal Mar 30 '21 at 10:55
  • @andreymal, обновил – Grundy Mar 30 '21 at 11:31
41

Вкратце: в первом случае Вы вызываете функцию как метод объекта, во втором - берете функцию и саму по себе.

Более многословно:
Функции в javascript, в отличие от некоторых других популярных языков, являются так называемыми объектами первого класса. То есть они существуют и имеют смысл сами по себе, без привязки к объекту.

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

Вызов функции сразу через точку myObject.myFunction() это просто сокращенный способ задания this сразу, этакий сахар. Когда Вы вызываете через точку на самом деле происходит примерно следующее:

var func = myObject.myFunction; //Получаем функцию-свойство объекта myObject
func.call(myObject); // Вызываем эту функцию с нужным контекстом.

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

var func = myObject.myFunction.bind(myObject);
29

Функция вызывается в контексте объета НЕ потому что она создана внутри объекта. Она вызывается в контексте объекта, потому что она вызывается как метод obj.func()

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

Одна и та же функция может быть вызвана как методы разных объектов.

function foo() {
  blah blah
}

x = {}; x.foo=foo; x.foo() //тут this будет установлен на объект х
x2 = {}; x2.foo=foo; x2.foo() //тут this будет установлен на объект х2
foo(); //тут this не будет установлен

То же самое если функция изначально создается внутри объекта.Значения не имеет. Имеет значение только то как вызывается данная функция.

Pavel Mayorov
  • 58,537
18

В дополнение к другим ответам.

Если вы пишите на TypeScript, то можно воспользоваться вот таким декоратором:

function bound(target, propertyKey: PropertyKey, descriptor: PropertyDescriptor): PropertyDescriptor {
    var value = descriptor.value;
return {
    configurable: descriptor.configurable,
    enumerable: descriptor.enumerable,

    get() { return value.bind(this); }
}

}

Когда декораторы появятся в Javascript, можно будет написать его и там При использовании Babel так можно сделать и в Javascript:

function bound(target, propertyKey, descriptor) {
    var value = descriptor.value;
return {
    configurable: descriptor.configurable,
    enumerable: descriptor.enumerable,

    get() { return value.bind(this); }
}

}

Использование:

class Foo {
    private readonly _baz;
constructor(baz) {
    this._baz = baz;
}

@bound bar() {
     console.log(this._baz);
}

}

var bar = new Foo(42).bar; bar(); // 42


Если принципиально чтобы последовательные обращения к свойству выдавали одинаковые значения (например, дабы избежать избыточных рендеров в React) - придется написать чуть более сложный код:

var bfmc = Symbol("boundFunctionsMemoizedCache");

function bound(target, propertyKey: PropertyKey, descriptor: PropertyDescriptor): PropertyDescriptor { return { enumerable: descriptor.enumerable,

    get() {
        if (!this.hasOwnProperty(bfmc))
            this[bfmc] = {};

        if (this[bfmc].hasOwnProperty(propertyKey))
            return this[bfmc][propertyKey];

        return this[bfmc][propertyKey] = descriptor.value.bind(this);
    }
}

}

В принципе, можно написать и чуть проще (например, создавать отдельный символ на каждый вызов bound или вообще переопределять свойство propertyKey) - но я не уверен что такой код сможет быть правильно оптимизирован и не наплодит кучу скрытых классов где не надо. Буду рад услышать замечания по этому поводу от того, кто уже сталкивался с оптимизациями Javascript под конкретные браузеры.


Замечание. Этот ответ показывает устаревшую старую версию декораторов, которая никогда не войдет в стандарт.

Pavel Mayorov
  • 58,537
  • var a = new Foo(7); console.log(a.bar === a.bar); - будет false. Можно улучшить так, чтобы было true? – Qwertiy Aug 22 '17 at 13:21
  • В реакте PureComponent будет пересчитываться из-за таких изменений, а это плохо. Да и вообще, ну логично же, что должна возвращаться ссылка на одну и ту же функцию, а не создаваться клон бинда на каждом вызове - вдруг она у меня в цикле 1000000 раз вызывается. – Qwertiy Aug 22 '17 at 13:28
  • @Qwertiy замечание принимается. Дописал ответ. – Pavel Mayorov Aug 22 '17 at 13:45
  • 1
    @VasyaShmarovoz обратите внимание, на дополнение – Pavel Mayorov Aug 22 '17 at 13:49
  • @PavelMayorov, а там нельзя как-нибудь value присобачить вместо get? – Qwertiy Aug 22 '17 at 13:57
  • @Qwertiy совсем без get обойтись нельзя – Pavel Mayorov Aug 22 '17 at 14:02
  • @PavelMayorov, а, это же тоже в класс попадает, а не в инстанс - в этом дело? А в инстанс его нельзя отправить? – Qwertiy Aug 22 '17 at 14:04
  • @Qwertiy оно в прототип попадает. Нет, в инстанс его отправить нельзя (видел где-то инциализаторы - но они то ли в стандарт не вошли, то ли от них отказались) – Pavel Mayorov Aug 22 '17 at 14:07
1

Когда вы создаете обьект - вы по сути создаете его контекст, this внутри обьекта ссылается на контекст обьекта. После присвоения данной функции обычной переменной - вы сами же вырываете функцию из её контекста, тем самым привязка this внутри данной функции теряется, под this теперь имеется ввиду обьект window. Тоесть обращаясь к this.x теперь на самом деле вы обращаетесь к window.x который и есть undefined.

Сама операция присвоения функции привязаной к обьекту, внешней переменной - это не совсем логически выглядит, функция не может "помнить" о том что когда-то она была в обьекте, поэтому должна использовать его контекст для поиска атрибутов