Попытка просто объяснить сложные, для новичков, вещи в javascript

Я попытаюсь просто объяснить, как работают замыкания в Javascript, как работает this, как создавать конструкторы для своих классов и чем различаются различные подходы к их созданию.
Статья не претендует на новаторство, но достаточно доступные объяснения how it works для новичков я не видел, и на мой взгляд — это три самых узких места в Javascript (не привязанному к какому либо контексту, серверу или браузеру, например).

Замыкания

Википедия нам говорит — замыканиями являются функции, определенные в других функциях.
Замыканиями в javascript являются все функции, потому что они неявно лежат в теле «Главной функции».
Что они из себя представляют? Что значит «замыкание»?
За терминологией лежит очень простой смысл, который так же просто можно объяснить.
Функции замыкания имеют возможность обращаться к переменным, созданным не только в контексте самой функции, но и на всех уровнях выше.
Проиллюстрирую кодом:

var a = 1;
var b = 2;
function closureFirstLevel() {
   return a + b;
}
function createrOfSecondLevelClosure() {
   var c = a + b;
   return function() {
        return c + closureFirstLevel() + a + b;
   }
}
var c = createrOfSecondLevelClosure();
function runCInAnotherContext() {
    return c();
}
console.log(a,b);
console.log('Сумма переменных a & b объявленных вне функции которая считает их сумму:',closureFirstLevel());
console.log('Сумма переменных c (объявленной на уровень выше), возвращаемого значения функции объявленной на два уровня выше, и переменных a и b объявленных так же на два уровня выше:',c());

Теперь немного разберемся, если что-то стало непонятно.
closureFirstLevel обращается к переменным объявленным вне этой функции(внешним переменным) и возвращает их сумму.
createrOfSecondLevelClosure обращается к переменным a и b, сохраняет их сумму в переменной, объявленную в этой функции и возвращает функцию, которая считает сумму c, результата возвращаемого функцией closureFirstLevel и переменных a и b, объявленных на два уровня ниже.

Если запустить runCInAnotherContext он запустит функцию 'c' (ведь createrOfSecondLevelClosure возвращает нам функцию, которую можно сохранить, и переменная 'c', объявленная в глобальной области видимости записывает эту функцию), которая будет работать как и задуманно: возвращать сумму переменных и результата функции, объявленных вне контекста функции runCInAnotherContext, так как при инициализации она замкнула на себя эти переменные.

Замыкания в массовом создании событий.
Обращение к переменной, использующейся в цикле как счетчик, всегда передается как ссылка (хоть обычно и является числом), пока работает цикл. В итоге все созданные функции будут иметь последнее значение этой переменной.
См. пример

var elem = document.getElementsByTagName('a');
for (var n = 0, l = elem.length; n < l; n++ ) {
	elem[n].onclick = function() {
		alert(n);
		return false;
	}
} //Все время будет выдавать порядковый номер последнего эллемента в alert

Можно замкнуть функцию:

var elem = document.getElementsByTagName('a');
for (var n = 0, l = elem.length; n < l; n++ ) {
	elem[n].onclick = function(x) {
		return function() {
			alert(x);
			return false;
		}
	}(n); //Создаем функцию, сразу же её вызываем она возвращает нам порядковый номер элемента в alert при событии click на элементе.
}

А так же можно использовать совершенно другой подход. У массивов метод forEach (является частью стандарта EcmaScript5) работает не совсем как цикл for.
Он принимает один аргумент — функцию, которая будет обрабатывать элементы и которая принимает аргументы: elementOfArray, positionInArray, Array. И каждый раз эта функция вызывается, естественно, в своем контексте.
Где-то достаточно принимать только первый аргумент, где-то больше.
Мы можем эту функцию вызвать для нашего NodeList объекта, с помощью подмены контекста исполнения. (Для более полного разъяснения как это работает смотри часть статьи про this и про прототипы).

var elem = document.getElementsByTagName('a');
Array.prototype.forEach.call(elem,function(el,position) {
	el.onclick = function() {
		alert(position);
		return false;
	}
})


Ключевое слово this

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

var a = {
    property1: 1,
    property2: 2,
    func: function() {
        console.log(this.property1 + this.property2, 'test');
        return this.property1 + this.property2;
    }
}
console.log(a.func());
//this ссылается на объект 'a', которому принадлежит вызываемый метод.
setTimeout(function() {
console.log(a.func());
//this все так же ссылается на объект 'a', потому что функция, переданная в таймаут, замкнула на себя объект 'a'
},100);

