06. Массивы
Контрольные задания
-
Написать функцию forEach, которая принимает в качестве первого аргумента массив и применяет к каждому его элементу функцию, заданную вторым аргументом. Если функция не задана, массив не меняется.
var a = [0,1,2,3,4,5]; forEach(a); // Масcив a не изменился var a = [0,1,2,3,4,5]; // Возвести все элементы массива в квадрат forEach(a,function(x){return x*x;}); // Массив a: [0,1,4,9,16,25]
Решениеfunction forEach(a,f) { if (f) for(var i=0; i<a.length; i++) a[i]=f(a[i]); }
Комментарий
Локальной переменной a присваивается ссылка на массив, который располагается где-то в памяти (вне функции). Поэтому a[i] — это формула доступа к i-ому элементу массива внутри функции. Можно читать элементы, а можно их менять при помощи инструкции a[i]=f(a[i]).
Тестирование
Как это проверить на испытательном стенде. Выносим в испытательный стенд такой код:
function forEach(a,f) { if (f) for(var i=0; i<a.length; i++) a[i]=f(a[i]); } var a = [0,1,2,3,4,5]; forEach(a); // Масcив a не изменился alert(a); // 0,1,2,3,4,5 // Возвести все элементы массива в квадрат forEach(a, function (x) {return x*x;} ); alert(a); // 0,1,4,9,16,25
-
Написать функцию filter, которая принимает в качестве первого аргумента массив и возвращает массив из элементов, для которых функция, заданная вторым аргументом, возвращает значение true.
var a = [0,-1,2,-3,45,-6]; // Создать массив из положительных элементов // исходного массива var b = filter(a, function (x) { return x>0;}); // [2,45] var a = [1,,,2,,3,undefined,4] // Создать массив из элементов исходного массива, // имеющих определённые значения. var b = filter(a, function (x) { return x != undefined;}); // [1,2,3,4] var a = [1,{x:1},"кот",[1,2],function(x){return x*x},true] // Создать массив из элементов исходного массива, имеющих // значения элементарного типа var b = filter(a, function (x) { return typeof(x) == "number" || typeof(x) == "string" || typeof(x) == "boolean"; }); // [1,"кот",true]
Решениеfunction filter(a,f) { var result = []; for(var i=0; i<a.length; i++) if (f(a[i])) result.push(a[i]); return result; }
Комментарий
Функция создает пустой массив и добавляет в него методом push те элементы исходного массива, которые проходят проверку.
Тестирование
Как это проверить на испытательном стенде. Выносим в испытательный стенд такой код:
function filter(a,f) { var result = []; for(var i=0; i<a.length; i++) if (f(a[i])) result.push(a[i]); return result; } var a = [0,-1,2,-3,45,-6]; // Создать массив из положительных элементов исходного массива var b = filter(a, function (x) { return x>0;}); // [2,45] alert(b); // 2,45 a = [1,,,2,,3,undefined,4] // Создать массив из элементов исходного массива, имеющих // определённые значения. b = filter(a, function (x) { return x != undefined;}); // [1,2,3,4] alert(b); // 1,2,3,4 a = [1,{x:1}, "кот", [1,2], function (x) {return x*x}, true] // Создать массив из элементов исходного массива, имеющих значения // элементарного типа b = filter(a, function (x) { return typeof(x) == "number" || typeof(x) == "string" || typeof(x) == "boolean"; }); // [1,"кот",true] alert(b); // 1,кот,true
-
Написать функцию toOne, которая принимает в качестве аргумента массив, каждый элемент которого содержит, либо значение элементарного типа, либо массив, подобный исходному. Функция должна возвращать массив, содержащий значения элементарного типа в том порядке, в котором они следуют в исходном массиве без учета вложенности.
var a = [0,1,[2,3],4,5,6]; toOne(a); // Возвращает [0,1,2,3,4,5,6] var a = [0,[1,[2]],3,[4,[[5,6]]]]; toOne(a); // Возвращает [0,1,2,3,4,5,6]
Решениеfunction toOne(a) { var result = []; for(var i=0; i<a.length; i++) if (typeof a[i] == "object") result = result.concat(toOne(a[i])); else result.push(a[i]); return result; }
Комментарий
Работает эта рекурсивная функция так.
- Создаётся пустой массив result.
-
Затем просматриваются все элементы массива-аргумента.
- Если очередной элемент имеет элементарный тип, он добавляется к result методом push.
- Если элемент является массивом на него воздействует функция toOne, которая преобразует его к массиву из значений элементарного типа. Этот массив при помощи метода concat добавляется к result. Так как метод result.concat возвращает результат, не меняя исходный массив result, результат присваивается result.
- Когда цикл завершается, возвращается массив result.
Пояснение
Инструкция:
var result = [];
работает так. Где-то в памяти создается массив (пустой) и ссылка на него записывается в локальную переменную result.
Слова «когда цикл завершается, возвращается массив result» означает следующее. Возвращается ссылка на массив, после чего локальная переменная result уничтожается. Заметьте, уничтожается не массив (который где-то в памяти, отдельно от функции), а локальная ссылка на него. Но если мы записываем x = toOne(a), то ссылка на созданный массив записывается в переменную x. Если мы вызываем toOne(a) без присваивания, ссылка пропадает. JavaScript обнаруживает, что на созданный массив нет ни одной ссылки, и запускает сборщик мусора.
Можно записать функцию, использую только метод push:
function toOne(a) { var result = []; for(var i=0; i<a.length; i++) if (typeof a[i] == "object") { var t = toOne(a[i]); for(var j=0; j<t.length; j++) result.push(t[i]); } else result.push(a[i]); return result; }
Этот вариант менее красив. Что касается его эффективности, то вопрос открыт: требуются тестовые испытания с замером времени исполнения и анализом затрат памяти. Все зависит от эффективности реализаций методов concat и push, про которые мы ничего не знаем.
-
Написать функцию-конструктор Matrix(n,M) для работы с квадратными матрицами размерностью nxn.
Аргументы конструктора: n Количество строк (и столбцов) матрицы M Матрица, необязательный параметр, если отсутствует, создается матрица, заполненная нулями. Конструктор должен поддерживать три метода: sum Сумма матриц mul Произведение матриц toString Переопределить метод toString так, чтобы результирующая строка содержала элементы матрицы по строкам, и каждая строка завершалась бы символом "\n" — переводом строки. Определения
Матрица размерностью mxn — это таблица, в которой m строк и n столбцов. В квадратной матрице m равно n, то есть число строк равно числу столбов.
Пусть даны две квадратные матрицы A и B:
a11 a12 ... a1n A = ... an1 an2 ... ann b11 b12 ... b1n B = ... bn1 bn2 ... bnn
Матрица C
c11 c12 ... c1n C = ... cn1 cn2 ... cnn
является суммой матриц A и B (C=A+B), если: cij = aij + bij для всех i и j от 1 до n.
То есть каждый элемент суммы равен сумме аналогичных элементов.
Пример
11 12 13 A = 21 22 23 31 32 33 1100 1200 1300 B = 2100 2200 2300 3100 3200 3300 1111 1212 1313 A+B = 2121 2222 2323 3131 3232 3333
Матрица С является произведением матриц A и B (C=A*B), если:
n cij = сумма(aik*bkj) для всех i и j от 1 до n. k=1
То есть каждый элемент cij равен сумме произведений элементов из i-ой строки матрицы A и j-го столбца матрицы B. Говорят, cij равно произведению i-ой строки матрицы A на j-ый столбец матрицы B.
Пример
11 12 13 A = 21 22 23 31 32 33 1100 1200 1300 B = 2100 2200 2300 3100 3200 3300 Первая строка матрицы-произведения: c11 = 11*1100 + 12*2100 + 13*3100 = 77600 c12 = 11*1200 + 12*2200 + 13*3200 = 81200 c12 = 11*1300 + 12*2300 + 13*3300 = 84800 Вторая строка матрицы-произведения: c21 = 21*1100 + 22*2100 + 23*3100 = 140600 c22 = 21*1200 + 22*2200 + 23*3200 = 147200 c23 = 21*1300 + 22*2300 + 23*3300 = 153800 Третья строка матрицы-произведения: c31 = 31*1100 + 32*2100 + 33*3100 = 203600 c32 = 31*1200 + 32*2200 + 33*3200 = 213200 c33 = 31*1300 + 32*2300 + 33*3300 = 222800 77600 81200 84800 С = A*B = 140600 147200 153800 203600 213200 222800
Пример использования конструктора
var m1 = new Matrix(3); var m2 = new Matrix(3, [ [11,12,13], [21,22,23], [31,32,33] ] ); m1 = m1.sum(m2); var m2 = new Matrix(3, [ [1100,1200,1300], [2100,2200,2300], [3100,3200,3300] ] ); var m4 = m1.sum(m2); alert(m4); // Получили: // 1111, 1212, 1313 // 2121, 2222, 2323 // 3131, 3232, 3333 // (alert использует метод toString автоматически) var m5 = m1.mul(m2); alert(m5); // Получили: // 77600, 81200, 84800 // 140600, 147200, 153800 // 203600, 213200, 222800 // (alert использует метод toString автоматически)
Решениеfunction Matrix(n,M) { this.n = n; // Создаём матрицу this.M = new Array(n); // Инициализируем матрицу for (var i=0; i<n; i++) { this.M[i] = new Array(n); for (var j=0; j<n; j++) this.M[i][j] = M ? M[i][j] : 0; } // Сложение текущей матрицы с матрицей matr this.sum = function (matr) { var a = new Matrix(this.n); for (var i=0; i<this.n; i++) for (var j=0; j<this.n; j++) a.M[i][j] = this.M[i][j] + matr.M[i][j]; return a; } // Умножение текущей матрицы на матрицу matr this.mul = function (matr) { var a = new Matrix(this.n); for (var i=0; i<this.n; i++) for (var j=0; j<this.n; j++) { var s = 0; for (var k=0; k<this.n; k++) s += this.M[i][k]*matr.M[k][j]; a.M[i][j] = s; } return a; } // Преобразование матрицы в строку this.toString = function () { var str = ""; for (var i=0; i<this.n; i++) str += this.M[i].join(", ") + "\n"; return str; } }
Комментарий
Создание и инициализация матрицы:
01 // Создаём матрицу 02 this.M = new Array(n); 03 // Инициализируем матрицу 04 for (var i=0; i<n; i++) 05 { 06 this.M[i] = new Array(n); 07 for (var j=0; j<n; j++) this.M[i][j] = M ? M[i][j] : 0; 08 }
В строке 02 в объекте (на который ссылается ключевое слово this) будет создано свойство М в виде массива из n элементов (строки будущей матрицы).
В строке 06 каждому элементу с номером i созданного массива присваивается массив из n элементов.
В строке 07 каждый элемент созданного массива инициализируется соответствующим элементом второго аргумента, а если его нет — нулем.
Сложение текущей матрицы с матрицей matr:
01 this.sum = function (matr) 02 { 03 var a = new Matrix(this.n); 04 for (var i=0; i<this.n; i++) 05 for (var j=0; j<this.n; j++) 06 a.M[i][j] = this.M[i][j] + matr.M[i][j]; 07 return a; 08 }
В строке 03 создаётся объект Matrix — нулевая квадратная матрица c размером, равным размеру текущей матрицы. Для построения этого объекта применяется создаваемый конструктор, но никакой рекурсии не возникает. Смотрите комментарии к решению задания 5 из 4 заметки (про комплексные числа). (Суть: методы будут вызываться уже после того, как будет создан экземпляр объекта.)
Теперь остается только заполнить построенную нулевую матрицу элементами, каждый из которых равен соответствующей сумме элементов (текущей матрицы и матрицы, выступающей в качестве аргумента метода).
В строке 07 ссылка на построенную матрицу (равная сумме матриц) возвращается в качестве результата выполнения метода.
Можно было, конечно, этот метод записать и по-другому:
01 this.sum = function (matr) 02 { 03 var a = new Matrix(this.n, this.M); 04 for (var i=0; i<this.n; i++) 05 for (var j=0; j<this.n; j++) 06 a.M[i][j] += matr.M[i][j]; 07 return a; 08 }
Умножение текущей матрицы на матрицу matr выполняется аналогично сложению (не считая более сложной математики).
Преобразование матрицы в строку — прозрачно. Символ "\n" задает переход на новую строку при выводе на экран.
-
Написать функцию-конструктор и создать с ее помощью объект Plusik, который реализует Роботландского исполнителя Плюсик.
Плюсик — это модель стекового калькулятора. Стек служит для хранения чисел. Числа помещаются в стек по одному, подобно тому, как кольца детской пирамидки нанизываются на стержень. За один раз взять из стека можно только одно число с вершины числовой пирамиды. Копия числа в стеке не сохраняется, зато становится доступным следующее число, расположенное «ниже» взятого.
Cистема команд Плюсика: команда сокращенная форма описание запомни число з число Число, записанное в команде отправляется на хранение в стек. очисть о Из стека удаляются все элементы. сложи
вычти
умножь
делис
в
у
дВсе эти команды работают одинаково: из стека извлекаются два числа и над ними выполняется операция. Результат помещается в стек. Второе число, извлеченное из стека, вычислитель считает первым операндом операции. Ситуация отказа возникает когда:
- в стеке меньше двух чисел;
- выполняется деление на ноль.
Класс Plusik должен иметь следующий интерфейс:
-
метод make, который получает в качестве аргумента
команду Плюсика (в виде строки), выполняет ее и возвращает код ошибки:
- 0 — ошибок нет
- 1 — ошибка в записи команды («не понял»)
- 2 — ошибка при выполнении команды («не могу»)
- метод get, который возвращает число с верхушки стека
Класс Plusik, конечно, может иметь и любые другие свойства и методы, знать о существовании которых, пользователю необязательно.
Пользователь может вводить команды в любом регистре с любым числом пробелов перед командой, после нее и перед числом в команде запомни.
Примеры работы с Плюсиком
1) Вычислить: 12*5
var p = new Plusik(); // Создали Плюсика p.make("Запомни 12"); // Записали в стек 12, вернули 0 p.make("Запомни 5"); // Записали в стек 5, вернули 0 p.make("у"); // Выполнили умножение, вернули 0 p.get(); // Вернули 60
2) Вычислить: 100-(20+10)
var p = new Plusik(); // Создали Плюсика p.make(" З 100 "); // Записали в стек 100, вернули 0 p.make(" з 20"); // Записали в стек 20, вернули 0 p.make("з 10"); // Записали в стек 10, вернули 0 p.make("Сложи"); // Сложили 20 и 10, вернули 0 p.make("Вычти"); // Выполнили вычитание, вернули 0 p.get(); // Вернули 70
3) Аварийные сообщения
var p = new Plusik(); // Создали Плюсика p.make("Сохрани 100"); // Вернули 1 ("не понял") p.make("запомни"); // Вернули 2 ("не могу") p.make("запомни 1"); // Записали в стек 1, вернули 0 p.make("сложи"); // Вернули 2 ("не могу") p.make("запомни 0"); // Записали в стек 0, вернули 0 p.make("дели"); // Вернули 2 ("не могу")
Решениеfunction Plusik() { this.stack = []; // Стек Плюсика // Вернуть число с верхушки стека this.get = function () { return this.stack.length ? this.stack[this.stack.length-1] : ""; }; // СКИ Плюсика: команды и функции, которые выполняют эти команды // this.op[i][0] -- i-ая команда Плюсика в виде строки // this.op[i][1] -- функция, выполняющая i-ую команду Плюсика this.op = [ [ "запомни", function (stack, x) { if (isNaN(-x)) return 1; // x -- не число stack.push(x); return 0; } ], [ "сложи", function (stack) { if (stack.length < 2) return 2; // "не могу" var op2 = stack.pop(); stack.push((stack.pop()-0)+(op2-0)); return 0; } ], [ "вычти", function (stack) { if (stack.length < 2) return 2; // "не могу" var op2 = stack.pop(); stack.push(stack.pop()-op2); return 0; } ], [ "умножь", function (stack) { if (stack.length < 2) return 2; // "не могу" var op2 = stack.pop(); stack.push(stack.pop()*op2); return 0; } ], [ "дели", function (stack) { if (stack.length < 2) return 2; // "не могу" var op2 = stack.pop(); if (!(-op2)) return 2; // "не могу" stack.push(stack.pop()/op2); return 0; } ], [ "очисти", function (stack) { stack.length = 0; return 0; } ] ]; // Выполнение команды, заданной строкой str this.make = function (str) { // Удалим незначащие пробелы (в начале и конце строки) str = str.replace(/(^\s*)|(\s*$)/g,""); // Выделим лексемы (их может быть две или одна) var lex = str.split(/\s+/); // Переведем команду (это первая лексема) в нижний регистр lex[0] = lex[0].toLowerCase(); // Определим команду for(var i=0; i<this.op.length; i++) if (this.op[i][0] == lex[0] || this.op[i][0].charAt(0) == lex[0]) break; // Если команда не найдена, возвращаем 1 ("не понял") if (i>=this.op.length) return 1; // Выполним команду return this.op[i][1](this.stack,lex[1]); } } var p = new Plusik(); p.make("Запомни 10"); p.make("Запомни 20"); p.make("сложи"); alert(p.get()); // 30 ...
Комментарий
Построенное выше решение нельзя назвать коротким, но оно предельно простое в силу своей структурности:
-
Заводим стек для Плюсика:
this.stack = []; // Стек Плюсика
-
Определяем СКИ Плюсика, то есть задаем команды и функции, которые
эти команды выполняют
this.op = [ ... ];
-
Записываем метод
который принимает строку с командой Плюсика и передает ее на выполнение нужной функции из СКИ.this.make = function (str) {...};
Обратите внимание, в каждой функции, выполняющей команду из СКИ Плюсика, первым аргументом записан параметр stack. Например, так выглядит функция выполняющая команду запомни x:
function (stack, x) { if (isNaN(-x)) return 1; // x -- не число stack.push(x); return 0; }
Зачем этот аргумент? Ведь стек Плюсика доступен, как this.stack, и проще было бы записать функцию без первого аргумента:
function (x) { if (isNaN(-x)) return 1; // x -- не число this.stack.push(x); return 0; }
Увы, это работать не будет. Ибо ключевое слово this относится к объекту, в контексте которого будет работать функция. И этим объектом будет не объект Plusik, а массив this.op (массив тоже объект). Мы будем вызывать функцию как this.op[i][1](...), то есть контекстом ее выполнения будет массив this.op. Вот почему стек Плюсика явно передается в функцию в качестве первого аргумента: this.op[i][1](this.stack,lex[1])
Вспомним определение из заметки 4:
Объект, посредством которого вызывается метод, становится значением ключевого слова this.
И вот еще один уместный по данному поводу фрагмент из заметки 4.
Пусть задано следующее определение функции:
function sq() {return this.x*this.y;}
и два объекта:
var rect = { x:10, y:20, square:sq }; var cube = { x:2, y:3, z:16, square:sq };
Тогда:
rect.square() // Равно 200 // Вызывается функция sq(), в ней this ссылается // на объект rect cube.square() // Равно 6 // Вызывается функция sq(), в ней this ссылается // на объект cube
Ничего не изменится, если функцию sq() задать в виде литерала внутри первого объекта. При вызове в контексте второго объекта this внутри этой функции будет ссылаться не на первый, а на второй объект, в контексте которого выполняется вызов:
var rect = { x:10, y:20, square:function () {return this.x*this.y;} }; var cube = { x:2, y:3, z:16, square:rect.square }; rect.square() // Равно 200 // Здесь this относится к rect cube.square() // Равно 6 // Здесь this относится к cube, хотя функция // определена в rect
Видим, насколько JavaScript является контекстно-зависимым языком. В других объектно-ориентированных языках методы жёстко привязаны к объектам, внутри которых они заданы, и не могут работать с другими объектами.
На самом деле, можно было поступить по-другому. Можно было обойтись без передачи аргумента stack:
this.op = [ [ "запомни", function (x) { if (isNaN(-x)) return 1; // x -- не число this.stack.push(x); return 0; } ], ...
если при вызове методов явно указывать объект вызова:
return this.op[i][1].call(this, lex[1]);
О методе call читайте в заметке 7.
Формат обращения к методу call:
функция.call(объект, список_фактических_аргументов);
Метод call позволяет выполнить функцию в контексте какого либо объекта, и тогда ключевое слово this внутри функции будет относиться к этому объекту.
Пример
var ob1 = {x:1,y:2}; var ob2 = {x:10,y:20}; function f(t) {return t*(this.x+this.y);} sum.call(ob1,2); // Равно 6 sum.call(ob2,2); // Равно 60