Skip to content

Instantly share code, notes, and snippets.

@GoodDayTodayOkey
Created December 14, 2017 04:12
Show Gist options
  • Save GoodDayTodayOkey/20841b794b6182a9569183a419cf00a8 to your computer and use it in GitHub Desktop.
Save GoodDayTodayOkey/20841b794b6182a9569183a419cf00a8 to your computer and use it in GitHub Desktop.
JS Шаблоны. Функции.

JS Шаблоны. Функции

Содержание

  • Терминология
  • Функции обратного вызова
  • Самоопределяемые функции
  • Немедленно вызываемые функции
  • Замыкание
  • Мемоизация
  • Каррирование

Терминология

Функции в JS - это объекты:

  • Могут создаваться динамически во время выполнения
  • Могут присваиваться переменным
  • Могут использоваться в качестве параметров для других функций
  • Могут иметь собственные свойства и методы

Именованная функция-выражение

var add = function add(a, b) {
	return a + b;
}

Функция-выражение или анонимная функция

var add = function (a, b) {
	return a + b;
}

Функция-объявление

function add(a, b) {
	return a + b;
}

Область видимости

Область видимости - область, в которой можно обратиться к переменной или функции по имени

Область видимости в JS:

  • Локальная - функция или объект
  • Глобальная - объект Window (в случае использования браузера)

В JS в отличие от других ЯП, нет блочной области вилимости, которая задается операторными скобками {}

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

Подъем функций (function hoisting) - работает также как и подъем переменных: интерпретатор определяет все функции в области видимости (напр., Window) и делает их методами объекта Window. Исключение составляют функции-выражения (т.е. ф-ии присвоенные переменной var a = function(){}): интерпретатор также определяет такие переменные и делает их свойствами объекта Window, но не он не читает их значения, т.е. не выполняет функцию, которая записана как значение в эту переменную. Надо быть аккуратным с порядком вызова таких функций.

Контекст

Контекст - это ключевое слово this. Контекст - объект на который указывает ключевое слово this в момент вызова функции. Если мы говорим, что контекст функции - глобальный объект, это означает, что когда внутри функции вы обратитесь к ключевому слову this, то обнаружите в нем глобальный объект Window. Если говорим, что контекст - это конкретный объект, то, обращаясь, увидим этот конкретный объект. Иными словами, контекст - это ссылка, которая ведет на какой-то конкретный объект и которая зафиксирована в ключевом слове this внутри самой функции.

В JS контекст может ссылаться на 3 типа объекта:

  • Глобальный
  • Конкретный
  • Новый пустой

Контекст ссылается на глобальный объект

function contextTest1 {
	console.log(this === window); // true
}
contextTest1();

Контекст ссылается на конкретный объект

var obj = {
	contextTest2: function(){
		console.log(this === obj);
	}
}
// При вызове метода, контекстом функции является объект, на котором произошел вызов
obj.contextTest2();

Контекст ссылается на новый пустой объект

function ContextTest3() {
	console.log(this instanceof ContextTest3); // ключевое слово instanceof используется вместе с конструктором (который указывается после ключевого слова), когда хотим проверить создавался ли какой-то объект (здесь this) данным конструктором (здесь ContextTest3)
}
// При вызове функции с ключевым словом new контекстом будет новый пустой объект
new ContextTest3(); // Когда мы указываем перед конструктором ключевое слово new, мы тем самым говорим, что внутри конструктора контекст нужно заменить на новый пустой объект

Callback-функции (или функции обратного вызова)

Стоит упомянуть, что задачи, которые решаются с помощью коллбэков, можно также решить и другими способами, например, с помощью promise- и deferred-объектов.

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

Коллбэк - это функция, которую мы даем какому-то другому объекту или функции для того, чтобы она позже (напр., когда станет известна информация от сервера) запустилась.

Коллбэки очень часто встречаются в библиотеках, например, в jQuery. Коллбэки часто используются как способ внедрить свою функциональность в чужой код.

// doOperation принимает на вход в качестве аргумента коллбэк
function doOperation(callback) {
	// Выполнение каких-то операций, в конце которых вызывается коллбэк
	callback();
}

// Коллбэк showAlert, который мы передадим в ф-ию doOperation
function showAlert() {
	alert('Hello from callback');
}

// Функции - это объекты, то есть их можно передавать как аргументы другим функциям.
doOperation(showAlert)

Рекомендации по использованию коллбэков:

  • называть аргумент, в который мы планируем передавать функцию как 'callback' function (callback) {}
  • передавать коллбэк последним аргументом для наглядности function (a, b, callback) {}