setTimeout(a.func,101);
//результат будет уже другой, NaN (как результат сложения undefined + undefined)
//потому, как здесь мы передаем лишь функцию, а сама по себе она не хранит ссылку на объект, к которому принадлежит

Вместо setTimeout можно подставить setInterval или привязку обработчика события (например: elem.onclick или addEventListener), или любой другой способ выполнять отложенные вычисления, все они так или иначе вызывают потерю контекста исполнения. И чтобы сохранить this есть несколько путей.
Можно просто обернуть это в анонимную функцию, можно создать переменную var that = this и использовать that вместо this (переменную создать вне вызываемой функции, естественно), а также воспользоваться самым правильным способом — насильно привязать. Для этого у функций есть встроенный метод bind (стал доступен в стандарте EcmaScript 5, поэтому для старых браузеров нужно реализовывать его поддержку), который возвращает новую функцию, привязанную к нужному контексту и аргументам. Примеры:

function logCurrentThisPlease() {
   console.log(this);
}
logCurrentThisPlease(); //window

var a = {}
a.logCurrentThisPlease = logCurrentThisPlease;
a.logCurrentThisPlease(); //a
setTimeout(a.logCurrentThisPlease, 100); //window, так как мы передаем только ссылку на функцию
setTimeout(function() {
   a.logCurrentThisPlease();
}, 200);//a
setTimeout(function() {
   this.logCurrentThisPlease();
}.bind(a), 200);//a

var that = a;
function logCurrentThatPlease() {
   console.log(that);
}
logCurrentThatPlease(); //a
setTimeout(logCurrentThatPlease, 200);//a

var logCurrentBindedContextPlease = logCurrentThisPlease.bind(a); //первый аргумент — контекст, к которому нужно привязать, остальные аргументы — аргументы функции
logCurrentBindedContextPlease(); //a
setTimeout(logCurrentBindedContextPlease, 200); //a

Ну и посложнее пример.
Потеря в рекурсивных функциях, работающих через определенные интервалы времени.

var a = {
	i: 0,
	infinityIncrementation: function() {
		console.log( this.i++ );
		if (this.i < Infinity) setTimeout(this.infinityIncrementation,500);
	}
}
a.infinityIncrementation(); // 0,undefined — не работает, потому что теряется контекст исполнения
a.infinityIncrementation = a.infinityIncrementation.bind(a); //не правильный но работающий способ
a.infinityIncrementation(); //0,1,2,3,4,5,6,7,8,9,10...Infinity-1
	
//правильный способ
var b = {
	i: 0,
	infinityIncrementation: function() {
		console.log( this.i++ );
		if (this.i < Infinity) setTimeout(function() {this.infinityIncrementation}.bind(this),500);
	}
}
b.infinityIncrementation(); //0,1,2,3,4,5,6,7,8,9,10...Infinity-1

Почему второй работающий способ правильный, а первый неправильный, смотри в часть статьи про прототипы.

Методы функций, позволяющие менять контекст исполнения — bind,call,apply

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

Конструкторы объектов

Многие создают конструкторы так:

function SuperObjectConstructor() {
	this.a = 1;
	this.b = 2;
	this.summ = function() {
		return this.a + this.b;
	}
}

И это не очень правильно. Что здесь неправильно? В данном примере неправильно только одно то, что функция объявлена в теле конструктора. Чем же это плохо?
Во-первых, переопределить такую функцию через изменение прототипа функции не получится, то есть всем объектам сразу, инициализировавшимся через данный конструктор не получится изменить метод на другой. Пропадает возможность нормально наследоваться.
Во-вторых — лишний расход памяти:

var a = new SuperObjectConstructor();
var b = new SuperObjectConstructor();
console.log(a.summ == b.summ); //false

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

