05Functions.md 32 KB

Функции, области видимости

Зачем?

Повторяющиеся действия.

Как можно было заметить, компьютеры сильны именно в однотипных задачах, и делают простые задачи очень быстро. Связанные друг с другом однотипные задачи обычно повторяются в циклах (однотипные операции над массивами данных, статистические задачи, отрисовка повторяющихся данных на экране и так далее). Так же есть задачи по требованию, которые могут пригодится в любом месте кода. Например: prompt, alert, Math.random и прочие встроенные функции, которые являются подпрограммами, содержащими в себе программный код, вызываемый для решения определенной задачи. Я думаю понятно, что данные возможности не являются возможностями аппаратуры, а воплощены на программном уровне. Это значит, что подпрограммы являются естественными в компьютерах.

DRY

Don't repeat yourself. Один из основопологающих принципов разработки. Суть в том, что в процессе программирования вы должны минимизировать повторяющиеся части кода, которые делают почти одинаковые задачи; так как копипаста в коде приводит к дублированию отладки, да и вообще некрасиво это :-)

KISS

Keep It Simple, Stupid. Решайте задачи самым простым способом.

Отладка кода вдвое сложнее, чем его написание. Так что если вы пишете код настолько умно, насколько можете, то вы по определению недостаточно сообразительны, чтобы его отлаживать. — Brian W. Kernighan.

DRY > KISS

Зачастую эти принципы противоречат друг другу; уменьшение объема кода требует более мощных и сложнее отлаживаемых средств языка; однако в долгосрочной перспективе принцип DRY полезней, чем простота кода (KISS).

Пример

var surname    = prompt("Введите фамилию","")
if (surname === null || surname === ""){
    surname    = "Иванов"
}

var name       = prompt("Введите имя","")      || "Иван"
var fathername = prompt("Введите отчество","") || "Иванович"

Это наш пример, который спрашивает у пользователя ФИО ИЛИ берет эти параметры по умолчанию. Как видите, алгоритм ввода каждого из полей ФИО однотипен, и его неплохо было бы выделить в функцию. Ко всему прочему, несмотря на эквивалентность алгоритма, surname вводится кодом, отличающимся от ввода name и fathername, что усложняет модификацию и отладку кода.

Задание

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

Ниже спойлер, имейте совесть :-D. Не омрачайте задание подглядыванием ответов.

СПОЙЛЕР СПОЙЛЕР СПОЙЛЕР СПОЙЛЕР

Функции

Функция - подпрограмма, которая принимает определенные параметры при вызове, выполняет определенный код, и возвращает выполнение кода в место вызова, опционально (не обязательно) вернув результат работы в место вызова.

Свойства функций,

...которые сделали её такой полезной для написания программ:

Вызов.

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

var callCounter = 0;
function ourFunction()
{
    alert("Вызов № " + callCounter);
    callCounter ++;
}
console.log(callCounter);
ourFunction()
console.log(callCounter);
//тут еще может быть много кода, но мы можем опять воспользоваться функцией когда захотим:
ourFunction()
console.log(callCounter);

Вызов происходит при наличии скобок после имени функции. Если скобок нет - вызов не происходит и это имеет совершенно другой смысл. Для входа и выхода из функции используются F11 и Shift-F11 в Developer Tools при пошаговой отладке

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

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

var surname = "Петров";
function readSurname()
{
    surname = prompt("Введите фамилию","") //тут мы портим внешнюю переменную surname, и это нехорошо
    if (surname === null || surname === ""){
        surname = "Иванов"
    }
}

console.log(surname);
readSurname();
console.log(surname);

Для решения этой проблемы используется концепция области видимости - правильно объявленная переменная в функции (через var) существует только в функции и создается каждый раз при вызове функции; внешние же переменные с таким же именем остаются нетронутыми

var surname = "Петров";
function readSurname()
{
    var surname = prompt("Введите фамилию","") // тут мы ничего не портим, эта переменная НЕ ЯВЛЯЕТСЯ внешней переменной surname
    if (surname === null || surname === ""){
        surname = "Иванов"
    }
}
console.log(surname);
readSurname();
console.log(surname);

Параметры.

Функция должна уметь получить те или иные данные для своего выполнения. Например встроенные функции confirm, prompt, alert.

Задание: Каковы параметры и какой у них смысл в вышеуказанных встроенных функциях?

var name = "Yohan"
function greet(name){ 
    alert("Hello, " + name);
}

