Я попытаюсь просто объяснить, как работают замыкания в Javascript, как работает this, как создавать конструкторы для своих классов и чем различаются различные подходы к их созданию.
Статья не претендует на новаторство, но достаточно доступные объяснения how it works для новичков я не видел, и на мой взгляд — это три самых узких места в Javascript (не привязанному к какому либо контексту, серверу или браузеру, например).
Википедия нам говорит — замыканиями являются функции, определенные в других функциях.
Замыканиями в javascript являются все функции, потому что они неявно лежат в теле «Главной функции».
Что они из себя представляют? Что значит «замыкание»?
За терминологией лежит очень простой смысл, который так же просто можно объяснить.
Функции замыкания имеют возможность обращаться к переменным, созданным не только в контексте самой функции, но и на всех уровнях выше.
Проиллюстрирую кодом:
Теперь немного разберемся, если что-то стало непонятно.
closureFirstLevel обращается к переменным объявленным вне этой функции(внешним переменным) и возвращает их сумму.
createrOfSecondLevelClosure обращается к переменным a и b, сохраняет их сумму в переменной, объявленную в этой функции и возвращает функцию, которая считает сумму c, результата возвращаемого функцией closureFirstLevel и переменных a и b, объявленных на два уровня ниже.
Если запустить runCInAnotherContext он запустит функцию 'c' (ведь createrOfSecondLevelClosure возвращает нам функцию, которую можно сохранить, и переменная 'c', объявленная в глобальной области видимости записывает эту функцию), которая будет работать как и задуманно: возвращать сумму переменных и результата функции, объявленных вне контекста функции runCInAnotherContext, так как при инициализации она замкнула на себя эти переменные.
Замыкания в массовом создании событий.
Обращение к переменной, использующейся в цикле как счетчик, всегда передается как ссылка (хоть обычно и является числом), пока работает цикл. В итоге все созданные функции будут иметь последнее значение этой переменной.
См. пример
Можно замкнуть функцию:
А так же можно использовать совершенно другой подход. У массивов метод forEach (является частью стандарта EcmaScript5) работает не совсем как цикл for.
Он принимает один аргумент — функцию, которая будет обрабатывать элементы и которая принимает аргументы: elementOfArray, positionInArray, Array. И каждый раз эта функция вызывается, естественно, в своем контексте.
Где-то достаточно принимать только первый аргумент, где-то больше.
Мы можем эту функцию вызвать для нашего NodeList объекта, с помощью подмены контекста исполнения. (Для более полного разъяснения как это работает смотри часть статьи про this и про прототипы).
Это слово ссылается на текущий объект, вызывающий функцию.
Все функции, объявленные в глобальном контексте, являются методами (в браузере) объекта window, так же все функции, вызванные без контекста в this, ссылаются на window.
Все довольно просто, пока не начинаешь разбираться с асинхронным программированием.
Вместо setTimeout можно подставить setInterval или привязку обработчика события (например: elem.onclick или addEventListener), или любой другой способ выполнять отложенные вычисления, все они так или иначе вызывают потерю контекста исполнения. И чтобы сохранить this есть несколько путей.
Можно просто обернуть это в анонимную функцию, можно создать переменную var that = this и использовать that вместо this (переменную создать вне вызываемой функции, естественно), а также воспользоваться самым правильным способом — насильно привязать. Для этого у функций есть встроенный метод bind (стал доступен в стандарте EcmaScript 5, поэтому для старых браузеров нужно реализовывать его поддержку), который возвращает новую функцию, привязанную к нужному контексту и аргументам. Примеры:
Ну и посложнее пример.
Потеря в рекурсивных функциях, работающих через определенные интервалы времени.
Почему второй работающий способ правильный, а первый неправильный, смотри в часть статьи про прототипы.
Function.bind — метод, принимающий первый аргумент как контекст, в котором он будет исполняться (каким будет this), и остальные как неограниченное количество аргументов, с которыми будет вызываться возвращаемая функция.
Function.apply — метод, вызывающий функцию, первый аргумент – аргумент, который будет являться this в функции, второй — массив аргументов, с которыми будет вызвана функция.
Function.call — то же самое, что и apply, только вместо второго аргумента, неограниченное количество аргументов, которые будут переданы в функцию.
Многие создают конструкторы так:
И это не очень правильно. Что здесь неправильно? В данном примере неправильно только одно то, что функция объявлена в теле конструктора. Чем же это плохо?
Во-первых, переопределить такую функцию через изменение прототипа функции не получится, то есть всем объектам сразу, инициализировавшимся через данный конструктор не получится изменить метод на другой. Пропадает возможность нормально наследоваться.
Во-вторых — лишний расход памяти:
Так как каждый раз заново создается функция.
По хорошему тону (и для лучшего понимания кода) в конструкторах нужно определять только переменные (точнее поля объекта), которые только для него будут уникальны.
Остальное лучше определять через прототип, в любом случае если только для конкретного объекта нужно переопределить общее свойство или метод, это можно сделать напрямую, не затрагивая прототип.
Как это делается:
Многим не хватает в javascript возможностей, которые и так практически всегда нужны лишь для самоконтроля, таких как приватные методы и функции, к которым сможет напрямую обращаться только сам объект и его методы, и часто реализуют их как переменные и функции, объявленные в теле функции-конструктора. Так же многие говорят, что это — плохой тон, но мало где говорится, почему это плохой тон.
Причина одна, то, что если в этом конструкторе нужно будет что-то изменить, нужно будет лезть в исходник и менять там, а не через прототип.
Так же наследоваться от данного конструктора с целью расширить использование этих «приватных» свойств и методов будет крайне трудно.
Ещё один тонкий момент. Не привязывайте с помощью bind методы к контексту объекта (в конструкторе при инициализации, вне его в принципе можно), если хотите получить возможность переносить этот метод на другие объекты или просто использовать его в другом контексте.
Это нам позволяют делать встроенные объекты.
Например, можно использовать метод массивов forEach для других enumerable(перечисляемых) объектов. Например, для всех видов NodeList (живых и не живых) (как было показано выше).
А теперь напишем не большой конструктор, как пример, объединяющий содержимое статьи.
В этом нелепом примере в принципе есть все: сохранение контекста вызова, замыкания, и создание конструктора.
Надеюсь на критику и исправления (так как второпях мог, что-то забыть дописать, или же просто ошибаюсь).
p.s. боярский выигрывает иногда.
Статья не претендует на новаторство, но достаточно доступные объяснения 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);
В этом нелепом примере в принципе есть все: сохранение контекста вызова, замыкания, и создание конструктора.
Надеюсь на критику и исправления (так как второпях мог, что-то забыть дописать, или же просто ошибаюсь).