function SuperObjectConstructorRightVersion(a,b) {
	this.a = a || this.constructor.prototype.a; //Берем дефолтное значение из прототипа конструктора
	this.b = b || this.constructor.prototype.b;
}
SuperObjectConstructorRightVersion.prototype = { //изменяем прототип полностью
	constructor: SuperObjectConstructorRightVersion, //так как мы его полностью заменяем нужно переопределить и конструктор
	a: 1,
	b: 2,
	summ: function() {
		return this.a + this.b;
	}
}
	/*или такой способ
SuperObjectConstructorRightVersion.prototype.a = 1;
SuperObjectConstructorRightVersion.prototype.b = 2;
SuperObjectConstructorRightVersion.prototype.summ = function() {....};
Но он менее элегантный и занимает больше места.
*/
var abc = new SuperObjectConstructorRightVersion();
console.log(abc.summ());//3
var bfg = new SuperObjectConstructorRightVersion(5,20);
console.log(bfg.summ());//25

Многим не хватает в javascript возможностей, которые и так практически всегда нужны лишь для самоконтроля, таких как приватные методы и функции, к которым сможет напрямую обращаться только сам объект и его методы, и часто реализуют их как переменные и функции, объявленные в теле функции-конструктора. Так же многие говорят, что это — плохой тон, но мало где говорится, почему это плохой тон.
Причина одна, то, что если в этом конструкторе нужно будет что-то изменить, нужно будет лезть в исходник и менять там, а не через прототип.
Так же наследоваться от данного конструктора с целью расширить использование этих «приватных» свойств и методов будет крайне трудно.

Ещё один тонкий момент. Не привязывайте с помощью bind методы к контексту объекта (в конструкторе при инициализации, вне его в принципе можно), если хотите получить возможность переносить этот метод на другие объекты или просто использовать его в другом контексте.
Это нам позволяют делать встроенные объекты.
Например, можно использовать метод массивов forEach для других enumerable(перечисляемых) объектов. Например, для всех видов NodeList (живых и не живых) (как было показано выше).

Вывод

А теперь напишем не большой конструктор, как пример, объединяющий содержимое статьи.

function Monster(name, hp, dmg) {
	this.name = name || this.constructor.prototype.name();
	this.hp = hp || this.constructor.prototype.hp;
	this.dmg = dmg || this.constructor.prototype.dmg;
}
Monster.prototype = {
	constructor: Monster,
	hp: 10,
	dmg: 3,
	name: function() {
		return 'RandomMonster'+(new Date).getTime();
	},
	offerFight: function(enemy) {
		if (!enemy.acceptFight) {
			alert('this thing cant fight with me :(');
			return;
		}
		enemy.acceptFight(this);
		this.acceptFight(enemy);
	},
	acceptFight: function(enemy) {
		var timeout = 50 + this.diceRollForRandom();
		this.attack(enemy,timeout);
	},
	diceRollForRandom: function() {
		return (Math.random() >= 0.5 ? 50 : 20);
	},
	takeDmg: function(dmg) {
		console.log(this.name,' was damaged (',dmg,'),current HP is ',this.hp-dmg);
		return this.hp -= dmg;
	},
	attack: function(enemy,timeout) {
		if (enemy.takeDmg(this.dmg) <= 0) {
			enemy.die();
			this.win();
			return;
		}
		this.to = setTimeout(function() {this.attack(enemy)}.bind(this),timeout);
	},
	win: function() {
		alert('My name is ' + this.name + ', and Im a winner');
	},
	die: function() {
		alert('I died, ' + this.name);
		clearTimeout(this.to);
	}
}
var ChuckNorris = new Monster('Chuck Norris', 100, 100);
var MikhailBoyarsky = new Monster('Misha Boyarsky', 200, 50);
MikhailBoyarsky.offerFight(ChuckNorris);