Наиболее универсальный способ использования коллбэков - передавать вместе с аргументом callback еще один аргумент для передачи контекста: function (callback, context) {}. И внутри этой функции вызывать на коллбэке метод call(): callback.call(context, args). Если наш коллбэк будет являться методом объекта, то всегда можно явно указать контекст: doOperation(obj.method, obj).

Самоопределяемая функция

После первого запуска функция переопределяет сама себя (свое тело).

var init = function(){
	alert('Инициализация');
	// Какие-то операции инициализации, которые выполняться только один раз в момент первого запуска функции
	init = function(){
		alert('Инициализация уже завершена');
		// Операции, которые будут выполняться все последующие разы, после первого запуска функции
	}
}

Вызов самоопределяемой функции init(); // Инициализация init(); // Инициализация уже завершена

Немедленно вызываемые функции

IIFE (Immediately Invoked Function Expression )

Как правильно написать НВФ и не запутаться: ()(); - первые скобки - сама функция, вторые скобки - передаваемые в функцию параметры (function(){})() - когда интерпретатор дойдет до этой функции, он ее сразу выполнит, а самое главное, что такая функция представляет собой свою локальную область видимости и сама она не создает глобальных переменных и ее имя не появится в глобальном объекте.

2 варианта написания

(function(){
	// code
})(); // Аргументы снаружи
(function(){
	// code
}()); // Аргументы внутри

Данная функция принимает при вызове ссылку на глобальный объект.

(function(global){
	global.alert('Helolo world');
}(this));

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

НВФ также могут применяться для инициализапции объектов, созданных через литерал.

var obj = {
	name: (function() {
		var fName = 'Ivan',
				lName = 'Ivanov';
		return fName + ' ' + lname;
	}()),
	sayHello: function() {
		alert('Hello, my name is ' + this.name);
	}
};
obj.sayHello();

В этом примере в свойстве name нам будет доступна уже сконкатенированная строка с именем и фамилией. Однако переменные fName и lName доступны не будут.

Принципы определения функций

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

В начале создается объект, где перечислены все необходимые методы для работы приложения

var utils = {
	addListener: null,
	removeListener: null
};

Затем делаются проверки

if ( condition 1 ) {
	utils.addListener = function(){};
	utils.removeListener = function(){};
} else if ( condition 2 ) {
	utils.addListener = function(){};
	utils.removeListener = function(){};
} else {
	utils.addListener = function(){};
	utils.removeListener = function(){};
}

Таким образом, мы избегаем лишних проверок. Проверка в браузере происходит 1 раз и затем нужная функция закрепляется за нужным методом объекта utils.

Функциональное программирование

Замыкания (первая парадигма функционального программирования)

Рассмотрим пример

function init() {
	var str = 'Hello world';
	function displayStr() {
		// вложенная функция имеет доступ к переменной str
		alert(str);	
	}
	displayStr();
}

Благодаря тому, что одна функция в своем теле может содержать другую функцию, есть возможность создавать замыкания.

Для каждой функции, когда она создается, появляется объект, который представляет ее скоуп. Получается для init() создается объект, в котором содержится переменная str, т.к. именно в этом скоупе она находится. Для displayStr() создается другой скоуп, в котором уже нет никаких переменных. Из-за того, что скоуп displayStr() вложен в скоуп init(), есть явная связь между двумя этими скоупами. Также у нас есть глобальный скоуп, который представлен объектом Window. Получается скоуп init() связан с глобальным скоупом. Если мы начнем пользоваться переменной str в функции displayStr(), то интерпретатор начнет искать str в текущем скоупе. Понятно, что в этом скоупе переменной нет, поэтому интерпретатор поднимется выше и возьмет str, опредленную на уровне функции init(). Т.о., когда мы получаем доступ к переменным, находящимся в скоупе выше, мы просто захватываем локальную переменную. Однако, если мы возьмем скоуп init() с его вложенными скоупами и отдадим кому-то другому - это и будет являться замыканием. Замыкание - это функция и переменные, которые ее окружают. Замыкание позволяет сохранить саму функцию и ее окружение.

Посмотрим как выглядит само замыкание:

function makeFunc() {
	var str = 'Hello world';
	function displayStr() {
		alert(str);	
	}
	// функция возвращается, но не запускается
	// Мы возвращаем (но не запускаем) ф-ию и вместе с тем возвращаем как бы снимок скоупа, который был доступен на момент запуска ф-ии, т.е. возвращаем значение переменной str и саму функцию. Но значенние переменной str, понятно, не будет доступно вне функции.
	return displayStr;
}

Замыкание - функция, в которой находятся переменные, объявленные вне тела этой функции. Замыкание - это функция и окружение, в которой была создана функция.