greet(name)
greet("John")
greet("Paul")
console.log(name)

Возвращаемое значение.

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

Задание: какие из функций prompt, confirm и alert возвращают значения, а какие - нет?

function random5(){
    return Math.random()*5;
}

alert(random5());
var someRandomValueFromZeroToFive = random5();

Определение и выполнение функций

Обратите внимание, что первый alert происходит ДО включения пошаговой отладки. Это говорит о том, что определение функции НЕ вызывает её. Код функции работает только после вызова, который происходит по d(). Для вызова надо указать в коде имя функции и скобки после имени (с параметрами или без оных)

function d()
{
    debugger;
}
alert("before d");
d()
alert("after d");

Определение начинается с ключевого слова function, после которого идет имя функции и параметры в скобках через запятую. Далее идет блок кода функции в фигурных скобках. В отличие от if, else и циклов, фигурные скобки обязательны.

При отладке и/или чтении чужого кода ищите вызовы функций. Иногда вызовы скрыты.

Именование функций

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

Выполнение функций.

Когда в коде упоминается имя функции со скобками и, возможно, параметрами происходят следующие действия:

  • вычисляются выражения в скобках. В функцию попадают уже значения выражений.
  • создается новая область видимости, в которую попадают параметры и их значения. Вам не нужно определять переменные для параметров.
  • начинается выполнение кода в фигурных скобках определения функции. Все переменные, определенные через var попадают в локальную область видимости функции, не перекрывающую внешнюю область видимости.
  • Код выполняется до выполнения return или окончания кода функции (закрывающей фигурной скобки). return прерывает выполнение функции, более того, с помощью return происходит возврат значения функции, которое подставляется на место вызова функции. Таким образом функция ведет себя как переменная для чтения. Если функция ничего не возвращает, то, на самом деле, она возвращает undefined
function sqr(a){
    alert("Вы передали:" + a);
    return a*a;
    alert("Этот код не выполнится");
}  

var sqr1 = sqr(5)
var otherVar = 2;
alert("Сумма квадратов: " + (sqr1 + sqr(otherVar + otherVar)));

Параметры функции и возвращаемое значение

Параметры (аргументы)

Параметры функции перечисляются в скобках после имени через запятую. Параметры - это переменные области видимости функции, в которые попадают вычисленные значения, передаваемые при вызове. Таким образом функции получают данные из внешнего кода.

В Javascript количество параметров при определении и при вызове может отличаться. Это не вызывает ошибок. В таком случае непереданные параметры равны undefined:

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

alert(add())
alert(add(1));
alert(add(2,3));

Если же параметров больше, чем указано в определении функции, то ошибки тоже не происходит. Для доступа к полям существует псевдомассив arguments, который всегда содержит актуальный набор параметров, переданных при вызове.

function add(a,b)
{
    console.log(arguments)
    a = a || 0;
    b = b || 0;
    return a + b;
}

alert(add(4,5,6))
alert(add(4,5,6,7));

prompt("Введите число", "0");
prompt("Введите число");

Задание

Используя перебор массива arguments циклом for, сделайте функцию, которая складывает любое количество параметров

Возвращаемое значение

Для возврата значения используется return. У него три основных свойства:

  • Собственно возврат значения во внешний код. Выражение после return вычисляется в контексте функции:
function add(a,b)
{
    return a + b; 
}
alert(add(3,4))

после чего значение попадает в место, где функция была вызвана (в alert)

  • Прекращение выполнения функции
  • return без параметра возвращает ничего, т. е. undefined:
function bigAndWeirdFunction()
{
    var somethingBad = Math.random() > 0.5;
    if (somethingBad){
        alert("Something bad happens");
        return;
    }
    alert("All OK!");
}
bigAndWeirdFunction();
bigAndWeirdFunction();
bigAndWeirdFunction();

console.log и return

При отладке вы видите в одной консоли вычисленное значение выражения (например 2 + 2 или prompt("Введите число")) и вывод console.log. console.log просто выводит текст в консоль, как document.write - в окно браузера, далее вы с этим ничего не можете сделать (почти). Выражение же может быть вставлено в код и являться частью другого выражения:

2 + 2
var a = 2 + 2
prompt("Введите число");
var num = prompt("Введите число");

var b;
    b = console.log(a); //неработает, метод log объекта console возвращает undefined, т. е. ничего
    b = a; //работает

function myLowerCase(str)
{
    console.log(str.toLowerCase()); //это просто пишет текст в консоли.
}