В этом нелепом примере в принципе есть все: сохранение контекста вызова, замыкания, и создание конструктора.
Надеюсь на критику и исправления (так как второпях мог, что-то забыть дописать, или же просто ошибаюсь).
p.s. боярский выигрывает иногда.
Поделиться публикацией
Комментарии 34
    +1
    Неплохая статья для новичков :)
      +3
      Вы пропустили много точек с запятыми в своём коде…
        +2
        спасибо, исправлено;
          +2
          Еще не помешает унифицировать пробелы вокруг арифметических операций, это косметика, но хорошо сказывается не читаемости, что для урока важно.
        –19
        как по мне; в javascript не нужны
          +2
          Это относительно, в некоторых ситуациях в устаревших браузерах (читай как IE) код может быть интерпретирован не верно. А так конечно, дело вкуса
            +4
            Возможно я чего-то не знаю, начал более тесное общение с JS совсем недавно, но во всех учебных материалах говориться, что необходимо ставить; везде, чтобы механизм автоматического разделения участков кода не создал вам странный неуловимый баг.

            Более того код вы будете сжимать, что в последствии тоже даст баги и всё-равно вам придётся её поставить.
              +1
              ну нормальный обфускаторы ставят все сами, но да, лучше обезопасить себя
                0
                yuicompressor не нормальный обфускатор? Вроде бы, один из основных, однако пару раз заставил меня поставить; и убрать debugger;-ы
                  +1
                  Это в идеальном случае, но мало ли какой код вы напишите и как сработает обфускатор. И за те 30-минут, что вы фиксили баг можно поставить море точек с запятыми.
                    –2
                    github.com/twitter/bootstrap/blob/master/js/bootstrap-alerts.js а пацаны и не знают… старые браузеры, обфускаторы ломающие код, какие еще предрассудки вас тревожат?
                      +1
                      100 строк кода? их даже в perl-стиле написать можно. Непонятно, почему они сам факт поддержки transition проверяют при помощи feature detection, а вот саму функцию выбирают уже при помощи проверки user-agent. Код, в общем, хорош, но это не значит, что надо из него брать порочную практику глотать знаки разделения. В русском языке тоже можно без точек с запятыми писать.
                        0
                        я не считаю это порочной практикой. если ие этого не поддерживал когда-то это еще ничего не значит. по-моему их всегда там быть не должно было, так что моя точка зрения не хуже вашей.
            +2
            огромное человеческое спасибо за статью!
            на самом деле многое прояснилось
              0
              спасибо за отзыв! Боялся, что все равно все запутал, сомневался что смог изложить доступно насколько хотел
              0
              Спасибо, было интересно почитать
                0
                А ну-ка, новички, задания для самоконтроля: что выведут 2 приведенные ниже куски кода и почему?

                Первый:
                if (42 == 42) {
                function test() {
                console.log('first');
                }
                } else {
                function test() {
                console.log('second');
                }
                }


                Второй:
                var funcs = []
                for (var i = 0; i < 10; i++)
                funcs.push(function() {
                console.log(i);
                });

                console.log('first');
                for(var i = 0; i < 10; i++)
                funcs[i]();
                console.log('second');
                for(var j = 0; j < 10; j++)
                funcs[j]();


                  +4
                  первый ничего не выведет. ;)
                    0
                    Сорри, в первом не хватает в конце вызова функции test();
                      0
                      интересно почему во втором варианте после for а в переменной i остается 10
                      0
                      Да первый тест не много не в тему, хотя может это тут тоже нужно было раскрыть, но я видел на хабре уже топики, где это раскрывалась.
                      Вся суть в том, что объявление функций самая приоритетная функция и вот такой код:
                      function test() {
                          function supertest() {
                               alert('NIKOGDA NE POKAJETSYA');
                          }
                          return supertest;
                          function supertest() {
                               alert('A ETA POKAJETSYA');
                          }
                      }
                      var a = test();
                      a() // A ETA POKAJETSYA!
                      

                      будет для не посвященных очень странно работать :)
                      0
                      первый ничего не выведет. ;)
                      0
                      Спасибо, интересная статья.
                      Если не ошибаюсь, везде по тексту, кроме первого вхождения (так же просто) «также» пишется слитно. «Небольшой» (конструктор) тоже без пробела. Мелочь, а отвлекает.
                        0
                        Я с Ваших слов не понял чем отличаются
                        Function.bind
                        Function.call

                        … объясните пожалуйста в чем разница.
                          +2
                          bind — создаёт новую функцию с нужным контекстом, call — вызывает текущую с нужным контекстом.
                            0
                            не помешает сослаться на функцию jQuery.proxy, т.к. bind, понятно, не везде работает.
                            Ну и или просто:
                            var proxy = function(func, thisObject){
                            return(function(){
                            return func.apply(thisObject, arguments);
                            });
                            };
                              0
                              Я вот тоже не видел повсеместного употребления bind. Всегда думал что это часть какого-нибудь фрэймворка. Какими конкретно браузерами он поддерживается? Точнее какими версиями IE?
                          0
                          Да bind возвращает новую функцию привязанную к нужному контексту и аргументам, а кол вызывает с фукцию с нужными контекстом и аргументами!

                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                        Самое читаемое