var myFunc = makeFunc(); // Теперь здесь лежит и функция и те переменные, которые необходимы для работы функции - это и есть замыкание
myFunc();

С помощью замыкания можно организовать инкапсуляцию и спрятать часть кода, чтобы никто не смог взаимодействовать с ним извне.

Мемоизация (вторая парадигма функционального программирования)

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

function calcFib(x) {
// Проверка наличия результата для параметра x
	if ( ! calcFib.cache[x] ) { // Если результат не найден, то производим вычисления
		if ( x > 1 ) {
			calcFib.cache[x] = calcFib(x - 1) + calcFib(x - 2);
		} else {
			calcFib.cache[x] = x
		}
	}
	return calcFib.cache[x];
}
// Создание сво-ва в ф-ии с пустым объектом
calcFib.cache = {};

for ( var i = 0; i < 40; i++ ) {
	console.log(i + ' = ' + calcFib(i) + '\n');
}

Каррирование (третья парадигма функционального программирования)

Чтобы разобраться что такое каррирование, нужно сначала разобраться с методами apply() и call(), а также дать определение понятию частичное применение.

Методы apply() и call()

Эти методы позволяют запустить функцию с измененным контекстом.

Apply принимает 2 аргумента: 1) контекст и 2) массив аргументов запускаемой функции: fn.apply(context, [a, b]). Call принимает первым аргументом контекст и последующие аргументы функции через запятую: fn.call(context, a, b).

В остальном отличий между этими двумя методами нет.

Где может пригодиться использование apply()? В основном при работе с массивами, когда во второй аргумент apply() можно передать массив. Например, поиск максимального значения в массиве.

var arr = [1, 4, 7, 34, 756, 97, 245, 3, 77];
var maxValue = Math.max.apply(null, arr); // идентичен вызову Math.max(arr[0], arr[1], ..., arr[n]);

Где может быть применен call()? В JS есть понятие Array-like Object - объекты, которые очень похожи на массивы, благодаря тому, что именами свойств у них являются индексы. Хотя массивы в JS являются объектами, есть ряд признаковом отличающих массивы от объектов: например, каждый элемент массива имеет порядковый индекс; массив имеет свойство length, определяющее его размер; массив имеет ряд встроенных методов для работы с массивами, которых нет у объекта (push, slice и т.д.).

Array-like Object выглядит так:

var arrayLikeObject = {
	'0': 'first',
	'1': 'second',
	'2': 'third',
	'3': 'fourth',
	'4': 'fifth',
	'5': 'sixth',
	length: 6
}

Например, чтобы скопировать участок массива, мы можем применить метод slice(). Но у нас объект и такого метода у него нет. Можно, конечно, написать сложную функцию, выполнение которой будет скорее всего завязано на обход в цикле всех свойств объекта с использованием контекста, сво-ва length и числового индексатора [i]. Но все это уже заложено в нашем массивоподобном объекте - порядковые индексы и свойство length - и поэтому гораздо проще все-таки обратиться к методу slice() в прототипе конструктора Array, передав ему в качестве контекста наш объект.

Array.prototype.slice.call(arrayLikeObject, 2, 4);

Частичное применение

Частичное применение - передача в функцию не всех аргументов, а только части.

function add(x, y) {
	var oldX = x, oldY = y;
	if ( typeof oldY === 'undefined' ) { // частичное применение
		return function (newY) {
			return oldX + newY;
		}
	}
	return x + y; // полное применение
}
console.log( typeof add(10) ); // function

Вызывая add(10) мы получаем новую функцию, которая в замыкании хранит значения переменной oldX. После этого запускаем новую функцию и передаем еще одно значение. var r = add(10)(20); // 30

Процесс создания такой функции, где используется частичное применение, и называется каррированием (curring). Каррирование и частичное применение являются некими формами перегрузки функции.

Универсальная функция каррирования может выглядеть так (из книги Стояна Стефанова):

function curry(fn) {
	var slice = Array.prototype.slice,
	    oldArgs = slice.call(arguments, 1); // отбрасываем 1ый аргумент
			
	return function () {
		var newArgs = slice.call(arguments),
		    args = oldArgs.concat(newArgs);
		
		return fn.apply(null, args); // вызываем функцию, для которой проводилось каррирование
	}
}

Применение функции curry()

function add(x, y) {
	return x + y;
}

var add10 = curry(add, 10);
var r1 = add10(5); // 15
var r2 = add10(10); // 20

function sum(a, b, c) {
	return a + b + c;
}

var newSum = curry(sum, 1, 2);
var r3 = newSum(5); // 8
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment