07. Функции. Часть 1. Общие вопросы

Контрольные задания

  1. Испытательный стенд При работе с объектами стандартная функция alert малопригодна. Она выводит на экран результат, возвращаемый методом toString объекта. Метод toString наследуется от Object и преобразует объект в строку "[оbject Object]". Создайте функцию alertob, которая выводила бы на экран свойства объектов и их значения в удобном для восприятия виде. Используйте в дальнейшем эту функцию при отладке кодов, которые работают с объектами.

    Решение
    
    function alertob(ob)
    {
      var a = [];                       // Поместим элементы объекта в массив,
      for(p in ob) a.push(p+":"+ob[p]); // а затем преобразуем массив методом 
      alert("{"+a.join(",")+"}");       // join в строку с разделителем "," 
    }
    
    // Тест
    var ob1 = {};
    alertob(ob1);             // {}
    var ob2 = {x:1,y:"киса"};
    alertob(ob2);             // {x:1,y:киса}
    
  2. Испытательный стенд Напишите функцию, которая добавляет значения свойств объекта в массив (в предположении, что значения свойств объекта имеют элементарный тип данных).

    
    // Добавить значения свойств объекта в массив и вернуть этот массив. 
    // Если массив не задан, создать его 
    function addObjectToArray    
      (
       ob, // объект, значения свойств которого добавляются в массив
       a   // массив, в который идет добавление (необязательный аргумент)
      )
    {
     ...
    }
    var m = [1,2];
    var d = {x:10,y:20,z:"лиса"}
    var x = addObjectToArray(d,m); // [1,2,10,20,"лиса"] 
    var y = addObjectToArray(d);   // [10,20,"лиса"]
    
    Решение
    
    // Добавить значения свойств объекта в массив и вернуть этот массив. 
    // Если массив не задан, создать его 
    function addObjectToArray    
      (
       ob, // объект, значения свойств которого добавляются в массив
       a   // массив, в который идет добавление (необязательный аргумент)
      )
    {
      a = a || [];
      for(var i in ob) a.push(ob[i]);
      return a;
    }
    var m = [1,2];
    var d = {x:10,y:20,z:"лиса"}
    var x = addObjectToArray(d,m); // [1,2,10,20,"лиса"] 
    var y = addObjectToArray(d);   // [10,20,"лиса"]
    

    Отметим, что написанная функция addObjectToArray будет корректно работать только для объектов, содержащих элементарные типы данных (числа, строки, логические значения). Только такие данные будут реально копироваться методом push. Если ob — объект, push(ob) будет копировать ссылку на ob, а не сам объект.

    
    var m = [1,2];
    var d = {x:10,y:{t:"кот"}};
    var x = addObjectToArray(d,m); // [1,2,10,{t:"кот"}] 
    d.y.t="лиса";
    x[3].t == "лиса"; // Равно true
    
  3. Испытательный стенд Напишите функцию sumObject, которая возвращает объект, объединяющий объекты, передаваемые в функцию в качестве аргументов. Объединение получается следующим образом: в результирующий объект добавляются те свойства (вместе со значениями), которых в результирующем объекте еще нет.

    Примеры использования этой функции:

    
    sumObject();    // Равно {}
    var ob1 = {x:1};
    sumObject(ob1); // Равно {x:1}
    var ob2 = {x:10,y:20};
    sumObject(ob1,ob2); // Равно {x:1,y:20}
    sumObject(ob2,ob1); // Равно {x:10,y:20}
    var ob3 = {x:100,y:200,z:"кот"};
    sumObject(ob1,ob2,ob3); // Равно {x:1,y:20,z:"кот"}
    
    Решение
    
    function sumObject()    
    {
     var sum = {};
     // Цикл по числу фактических аргументов
     for(var i=0; i<arguments.length; i++) 
     {
        // Цикл по числу свойств в текущем объекте
        for(var p in arguments[i]) 
          // Если этого свойства в объекте sum еще нет, добавить 
          // его вместе с его значением 
          if (!(p in sum)) sum[p] = arguments[i][p];
     }
     return sum;
    }
    sumObject();    // Равно {}
    var ob1 = {x:1};
    sumObject(ob1); // Равно {x:1}
    var ob2 = {x:10,y:20};
    sumObject(ob1,ob2); // Равно {x:1,y:20}
    sumObject(ob2,ob1); // Равно {x:10,y:20}
    var ob3 = {x:100,y:200,z:"кот"};
    sumObject(ob1,ob2,ob3); // Равно {x:1,y:20,z:"кот"}
    

    Комментарий

    Будет ли корректно работать эта функция, если некоторые свойства объекта будут являться объектами? Например, как будет работать следующий код:

    
    var ob1 = {x:1};
    var ob2 = {x:10,y:{z:1}};
    var ob3 = sumObject(ob1,ob2); // Равно ли ob3 {x:1,y:{z:1}} ?
    

    Да, ob3, действительно, равен {x:1,y:{z:1}}, хотя инструкция sum[p] = arguments[i][p]; копирует, конечно, не объект {z:1}, а ссылку на него.

    А что будет, если мы теперь запишем delete ob2.y?

    Проверим. По прежнему ob3 равен {x:1,y:{z:1}}.

    Вот как это объясняется. Инструкция var ob2 = {x:10,y:{z:1}}; создала переменную ob2 и записала в нее ссылку на объект {x:10,y:{z:1}}, который разместила где-то в памяти.

    Свойство у в этом объекте само содержит ссылку на объект {z:1}, который размещается где-то в памяти.

    После работы функции, инструкция var ob3 = sumObject(ob1,ob2); записывает в переменную ob3 ссылку на созданный функцией объект. В этом объекте есть свойство y, и в него записана ссылка на объект {z:1} — тот же самый, на который ссылается свойство y в объекте ob2.

    Получается, что на один и тот же объект, расположенный где-то в памяти, ссылаются два разных свойства: ob2.y и ob3.y

    Применяем инструкцию delete ob2.y Она уничтожает свойство y объекта ob2. Уничтожается ссылка на объект {z:1}, но не сам объект! Объект в памяти сохраняется потому, что существует вторая ссылка на него ob3.y.

    Вот если бы на объект {z:1} ссылок больше не было, сборщик мусора JavaScript уничтожил бы объект {z:1} автоматически. Но ссылка есть, объект в памяти сохраняется.

    Однако надо помнить, что функция sumObject реально объекты не копирует, а копирует только ссылки на них. И если объект меняется по первой ссылке, изменения будут видны и по второй.

    
    var ob1 = {x:1};
    var ob2 = {x:10,y:{z:1}};
    var ob3 = sumObject(ob1,ob2); // Равно {x:1,y:{z:1}}
    ob2.y.z="лист";
    ob3.y.z == "лист";  // Равно true
    

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

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

  4. Испытательный стенд Написать функцию goExecutor, которая выполняла бы программу, записанную для некоторого исполнителя.

    
    // Выполнить программу исполнителя
    // Аргументы:
    // program  -- строка, содержащая программу исполнителя
    //             Программа представляет собой последовательность 
    //             команд исполнителя из его СКИ. Команды отделяются 
    //             друг от друга одним или несколькими пробелами.
    //             Начальные и конечные пробелы в строке игнорируются.
    //             Команды исполнителя -- это идентификаторы (без параметров)
    // executor -- объект, реализующий среду исполнителя и его СКИ.
    //             СКИ исполнителя задаётся объектом ski, состоящим из пар 
    //             "команда:функция_которая_эту_команду_выполняет"
    // Возвращаемое значение:
    //              0 -- программа выполнена
    //              1 -- ошибочная команда ("Не понял") 
    //              2 -- ошибка при выполнении программы ("Не могу").  
    //                   Эта ошибка возвращается функциями из СКИ.
    //                   Если ошибок при выполнении не было, функции 
    //                   СКИ возвращают 0.
    function goExecutor(program, executor)
    {
      // Здесь код тела функции, который вы должны написать 
    }
    

    Тестирование

    Проверим работу построенной функции на исполнителе Кукарача, который описан ниже.

    Среда Кукарача — клетчатое поле. В начальный момент исполнитель расположен в левом верхнем углу своего поля. СКИ исполнителя состоит из четырёх команд: ВЛЕВО, ВПРАВО, ВНИЗ и ВВЕРХ. Команды перемещают исполнителя на одну клетку, соответственно, влево, вправо, вниз и вверх. Если команда пытается переместить исполнителя за пределы поля, возникает ошибка времени исполнения ("Не могу"). Опишем исполнителя в виде объекта Cockroach. Так как исполнитель предполагается в одном экземпляре, объект построим не с помощью функции-конструктора, а с помощью литерала:

    
    // Исполнитель Кукарача
    // --------------------
    var Cockroach =  
    {
      // Среда
      x:0,           // Координата исполнителя по горизонтали
      y:0,           // Координата исполнителя по вертикали
      xLength:5,     // Число клеток по горизонтали 
      yLength:5,     // Число клеток по вертикали
    
      // СКИ
      ski:
      {
        "ВЛЕВО": 
         function()    // Если исполнитель не в первом столбце,  
         {             // переместить его влево и вернуть 0,
           var rez=0;  // иначе вернуть 2.
           this.x>0 ? this.x-- : rez=2;
           return rez;
         }, 
    
        "ВПРАВО": 
         function()    // Если исполнитель не в последнем столбце,  
         {             // переместить его вправо и вернуть 0,
           var rez=0;  // иначе вернуть 2.
           this.x<this.xLength-1 ? this.x++ : rez=2;
           return rez;
         }, 
        "ВВЕРХ": 
         function()    // Если исполнитель не в первой строке,  
         {             // переместить его вверх и вернуть 0,
           var rez=0;  // иначе вернуть 2.
           this.y>0 ? this.y-- : rez=2;
           return rez;
         }, 
        "ВНИЗ": 
         function()    // Если исполнитель не в последней строке,  
         {             // переместить его вниз и вернуть 0,
           var rez=0;  // иначе вернуть 2.
           this.y<this.yLength-1 ? this.y++ : rez=2;
           return rez;
         } 
      },
    
      // Сообщения
      mess:
      [
        "Выполнено",  // Программа успешно выполнена
        "Не понял",   // Команда не входит в СКИ
        "Не могу"     // Попытка переместить исполнителя за пределы поля
      ],
    
      // Переместить исполнителя в начальное положение
      home: function () {this.x = 0; this.y = 0;},
    
      // Показать среду исполнителя (подготовить строку для alert)
      show:  
        function () 
        {
          var str = "";
          for(var i=0; i<this.yLength; i++)
          {
           str += "\n";
            for(var j=0; j<this.xLength; j++)
              str += (i==this.y && j==this.x) ? "!" :".";
          }
          return str;
        }
    };
    

    Подготовим тестовый код:

    
    // Тестирование кода
    var rez = goExecutor("  ВПРАВО  ВПРАВО  ВНИЗ ", Cockroach);
    alert(Cockroach.mess[rez]+Cockroach.show());
    rez = goExecutor("  ВЛЕВО  ВНИЗ ", Cockroach);
    alert(Cockroach.mess[rez]+Cockroach.show());
    rez = goExecutor("ВНИЗ В ПРАВО", Cockroach);
    alert(Cockroach.mess[rez]+Cockroach.show());
    Cockroach.home();
    rez = goExecutor("ВНИЗ ВНИЗ ВНИЗ ВНИЗ ВНИЗ", Cockroach);
    alert(Cockroach.mess[rez]+Cockroach.show());
    

    Вот что мы должны увидеть на экране во время выполнения этого кода:

    
    Выполнено
    . . . . . 
    . . ! . . 
    . . . . . 
    . . . . . 
    . . . . . 
    
    Выполнено
    . . . . . 
    . . . . . 
    . ! . . . 
    . . . . . 
    . . . . . 
    
    Не понял
    . . . . . 
    . . . . . 
    . . . . . 
    . ! . . . 
    . . . . . 
    
    Не могу
    . . . . . 
    . . . . . 
    . . . . . 
    . . . . . 
    ! . . . . 
    
    Решение
    
    // Выполнить программу исполнителя
    // Аргументы:
    // program  -- строка, содержащая программу исполнителя
    //             Программа представляет собой последовательность 
    //             команд исполнителя из его СКИ. Команды отделяются 
    //             друг от друга одним или несколькими пробелами.
    //             Начальные и конечные пробелы в строке игнорируются.
    //             Команды исполнителя -- это идентификаторы (без параметров)
    // executor -- объект, реализующий среду исполнителя и его СКИ.
    //             СКИ исполнителя задаётся объектом ski, состоящим из пар 
    //             "команда:функция_которая_эту_команду_выполняет"
    // Возвращаемое значение:
    //              0 -- программа выполнена
    //              1 -- ошибочная команда ("Не понял") 
    //              2 -- ошибка при выполнении программы ("Не могу").  
    //                   Эта ошибка возвращается функциями из СКИ.
    //                   Если ошибок при выполнении не было, функции 
    //                   СКИ возвращают 0.
    function goExecutor(program, executor)
    {
      // Предварительная обработка program
      // ---------------------------------
      // Удалить начальные и конечные пробелы
      program = program.replace(/^\s+|\s+$/g,'');
      // Преобразовать program в массив команд
      // --------------------------------------
      var commands = program.split(/\s+/);
      
      var rez = 0;
      // Выполнить программу
      for(var i=0; i<commands.length && !rez; i++) // Цикл по командам
        // Выполнить очередную команду, если она есть в CКИ
        rez = (commands[i] in executor.ski) ? 
                  executor.ski[commands[i]].call(executor) : 1;
      return rez; // Возвращаем флаг выполнения
    }
    
    // Исполнитель Кукарача
    // --------------------
    var Cockroach =  
    {
      // Среда
      x:0,           // Координата исполнителя по горизонтали
      y:0,           // Координата исполнителя по вертикали
      xLength:5,     // Число клеток по горизонтали 
      yLength:5,     // Число клеток по вертикали
    
      // СКИ
      ski:
      {
        "ВЛЕВО": 
         function()    // Если исполнитель не в первом столбце,  
         {             // переместить его влево и вернуть 0,
           var rez=0;  // иначе вернуть 2.
           this.x>0 ? this.x-- : rez=2;
           return rez;
         }, 
    
        "ВПРАВО": 
         function()    // Если исполнитель не в последнем столбце,  
         {             // переместить его вправо и вернуть 0,
           var rez=0;  // иначе вернуть 2.
           this.x<this.xLength-1 ? this.x++ : rez=2;
           return rez;
         }, 
        "ВВЕРХ": 
         function()    // Если исполнитель не в первой строке,  
         {             // переместить его вверх и вернуть 0,
           var rez=0;  // иначе вернуть 2.
           this.y>0 ? this.y-- : rez=2;
           return rez;
         }, 
        "ВНИЗ": 
         function()    // Если исполнитель не в последней строке,  
         {             // переместить его вниз и вернуть 0,
           var rez=0;  // иначе вернуть 2.
           this.y<this.yLength-1 ? this.y++ : rez=2;
           return rez;
         } 
      },
    
      // Сообщения
      mess:
      [
        "Выполнено",  // Программа успешно выполнена
        "Не понял",   // Команда не входит в СКИ
        "Не могу"     // Попытка переместить исполнителя за пределы поля
      ],
    
      // Переместить исполнителя в начальное положение
      home: function () {this.x = 0; this.y = 0;},
    
      // Показать среду исполнителя (подготовить строку для alert)
      show:  
        function () 
        {
          var str = "";
          for(var i=0; i<this.yLength; i++)
          {
           str += "\n";
            for(var j=0; j<this.xLength; j++)
              str += (i==this.y && j==this.x) ? "!" :".";
          }
          return str;
        }
    };
    
    // Тестирование кода
    var rez = goExecutor("  ВПРАВО  ВПРАВО  ВНИЗ ", Cockroach);
    alert(Cockroach.mess[rez]+Cockroach.show());
    rez = goExecutor("  ВЛЕВО  ВНИЗ ", Cockroach);
    alert(Cockroach.mess[rez]+Cockroach.show());
    rez = goExecutor("ВНИЗ В ПРАВО", Cockroach);
    alert(Cockroach.mess[rez]+Cockroach.show());
    Cockroach.home();
    rez = goExecutor("ВНИЗ ВНИЗ ВНИЗ ВНИЗ ВНИЗ", Cockroach);
    alert(Cockroach.mess[rez]+Cockroach.show());
    

    Скопируйте этот код в испытатель и запустите.

    Пояснения, пожалуй, заслуживает лишь одна инструкция:

    
        rez = (commands[i] in executor.ski) ? 
                  executor.ski[commands[i]].call(executor) : 1;
    

    Во-первых, здесь переменной rez присваивается результат выполнения условного трехместного оператора ?. Напомню его формат:

    (логическое_выражение) ? выражение1 : выражение2

    Работает так: проверяется логическое_выражение. Если оно истинно, результатом операции ? является значение, полученное при вычислении выражения 1, иначе — значение, полученное при вычислении выражения 2.

    В нашем случае (логическое_выражение) есть (commands[i] in executor.ski) Проверяется, входит ли свойство commands[i] в состав объекта executor.ski Если входит, вычисляется выражение executor.ski[commands[i]].call(executor).

    Это выражение — вызов соответствующей функции из СКИ в контексте объекта executor.

    Заметим, что если бы мы записали вызов функции обычным образом: executor.ski[commands[i]](), то this в этой функции относился бы к объекту executor.ski, а нам нужно, чтобы он относился к объекту executor, в котором заданы переменные среды исполнителя. Именно поэтому используется метод call для явного указания контекста исполнения функции.

  5. Испытательный стенд Придумать и построить (в виде объекта) нового исполнителя (отличного от Кукарачи) и проверить работу построенной в задании 4 функции goExecutor на этом исполнителе.

    Решение
    
    // Выполнить программу исполнителя
    // Аргументы:
    // program  -- строка, содержащая программу исполнителя
    //             Программа представляет собой последовательность 
    //             команд исполнителя из его СКИ. Команды отделяются 
    //             друг от друга одним или несколькими пробелами.
    //             Начальные и конечные пробелы в строке игнорируются.
    //             Команды исполнителя -- это идентификаторы (без параметров)
    // executor -- объект, реализующий среду исполнителя и его СКИ
    //             СКИ исполнителя в объекте задаётся объектом ski,
    //             состоящим из пар "команда:функция_которая_эту_команду_выполняет"
    // Возвращаемое значение:
    //              0 -- программа выполнена
    //              1 -- ошибочная команда ("Не понял") 
    //              2 -- ошибка при выполнении программы ("Не могу").  
    //                   Эта ошибка возвращается функциями из СКИ.
    //                   Если ошибок при выполнении не было, функции 
    //                   СКИ возвращают 0.
    function goExecutor(program, executor)
    {
      // Предварительная обработка program
      // ---------------------------------
      // Удалить начальные и конечные пробелы
      program = program.replace(/^\s+|\s+$/g,'');
      // Преобразовать program в массив команд
      // --------------------------------------
      var commands = program.split(/\s+/);
      
      var rez = 0;
      // Выполнить программу
      for(var i=0; i<commands.length && !rez; i++) // Цикл по командам
        // Выполнить очередную команду, если она есть в CКИ
        rez = (commands[i] in executor.ski) ? executor.ski[commands[i]].call(executor) : 1;
      return rez; // Возвращаем флаг выполнения
    }
    // Исполнитель Вычислитель
    // -----------------------
    // Среда исполнителя -- ячейка памяти (number)
    // Исполнитель умеет увеличивать содержимое ячейки на 1 ("Добавить")
    // и умножать содержимое ячейки на 2 ("Умножить")
    var Numerator =  
    {
      // Среда
      number:0,       // Число
      // СКИ
      ski:
      {
        "Добавить": 
         function()    // Добавить к числу 1  
         {             
           this.number++;
           return 0;
         }, 
        "Умножить": 
         function()    // Умножить число на 2  
         {             
           this.number *= 2;
           return 0;
         } 
      },
    
      // Сообщения
      mess:
      [
        "Выполнено",  // Программа успешно выполнена
        "Не понял",   // Команда не входит в СКИ
        "Не могу"     // Попытка переместить исполнителя за пределы поля
      ],
    
      // Переместить исполнителя в начальное положение
      home: function () {this.number = 0;},
    
      // Показать среду исполнителя (подготовить строку для alert)
      show:  
        function () 
        {
          return "\nСодежимое ячейки: "+this.number;
        }
    };
    
    // Тестирование кода
    var rez = goExecutor("  Добавить Добавить Умножить", Numerator);
    alert(Numerator.mess[rez]+Numerator.show());
    rez = goExecutor("Добавить Умножить", Numerator);
    alert(Numerator.mess[rez]+Numerator.show());
    Numerator.home();
    rez = goExecutor("Добавить Умножить", Numerator);
    alert(Numerator.mess[rez]+Numerator.show());