function rightUpperCase(str)
{
    return str.toUpperCase(); //это работает правильно
}

var lowerCase = myLowerCase("AbCdEf") //не работает.
var upperCase = rightUpperCase("AbCdEf") //работает

Что бы отличить результат выражения от вывода console.log, отметьте что возле значения выражения есть знак <.

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

Как было указано выше, переменные, объявленные с var внутри функции, являются незаметными для окружающего кода и перекрывают совпадающие переменные внутри функции, оставляя невредимыми внешние переменные:

var a = 5;

alert("global a: " + a);
function someFunctionWithA(){
    var a = "string";
    alert("function scope a: " + a);
}

alert("global a after function declaration" + a);
someFunctionWithA()
alert("global a after function execution" + a);

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


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

add(1,2)
add(5,6)

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

Глобальная область видимости

Если переменная создается без var в любом месте кода, в том числе в функции, она является глобальной, т. е. видимой везде. В ES5 это значит что любая переменная без var попадает в объект window. В ES6 это вызывает ошибку.

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

result = add(1,2)
alert(result);
add(5,6)
alert(result);

Как видно в примере выше, мы не можем расчитывать на целостность переменной result, пользуясь функцией add. Использование глобальных переменных в большинстве случаев неоправдано; они нужны в-основном только для каких-то общих данных для чтения. Например Math.PI является глобальной переменной, доступной только на чтение; то есть константой. Ваши же переменные будут доступны и на запись, будьте аккуратны используя их.

Общее правило: всегда ставьте var.

Вложенные функции и их области видимости

var a = "0";
var b = "0";
var c = "0";

function level1(){
    var b = "1";
    var c = "1";

    function level2(){
        var c = "2";
        console.log("Level 2 scope: a: " + a + " b: " + b + " c: " + c);
    }
    level2();
    console.log("Level 1 scope: a: " + a + " b: " + b + " c: " + c);
}

level1();
console.log("Level 0 scope: a: " + a + " b: " + b + " c: " + c);

Проанализируйте вывод кода выше. Самая вложенная функция level2 видит переменные своей области видимости (c), потом ищет значение на уровень выше (для переменной b), и на уровень еще выше (для a). Промежуточная функция level1 ничего не знает о переменных в level2, но видит свою область видимости и глобальную. Глобальная же имеет свои переменные a, b, c в первозданном виде.

var a = "0";
var b = "0";
var c = "0";

function level1(){
    var b = "1";
    var c = "1";
    var d = "1";

    function level2(){
        var c = "2";
        var e = "2";
        console.log("Level 2 scope: a: " + a + " b: " + b + " c: " + c + " d: " + d + " e: " + e); 
        d = "2";
    }
    console.log("Level 1 before level2, scope: a: " + a + " b: " + b + " c: " + c + " d: " + d + " e: " + e);
    level2();
    console.log("Level 1 after  level2, scope: a: " + a + " b: " + b + " c: " + c + " d: " + d + " e: " + e);
}

level1();
console.log("Level 0 scope: a: " + a + " b: " + b + " c: " + c + " d: " + d + " e: " + e);

Данный пример иллюстрирует отсутствие переменных e в глобальной области видимости и level1, переменной d - в глобальной области видимости. Переменная d попадает из level1 в level2.

Функции высшего порядка.

Функция как тип данных.

Функции в JS являются типом данных, наряду с числами и строками. Определение функции является выражением и вычисляется как значение типа данных function:

function a(){
}
alert(typeof a);

Набор операций с функциями невелик, в отличие от строк их нельзя конкатенировать, нельзя складывать и умножать как числа; однако их можно присваивать переменным и вызывать. JS позволяет создавать функции без названия:

a();
function a(){
    console.log('declared func');
}

someFuncVariable()
var someFuncVariable = function (){
    console.log('anon func');
}

someFuncVariable()

var b = a;
a = null;
a()
a = b
a()

Знакомство: ООП в функциональном стиле. .

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

var rectangle = {
    x: 0,
    y: 0,
    w: 100,
    h: 100,
    color: "black",

    draw: function(/* this */){
        console.log("I'm drawing a rectangle with coordinates " + this.x + "x" + this.y + " and dimensions " + this.w + 'x' + this.h + " in " + this.color + " color");
    },

    setColor: function(/* this, */ color){
        this.color = color;
        this.draw();
    }
}

rectangle.draw();

this позволяет функциям-полям объектов получить доступ к другим полям объекта (x, y и другие в примере выше)

Функции высшего порядка

Функциями высшего порядка называют функции, которые оперируют другими функциями - принимают их в качестве параметров или возвращают как результат выполнения. Такой подход позволяет произвести инъекцию своего кода. Например, все реализации алгоритма сортировки сравнивают разные сортируемые элементы, при этом для работы алгоритма вовсе не обязательно знать структуру сортируемых данных; достаточно просто знать, какой элемент из двух больше или меньше.

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

var arrayOfNumbers = [4,18,10,2,-1,100, 0, 0.5];
arrayOfNumbers.sort(); //сортирует, используя обычное строковое сравнение `<` и `>`

function numberSort(a, b){
    var result = a > b ? 1 : -1;
    console.log("Нас вызвали для сравнения " + a + " и " + b + ". Результат будет " + result);
    return result;
}
arrayOfNumbers.sort(numberSort); //сортировка по числовому значению

Первый sort выше сортирует, используя знаки < для элементов массива, интерпретируя элементы как строки;

Второй sort принимает в качестве параметра функцию, которая вызывается многократно внутри sort для некой пары сортируемых элементов. Пара выбирается согласно логике алгоритма сортировки; выбор же, кто из этих двух элементов больше, а кто - меньше, возлагается на переданную функцию numberSort, которая должна вернуть 1 если а считается больше b и -1 в обратном случае. В случае равенства a и b - возвращается 0, однако это можно не использовать

Таким же образом мы можем отсортировать по тому или иному критерию массив объектов (ассоциативных массивов), например:

var persons = [
    {name: "Иван", age: 17},
    {name: "Мария", age: 35},
    {name: "Алексей", age: 73},
    {name: "Яков", age: 12},
]
persons.sort(function(a,b){ //сортируем по возрасту
    if (a.age > b.age){
        return 1;
    }
    return -1;
});

persons.sort(function(a,b){ //сортируем по имени
    if (a.name > b.name){
        return 1;
    }
    return -1;
});

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

function intPrompt(message, defaultValue)
{
    do {
        var value = prompt(message, defaultValue)
    } while(value !== null && isNaN(+value) || !Number.isInteger(+value))
    return value
}

function gamePrompt(message, defaultValue)
{
    do {
        var value = prompt(message, defaultValue)
    } while(value !== null && !(value == 'камень' || value == 'ножницы' || value == 'бумага'))
    return value
}

Далее идет общее решение ввода с валидацией:

function validatedPrompt(message, defaultValue, validator)
{
    do {
        var value = prompt(message, defaultValue);
    } while( value !== null && !validator(value));
    return value;
}
alert(validatedPrompt("number", "", function(value) {
            return !isNaN(+value) && Number.isInteger(+value) 
}))

alert(validatedPrompt("камень-нжнцы-бмг", "", function(value) {
            return ["камень", "ножницы", "бумага"].indexOf(value.toLowerCase()) > -1;
}))

В этом примере код валидации выделен в функцию обратного вызова, а общее решение циклического ввода находится в функции validatedPrompt

Для чего используются функции.

  • Для избавления повторяющихся кусков кода. DRY
  • Для структуризации и задания имени какой-либо последовательности операций. Например зачастую в начале работы кода запускают однократно функцию init (имя приведено для примера), которая выполняется один раз, т. е. не уменьшает объем кода. Однако, таким образом, все действия, которые относятся к подготовке программного окружения, заносятся в отдельный блок кода, что более наглядно
  • Функции обратного вызова используются для внедрения кода, как в случае с sort и validatedPrompt
  • Функции обратного вызова используются для того, что бы отказаться от опроса (poll) и перейти к событийной архитектуре (push), т. е. вместо постоянной проверки произошло то или иное событие или нет - происходит вызов callback когда это событие произошло.
console.log("Начал");
setTimeout(function(){
    console.log("Отложил на 5 сек");
}, 5000);
console.log("Закончил");

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

  • Функции используются для создания обособленной области видимости, что бы не нарушать окружающее пространство имен:
(function(){
    var a = 5;
    var b = "100500";
})()

в данном примере создается функция и тут же вызывается. Функция не сохраняется ни в какой из переменных, а значит вы не сможете её вызвать более чем один раз. Единственная цель такой функции (Self-Invoked Function) - создать свою собственную область видимости, в которой можно оперировать любыми именами переменных не опасаясь побочных эффектов и влияния на переменные окружающего кода. Просто блок кода со своими именами.