Глава 4Структури от данни: Обекти и масиви
На два пъти съм бил питан, „Кажете моля г-н Бабидж, ако поставите грешните данни в машината, ще излязат ли правилните отговори? […]“ Аз не съм в състояние правилно да схвана вида на обърканите идеи, които биха могли да провокират такъв въпрос”
Numbers, Booleans и strings са тухлите, от които структурите от данни са изградени. Но не можем да построим цяла къща с една- единствена тухла. Objects ни позволяват да групираме стойности - включително и други обекти заедно и по този начин да изградим по-сложни структури.
Програмите, които правихме досега бяха сериозно затруднени от факта, че действат само върху прости типове данни. Тази глава ще добави основно разбиране на структури от данни към нашия инструментариум. До края на тази глава вие ще знаете достатъчно, за да започнете да пишете някои полезни програми.
Ще работим преминавайки през повече или по-малко реалистично програмиране, въвеждане на понятия, които се отнасят към разглежданите проблеми. Например кодът, често ще се основава на функции и променливи, които бяха въведени по-рано в книгата.
The weresquirrel
Винаги, сега и тогава, обикновено между осем и десет вечерта, Жак се озоваваше трансформиран в малък космат гризач с пухкава опашка.
От една страна, Жак беше доста доволен, че няма класическа ликантропия. Превъплащението в катерица имаше тенденцията да предизвиква по-малко проблеми, отколкото да се превърне във вълк, например. Вместо да се налага да се притеснява за случайно изяждане на съседа(това би било неловко), той се притесняваше да не бъде изяден от котката на съседа. След два случая, когато той се събуди върху несигурно тънък клон в короната на един дъб, гол и дезориентриран, той предприе да заключва вратите и прозорците на стаята си през нощта и пускаше няколко ореха на пода, за да държи себе си зает.
Това трябваше да се погрижи за проблемите с котката и дъба. Но Жак все още страдаше от състоянието си. Нередовните случаи на трансформация го караха да се съмнява, че тя може да се задейства от нещо. За известно време той вярваше, че това се случва само в дните, когато е докосвал дървета. Така че, той спря изцяло да докосва дървета и дори избягваше случайна близост със тях. Но проблемът продължаваше да съществува.
Преминавайки към по-научен подход, Жак възнамеряваше да започне воденето на дневник за всичко, което той е правил този ден и дали това променя формата. С тази информация, се надяваше да стесни условията, които задействат трансформациите.
Първото нещо, което направи - проектира структура от данни, за да съхранява тази информация.
Набор от данни
За да можете да работите с парче цифрови данни, първо трябва да намерите начин да ги представите в паметта на вашата машина. Кажете един прост пример, с който искате да представите колекция от цифри: 2, 3, 5, 7 и 11.
Бихме могли да ги представим творчески със strings, в края на краищата, strings могат да бъдат с всякаква дължина, така че можем да поставим много данни в тях и да ги представим по следния начин "2 3 5 7 11"
. Но това е неудобно. Ще трябва някак да извлечем цифрите и да ги преобразуваме обратно в номера за да имаме достъп до тях.
За щастие, JavaScript осигурява начин специално за съхранение на поредици от стойности. Той се нарича масив (array) и е написан, като списък от стойности между квадратни скоби, разделени със запетаи.
var listOfNumbers = [2, 3, 5, 7, 11]; console.log(listOfNumbers[1]); // → 3 console.log(listOfNumbers[1 - 1]); // → 2
Означението за получаване на елементите вътре в масива също използва квадратни скоби. Чифт квадратни скоби веднага след израз с друг израз вътре в тях, вижда елементите в израза от лявата страна, които съответстват на индекса даден от израза в скобите.
Първият индекс в масива е нула, не едно. Така че, първият елемент може да се прочете с listOfNumbers[0]
. Ако нямате познания в програмирането, тази конвенция може да отнеме известно време за привикване. Нулевата база за преброяване има дълга традиция в областта на технологиите и стига да бъде следвана последователно (както в JavaScript ) то тя работи добре.
Свойства
Видяхме няколко подозрително изглеждащи израза, като myString.length
(за да получим дължината на string) и Math.max
функцията в последните примери. Това са изрази, които имат достъп до свойствата на някаква стойност. В първия случай имаме достъп до свойството length
на стойността в myString
. Във втория, достъп до свойството max
в Math
обекта (който е колекция от математически свързани стойности и функции).
Почти всички стойности на JavaScript имат свойства. Изключение правят null
и undefined
. Ако се опитате да получите достъп до свойствата на една от тези nonvalues, ще получите съобщение за грешка.
null.length; // → TypeError: Cannot read property 'length' of null
Двата най-често срещани начина за достъп до свойства в JavaScript са точка или квадратни скоби. И двете value.x
и value[x]
са достъп до свойството value
- но не са непременно едно и също свойство. Разликата е в това, как се тълкува x
. При израза с точка, частта след точката трябва да е валидно име на променлива и получавате директно името на свойството. При ползване на квадратни скоби, изразът между скобите се оценява, за да получите името на свойството. Какво имам в предвид, value.x
извлича свойството value
с име “x”, а value[x]
се опитва да направи оценка на израза x
и използва резултата, като име на свойство.
Така че ако знаете, че свойството, което ви интересува се нарича “length” вие казвате value.length
. Ако искате да извлечете свойство с името на стойността съдържана в променливата i
, вие казвате value[i]
. И тъй като, името на свойството може да бъде всеки string, ако искате да получите достъп до името на свойството “2” или “John Doe”, трябва да използвате квадратни скоби: value[2]
или value["John Doe"]
. Така се прави, въпреки, че знаете името на свойството предварително, защото нито “2”, нито “John Doe” са валидно име на променлива и за това не могат да бъдат достъпни чрез точкова нотация.
Елементите в масив се съхраняват в свойства. Тъй като, имената на тези свойства са числа, а ние често трябва да получим техните имена от една променлива, трябва да използваме синтаксиса на скобите, за да имаме достъп до тях. Свойството length
на масив ни казва колко елемента съдържа. Това име на свойство е валидно име на променлива, а ние знаем това име от по-рано, така че за да се намери дължината на масив, обикновено пишем array.length
защото това е по-лесно да се напише, отколкото array["length"]
.
Методи
И двата - strings и масиви обекти, съдържат в допълнение към свойството length
, редица свойства, които се отнасят за функционирането на стойностите.
var doh = "Doh"; console.log(typeof doh.toUpperCase); // → function console.log(doh.toUpperCase()); // → DOH
Всеки string има toUpperCase
свойство. Когато се извика, то ще върне копие на string-а, в което всички букви ще са превърнати в главни букви. Също така има и toLowerCase
. Може да се досетите, какво значи това.
Интересно е, че въпреки извикването на toUpperCase
, то не минава през всички аргументи, функцията по някакъв начин има достъп до стойността на string ”Doh”
, чието свойство извикахме. Как става това е описано в Глава 6.
Свойства, които съдържат функции обикновено се наричат методи на стойността, към която принадлежат. Както “toUpperCase
е метод на string”.
Този пример показва някои методи за масиви, които имат:
var mack = []; mack.push("Mack"); mack.push("the", "Knife"); console.log(mack); // → ["Mack", "the", "Knife"] console.log(mack.join(" ")); // → Mack the Knife console.log(mack.pop()); // → Knife console.log(mack); // → ["Mack", "the"]
Метода push
, може да се използва за добавяне на стойност в края на масива. Метода pop
прави точно обратното: премахва стойност от края на масива и я връща. Масив от strings, може да бъде обединен в един string с метода join
. Аргумента даден на join
определя текста за залепяне, който е между елементите на масива.
Обекти
Обратно към weresquirrel. Набор от ежедневните записки може да бъде представен, като масив. Но записите не се състоят от само номер или string - всеки запис трябва да съхранява списък от дейности и булева стойност, която показва дали Жак се превърнал в катерица. В идеалния случай, бихме могли да групираме тези стойности заедно в една стойност и след това да пуснем тези групирани стойности в масив от записи.
Стойности на типа обект са произволни колекции от свойства и ние можем да добавяме или премахваме тези свойства, както преценим. Един от начините за създаване на обект е с помощта на фигурни скоби.
var day1 = { squirrel: false, events: ["work", "touched tree", "pizza", "running", "television"] }; console.log(day1.squirrel); // → false console.log(day1.wolf); // → undefined day1.wolf = false; console.log(day1.wolf); // → false
Вътре във фигурните скоби, може да дадем списък от свойства, разделени със запетаи. Всяко свойство е написано като име, следвано от двуточие, следвано от израз, който осигурява стойност на свойството. Интервали и нови редове не са от значение. Когато даден обект обхваща, няколко линии, както в предишния пример, новите редове подобряват четимоста. Информациите чиито имена не са валидни имена на променливи или валидни номера, трябва да бъдат цитирани.
var descriptions = { work: "Went to work", "touched tree": "Touched a tree" };
Това означава, че фигурните скоби имат две значения в JavaScript. В началото на изявлението, те започват блок от твърдения. В друга позиция, те описват обект. За щастие, почти никога не е полезно да се започне изявление, като обект с къдрави скоби и в типични програми няма двусмислие между тези две цели.
Четене на свойство, което не съществува генерира стойност undefined
, както се случи с опита да прочетем wolf
в предишния пример.
Възможно е да се присвои стойност на свойство с оператора =
. Това ще замени стойността на свойството, ако тя вече съществува или ще създаде ново свойство в обекта, ако той не го е направил.
Да се върнем за кратко към нашия пример с пипалата от модела за обвързване на променливите, модела за обвързване на свойствата е подобен. Те хващат стойности, но може да има други променливи и свойства, които държат същите стойности. Може да мислите за обекти, като октоподи с произволен брой пипала, всяко от които има име изписано върху него.
Оператора delete
отрязва пипалото на такъв октопод. Това е унарен оператор, който когато се прилага, ще премахне името на свойството от обекта. Това не е често срещано, но е възможно да се направи.
var anObject = {left: 1, right: 2}; console.log(anObject.left); // → 1 delete anObject.left; console.log(anObject.left); // → undefined console.log("left" in anObject); // → false console.log("right" in anObject); // → true
Бинарния оператор in
, когато се прилага в string и обект, връща булева стойност, която показва дали този обект има това свойство. Разликата между настройване на свойството към undefined
и действителното му изтриване, е че в първия случай, обекта все още има свойството (то просто не разполага с определена стойност), докато във втория случай свойството вече не е налично и in
ще върне false
.
Масивите, а това са само един вид обект, са специализирани за съхранение на последователности от неща. Ако оцените typeof [1, 2]
, това ще произведе "object"
. Можете да си ги представите, като дълги, плоски октоподи с всичките си пипала по ред, етикетирани с номера.
Така можем да представим дневника на Жак, като масив от обекти.
var journal = [ {events: ["work", "touched tree", "pizza", "running", "television"], squirrel: false}, {events: ["work", "ice cream", "cauliflower", "lasagna", "touched tree", "brushed teeth"], squirrel: false}, {events: ["weekend", "cycling", "break", "peanuts", "beer"], squirrel: true}, /* and so on... */ ];
Изменчивост
Ще стигнем до реалното програмиране скоро. Но първо има едно последно парче теория за разбиране.
Видяхме, че стойностите на обекта могат да бъдат променяни. Типовете стойности, разгледани в предишните глави, като например номера, string и булев тип, са неизменни - невъзможно е да се промени съществуваща стойност на тези типове. Можете да ги комбинирате и да извличате нови стойности от тях, но когато вземете специфична string стойност, тази стойност винаги ще си остане същата. Текстът в нея не може да се променя. Ако имате референция към string, който съдържа "cat"
е невъзможно друг код да го промени в string "rat"
, например.
В обектите, от друга страна, съдържащата се стойност може да се промени, като се промнят нейните свойства.
Когато имаме две числа, 120 и 120, ние можем да ги разглеждаме по техния номер, независимо дали се отнасят до едни и същи физически бит. Но с обектите има разлика между двете препратки към същия обект, като има два различни обекта, които съдържат едни и същи свойства. Да разгледаме следния код:
var object1 = {value: 10}; var object2 = object1; var object3 = {value: 10}; console.log(object1 == object2); // → true console.log(object1 == object3); // → false object1.value = 15; console.log(object2.value); // → 15 console.log(object3.value); // → 10
Променливите object1
и
object2
държат един и същи обект и когато се промени object1
също се променя стойността на object2
. Променливата object3
сочи към друг обект, който първоначално съдържа същите свойства като object1
, но живее отделен живот.
Оператора ==
в JavaScript се използва, когато се сравняват обекти и ще върне true
само ако и двата обекта са точно с една и съща стойност. Сравнявайки различни обекти, ще върне false
, дори ако те имат идентично съдържание. Няма оператор за "дълбоко" сравнение, вграден в JavaScript, който разглежда съдържанието на обекта, но е възможно да го напишем сами (което ще бъде едно от упражненията в края на тази глава).
The lycanthrope’s log
И така Жак, стартира JavaScript интерпретатора си и създаде средата, от която се нуждаеше за да запази своя дневник.
var journal = []; function addEntry(events, didITurnIntoASquirrel) { journal.push({ events: events, squirrel: didITurnIntoASquirrel }); }
След това, всяка вечер в десет или понякога на следващата сутрин, след като се спусне от най-горния рафт на своята библиотека, той записваше случилото се през деня.
addEntry(["work", "touched tree", "pizza", "running", "television"], false); addEntry(["work", "ice cream", "cauliflower", "lasagna", "touched tree", "brushed teeth"], false); addEntry(["weekend", "cycling", "break", "peanuts", "beer"], true);
След като разполага с достатъчно точки от данни, той възнамеряваше да изчисли съотношението между неговата кетерицо-фикация и всяко от събитията през деня, и в идеалния случай да научи нещо полезно от тези корелации.
Корелация е мярка за зависимостта между променливите (променливи в статистическия смисъл, а не в смисъл на JavaScript). Тя обикновено се изразява, като коефициент, който варира между -1 и 1. Нулевата корелация означава, че променливите не са свързани, като се има в предвид, че съответствието на единия показва, че двата са напълно свързани и ако знаете единия, то знаете и другия. Отрицателната означава, че променливите са напълно свързани, но са противоположни - когато едното е true, другото е false.
За бинарни (булеви) променливи, коефициента на phi (ϕ) дава добра мярка за корелация и е относително лесно да се изчисли. За да се изчисли ϕ, имаме нужда от една таблица n, която съдържа броя пъти, когато са наблюдавани различни комбинации на двете променливи. Например, може да разгледаме случаите с ядене на пица и ги отбележим в таблица, като тази:
ϕ може да се изчисли по следната формула, където n се отнася за таблицата.
ϕ = |
n11n00 - n10n01
√
n1•n0•n•1n•0
|
Нотацията n01 показва броя на измерванията, където първата променлива (squirrelness) е false(0), а втората променлива (пица) е true(1) . В този пример n01 е 9.
Стойноста n1• се отнася за сумата от всички измервания, където първата променлива е true , като например 5 в таблицата. По същия начин n•0 се отнася за сумата от измервания, където втората променлива е false.
Така че, таблицата за пица, от горната страна над-линията за разделяне (дивидента) ще бъде 1 х 76 - 4 х 9 = 40, а частта под нея(делителя) ще бъде корен квадратен от 5 х 85 х 10 х 80 или √340000. От това излиза, че ϕ ≈ 0.069, което е малко. Яденето на пица не изглежда да има влияние върху трансформациите.
Изчисляване на корелации
Можем да представим таблица с две от две в JavaScript с масив от четири елемента ([76, 9,
4, 1]
). Можем също да използваме и други представяния, като масив съдържащ два масива с елементи ([[76, 9], [4, 1]]
) или обект с имена, като "11"
и "01"
, но масивът е семпъл и прави изразите за достъп до таблицата приятно кратки. Ние ще интерпретираме индексите на масива, като дву-битов бинарен номер, където най-лявата (най-значимата) цифра се отнася до променливата на катерицата и най-дясната (най-маловажната) цифра се отнася до променливата на събитията. Например бинарния номер 10
се отнася за случаите, когато Жак се превръща в катерица, но събитието (пица) не се е случвало. Това се случвало четири пъти. И тъй като бинарно 10
е 2 в десетична бройна система, ние ще съхраняваме този номер на индекс 2 в масива.
Това е функция, която изчислява коефициента на ϕ, като масив от:
function phi(table) { return (table[3] * table[0] - table[2] * table[1]) / Math.sqrt((table[2] + table[3]) * (table[0] + table[1]) * (table[1] + table[3]) * (table[0] + table[2])); } console.log(phi([76, 9, 4, 1])); // → 0.068599434
Това е просто един директен превод на формулата ϕ в JavaScript. Math.sqrt
е функция за корен квадратен, която е предвидена от Math
обекта в стандартна JavaScript среда. Ние трябва да сумираме две полета от таблицата, за да получим области, като n1•, защото сумите от редовете и колоните не се съхраняват директно в нашата структура от данни.
Жак води дневника си в продължение три месеца. Полученият набор от данни е на разположение в пясъчника за кодиране на тази глава, където той се съхранява в променливата JOURNAL
и в достъпен файл за теглене.
.
За да се извлече две по две таблица за конкретно събитие от този журнал, ние трябва да поставим всички записи в цикъл и да видим колко пъти дадено събитие се случва по отношение на трансформацията в катерица.
function hasEvent(event, entry) { return entry.events.indexOf(event) != -1; } function tableFor(event, journal) { var table = [0, 0, 0, 0]; for (var i = 0; i < journal.length; i++) { var entry = journal[i], index = 0; if (hasEvent(event, entry)) index += 1; if (entry.squirrel) index += 2; table[index] += 1; } return table; } console.log(tableFor("pizza", JOURNAL)); // → [76, 9, 4, 1]
Функцията hasEvent
тества дали даден запис съдържа дадено събитие. Масивите имат indexOf
метод, който се опитва да намери дадена стойност (в този случай име на събитие) в масива и връща индекса, при който е установено или -1 ако не е намерено. Така че, ако метода indexOf
не връща -1, тогава знаем, че събитието е намерено в записа.
Тялото на цикъла в tableFor
преценява в коя кутия на таблицата попада при всяко влизане, чрез проверка дали записа съдържа конкретно събитие от което се интересува и дали събитието съвпада с инцидента с катерицата. След това, цикъла добавя едно към броя в масива, който съответства на това поле в таблицата.
Сега имаме инструментите, които трябва да изчислят индивидуалните корелации. Единствената стъпка, която остава е да се намери съответствието за всеки тип събитие, което е записано и да видим дали нещо се откроява. Но как трябва да се съхраняват тези корелации, след като сме ги изчислили?
Обекти като карти (map)
Един възможен начин е да се съхраняват всички корелации в масив, използвайки обекти с name
и value
свойства. Но това прави търсенето на корелация за дадено събитие тромаво: ще трябва да мине цикъл през целия масив, за да намери обекта с правилното name
. Ние можем да приключим това търсене във функция, но щяхме да пишем повече код и компютъра щеше да върши повече работа, отколкото е необходимо.
По-добър начин е да се използва обектното свойство, кръстено на видовете събития event. Можем да използваме квадратни скоби за достъп при създаването и четенето на свойствата и да използваме оператора in
, за да провери дали съществува дадено свойство.
var map = {}; function storePhi(event, phi) { map[event] = phi; } storePhi("pizza", 0.069); storePhi("touched tree", -0.081); console.log("pizza" in map); // → true console.log(map["touched tree"]); // → -0.081
Картата (map) е начин да се премине от стойности в дадена област ( в този случай, имена на събития) в съответните стойности в друга област (в този случай, ϕ коефициенти).
Има няколко потенциални проблема при използването на обекти по този начин, които ще обсъдим в Глава 6, но за момента няма да се притесняваме за това.
Какво става, ако искаме да намерим всички събития, за които съхраняваме коефициент? Свойствата не образуват предвидима серия, когато ако са в масив, така че не можем да използваме нормален for
цикъл. JavaScript осигурява специален цикъл за преминаване през свойствата на даден обект. Той изглежда, като нормален for
цикъл, но се отличава с използването на думата in
.
for (var event in map) console.log("The correlation for '" + event + "' is " + map[event]); // → The correlation for 'pizza' is 0.069 // → The correlation for 'touched tree' is -0.081
Финален анализ
За да намерим всички видове събития, които се намират в набора от данни, ние просто обработваме всеки запис в ред от данните и след това минаваме с цикъл над събитията в този запис. Ние поддържаме обект phis
, който съдържа коефициентите на корелациите за всички видове събития, които сме видяли досега. Всеки път, когато попаднем на тип, който още не е в обекта phis
, ние изчисляваме неговата корелация и го добавяме към обекта.
function gatherCorrelations(journal) { var phis = {}; for (var entry = 0; entry < journal.length; entry++) { var events = journal[entry].events; for (var i = 0; i < events.length; i++) { var event = events[i]; if (!(event in phis)) phis[event] = phi(tableFor(event, journal)); } } return phis; } var correlations = gatherCorrelations(JOURNAL); console.log(correlations.pizza); // → 0.068599434
for (var event in correlations) console.log(event + ": " + correlations[event]); // → carrot: 0.0140970969 // → exercise: 0.0685994341 // → weekend: 0.1371988681 // → bread: -0.0757554019 // → pudding: -0.0648203724 // and so on...
Повечето корелации изглеждат близки до нула. Яденето на моркови, хляб или пудинг очевидно не задействат катерицо-трансформацията. Тя изглежда се появява малко по-често в събота и неделя обаче. Нека филтрираме резултатите, за да се покажат само корелации по-големи от 0.1 или по-малки от - 0.1.
for (var event in correlations) { var correlation = correlations[event]; if (correlation > 0.1 || correlation < -0.1) console.log(event + ": " + correlation); } // → weekend: 0.1371988681 // → brushed teeth: -0.3805211953 // → candy: 0.1296407447 // → work: -0.1371988681 // → spaghetti: 0.2425356250 // → reading: 0.1106828054 // → peanuts: 0.5902679812
А - ха! Има два фактора, чието съотношение е по-ясно изразено от другите. Яденето на фъстъци има силен, положителен ефект върху шанса да се превърне в катерица, докато миенето на зъби има значителен отрицателен ефект.
for (var i = 0; i < JOURNAL.length; i++) { var entry = JOURNAL[i]; if (hasEvent("peanuts", entry) && !hasEvent("brushed teeth", entry)) entry.events.push("peanut teeth"); } console.log(phi(tableFor("peanut teeth", JOURNAL))); // → 1
Well, няма грешка! Явлението се случва само когато Жак яде фъстъци и не си мие зъбите. Ако той не беше такъв мърляч на зъбната хигиена, нямаше никога да се сблъска с нещастието си.
Знаейки това, Жак просто спря да яде фъстъци и установи, че това напълно слага край на неговите трансформации.
Сега с Жак всичко е на ред за известно време. Но няколко години по-късно, той губи работата си и в крайна сметка е принуден да работи в един цирк, където изпълнява номера - Невероятната Катерицо-трансформация, тъпчейки устата си фъстъчено масло преди всяко шоу. Един ден, отвратен от това жалко съществуване, Жак не успя да се върне в неговата човешка форма и през една цепнатина в цирковата шатра, изчезна в гората. Никой никога не го е виждал отново.
Допълнителна масиво-логия
Преди да завършим тази глава, искам да ви запозная с още няколко понятия свързани с обекти. Ще започнем с въвеждането на някои по принцип полезни методи за масиви.
Видяхме push
и pop
, които добавят и премахват елементи в края на масива, по-рано в тази глава. Съответните методи за добавяне и премахване на елементи в началото на масива се наричат unshift
и shift
.
var todoList = []; function rememberTo(task) { todoList.push(task); } function whatIsNext() { return todoList.shift(); } function urgentlyRememberTo(task) { todoList.unshift(task); }
Тази програма управлява списъци от задачи. Можете да добавите задачи в края на списъка, като извикате rememberTo("eat")
и когато сте готови да направите нещо, извикайте whatIsNext()
за да вземете( и премахнете) предния елемент от списъка. Функцията urgentlyRememberTo
добавя задача, но я добавя към предната вместо към задната част на списъка.
Метода indexOf
има братче, наречено lastIndexOf
, който започва да търси дадения елемент от края на масива вместо отпред.
console.log([1, 2, 3, 2, 1].indexOf(2)); // → 1 console.log([1, 2, 3, 2, 1].lastIndexOf(2)); // → 3
И двата indexOf
и lastIndexOf
вземат втори не задължителен аргумент, който показва от къде да започне да търси.
Друг основен метод е slice
, който взема индекс за начало и индекс за край, и връща масив, който съдържа елементите между тези индекси. Стартовия индекс е включващ, крайният индекс е изключващ.
console.log([0, 1, 2, 3, 4].slice(2, 4)); // → [2, 3] console.log([0, 1, 2, 3, 4].slice(2)); // → [2, 3, 4]
Когато крайният индекс не е зададен, slice
ще вземе всички елементи, след стартовия индекс. Strings също имат slice
метод, който има подобен ефект.
Метода concat
може да се използва за залепване на масиви заедно, подобно на това, което оператора +
прави със strings. Следващия пример показва concat
и slice
в действие. Те вземат array и index и връщат нов масив, който е копие на оригиналния масив с елемента премахнат от дадения индекс.
function remove(array, index) { return array.slice(0, index) .concat(array.slice(index + 1)); } console.log(remove(["a", "b", "c", "d", "e"], 2)); // → ["a", "b", "d", "e"]
Strings и техните свойства
Можем да четем свойства, като length
и toUpperCase
от strings стойности. Но ако се опитате да добавите ново свойство, то не се добавя.
var myString = "Fido"; myString.myProperty = "value"; console.log(myString.myProperty); // → undefined
Стойности на strings, nuber и Boolean не са обекти и въпреки, че езикът не се оплаква, ако се опитате да им зададете нови свойства, те всъщност не съхраняват тези свойства. Стойностите са неизменни и не могат да бъдат променяни.
Но тези видове имат някои вградени свойства. Всяка стойност на string има редица методи. Най-полезните от тях вероятно са slice
и indexOf
, които приличат на методите на масива със същото име.
console.log("coconuts".slice(4, 7)); // → nut console.log("coconut".indexOf("u")); // → 5
Една от разликите е, че string-а на indexOf
може да е string съдържащ повече от един характер, докато съответния метод за масив е само за един единствен елемент.
console.log("one two three".indexOf("ee")); // → 11
Метода trim
премахва празното пространство (интервали, нови редове, раздели и др. подобни характери) от началото и края на string-a.
console.log(" okay \n ".trim()); // → okay
Вече видяхме, свойството length
на типа string. Достъп до характерите на отделните елементи в string-a може да се направи с метода charAt
, но също и като просто се четат цифровите свойства, както бихме направили в масив.
var string = "abc"; console.log(string.length); // → 3 console.log(string.charAt(0)); // → a console.log(string[1]); // → b
Обекта на аргументите
Всеки път, когато дадена функция се извиква, специална променлива наречена arguments
се добавя към средата, в която тялото на функцията се изпълнява. Тази променлива се отнася към обект, който притежава всички аргументи подадени на функцията. Не забравяйте, че в JavaScript ви е позволено да подадете повече (или по-малко ) аргументи към функция от броя на параметрите, които функцията обявява.
function noArguments() {} noArguments(1, 2, 3); // This is okay function threeArguments(a, b, c) {} threeArguments(); // And so is this
Обекта arguments
има свойство length
, което ни казва броят на аргументите, които се подават на функцията. Тя също има свойство за всеки аргумент, 0, 1, 2 и т.н.
Ако това ви прилича на масив сте прави, това много прилича на масив. Но този обект за съжаление, няма никакви методи, като масиви (като slice
или
indexOf
), така че е малко по-труден за използване от истински масив.
function argumentCounter() { console.log("You gave me", arguments.length, "arguments."); } argumentCounter("Straw man", "Tautology", "Ad hominem"); // → You gave me 3 arguments.
Някои функции могат да имат произволен брой аргументи, като console.log
. Те обикновено циклят над стойностите в техния arguments
обект и могат да бъдат използвани за създаване на много приятни интерфейси. Например да си спомним как създадохме вписванията в журнала на Жак.
addEntry(["work", "touched tree", "pizza", "running", "television"], false);
Тъй като, ще трябва да извикваме тази функция много пъти, бихме могли да създадем алтернатива , която е по-лесна за извикване.
function addEntry(squirrel) { var entry = {events: [], squirrel: squirrel}; for (var i = 1; i < arguments.length; i++) entry.events.push(arguments[i]); journal.push(entry); } addEntry(true, "work", "touched tree", "pizza", "running", "television");
Тази версия чете първия си аргумент (squirrel
) по нормален начин, а след това минава над останалата част от аргументите (цикъла започва от индекс 1, като прескача първия), за да ги събере в масив.
Math обект
Както видяхме, Math
e пакет от свързани с цифри полезни функции, като например Math.max
(максимум), Math.min
(минимум) и Math.sqrt
(корен квадратен).
Обекта Math
се ползва просто, като контейнер за група от свързана функционалност. Има само един обект Math
и почти никога полезен, само като стойност. Вместо това, той осигурява пространство от имена (namespace
), така че всички тези стойности на функции, да не трябва да бъдат глобални променливи.
Имайки твърде много глобални променливи се “замърсява” пространството от имена. Колкото повече имена са въведени, толкова по вероятно е случайно презаписване на стойността на някоя променлива. Например, не е малко вероятно да искате да назовете нещо с max
във вашата програма. Тъй като max
е вградена функция в JavaScript, прибрана на сигурно вътре в Math
обекта, не трябва да се притеснявате за това презаписване.
Много езици ще ви спрат или поне предупредят, когато дефинирате променлива с име, което вече е заето, JavaScript не го прави, така че бъдете внимателни.
Обратно към Math
обекта. Ако правите някаква тригонометрия, Math
може да помогне. Той съдържа cos
(косинус), sin
(синус) и tan
(тангенс), както и техните обратни функции acos
, asin
, и atan
респективно. Числото π (пи) - или поне най-близкото приближение, което се вписва в JavaScript, като номер е на разположение с Math.PI
. (Има една стара традиция за писане на програмни имена на постоянни стойности (константи) с главни букви.)
function randomPointOnCircle(radius) { var angle = Math.random() * 2 * Math.PI; return {x: radius * Math.cos(angle), y: radius * Math.sin(angle)}; } console.log(randomPointOnCircle(2)); // → {x: 0.3667, y: 1.966}
Ако sin и cos са нещо, с което не сте много добре запознати, не се притеснявайте. Когато се използват в Глава 13, аз ще ги обясня.
Предишният пример използва Math.random
. Това е функция, която връща нов случаен номер, между нула (включително) и едно, всеки път когато се извика.
console.log(Math.random()); // → 0.36993729369714856 console.log(Math.random()); // → 0.727367032552138 console.log(Math.random()); // → 0.40180766698904335
Въпреки, че компютрите са детерминирани машини - те винаги реагират по един и същи начин, ако се подават едни и същи входни данни - е възможно да произвеждат номера на случаен принцип. За да направи това, устройството поддържа редица номера във вътрешното си състояние. След това, всеки път, когато се искат случайни числа, тя изпълнява някои сложни детерминирани изчисления във вътрешността си и връща част от резултата на тези изчисления. Машината използва резултата за да промени вътрешното си състояние, така че произведените следващи случайни числа да бъдат различни.
Ако искаме цели случайни числа вместо дробни, можем да използваме Math.floor
(която закръгля на долу към най-близкото цяло число) преди резултата на Math.random
.
console.log(Math.floor(Math.random() * 10)); // → 2
Умножаването на случайни числа по 10, ни дава число по-голямо или равно на нула, но по-малко от 10. Тъй като Math.floor
закръгля на долу, този израз ще произведе с равен шанс всяко число от 0 до 9.
Също има функция Math.ceil
(която закръгля на горе до цяло число), както и Math.round
(закръгля до най-близкото цяло число).
Глобален обект
Глобален обхват е пространство, в което живеят глобални променливи и към него може да се подхожда, като към всеки обект в JavaScript. Всяка глобална променлива присъства, като свойство на този обект. В браузърите, глобалния обхват на обекта се съхранява в window
променлива.
var myVar = 10; console.log("myVar" in window); // → true console.log(window.myVar); // → 10
Резюме
Обекти и масиви (които са специфични видове обекти) осигуряват начини за групиране на няколко стойности в една единствена стойност. Концептуално, това ни позволява да сложим един куп свързани неща в един пакет, вместо да се опитваме да приключим всички отделни неща и да ги ползваме по отделно.
Повечето стойности в JavaScript имат свойства с изключение на null
и undefined
. Информацията се достига чрез value.propName
или value["propName"]
. Обектите са склонни да използват имена за техните свойства и съхранение на повече или по-малко фиксиран набор от тях. Масивите, от друга страна, обикновено съдържат различен брой концептуално идентични стойности и използват номера(започвайки от 0) за имена на техните свойства.
Има някои именувани свойства за масиви, като length
и както и редица методи. Методите са функции, които живеят в свойства и (обикновено) действат на стойността, на която са свойство.
Обектите могат да служат и като карти, за асоцииране на стойностти с имена. Оператора in
може да се използва , за да се разбере, дали даден обект съдържа свойство с дадено име. Същата ключова дума може да се използва и за цикъл (for (var name in object)
), който минава над свойствата на даден обект.
Упражнения
Сумата на редица
В увода на тази книга, споменах за следния добър начин да се изчисли сумата на обхвата от номера:
console.log(sum(range(1, 10)));
Напишете функция range
, която използва два аргумента start
и end
и връща масив, който съдържа всички цифри от start
до end
(включително).
На следващо място, напишете функция sum
, която използва масива от числа и връща сумата от тези числа. Стартирайте предишната програма и вижте дали наистина връща 55.
Като бонус задача, модифицирайте обхвата на функция да взима по желание трети аргумент - “step”, който показва стойността на стъпката, която се използва за изграждане на масива. Ако не е дадена стъпка елементите на масива вървят на горе със стъпка едно, съответстващо на старото поведение. Извикването на функция range(1, 10, 2)
, трябва да върне [1, 3, 5, 7, 9]. Уверете се, че тя също работи и с отрицателни стойности на стъпката, така че range(5, 2, -1)
произвежда [5, 4, 3, 2].
// Your code here. console.log(range(1, 10)); // → [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] console.log(range(5, 2, -1)); // → [5, 4, 3, 2] console.log(sum(range(1, 10))); // → 55
Изграждането на масив най-лесно може да се направи, като първо се инициализира променлива с []
(празен масив) и многократно извиква своя push
метод за добавяне на стойност. Не забравяйте да върнете масива в края на функцията.
За край на границата включително, ще трябва да използвате оператора <=
, вместо <
при проверката за край на цикъла.
Трябва също да проверите дали е даден аргумент за стъпката, да проверите arguments.length
и да сравните стойността на аргумента с undefined
. Ако не е даден, просто го настройте към стойност по подразбиране (1) в горната част на функцията.
Когато знаете, че имате в range
отрицателни стойности на стъпката, най-добре е да напишете два отделни цикъла - един за броене на горе и един за броене на долу, защото сравнението, което проверява дали цикълът е завършен трябва да бъде по-скоро >=
, отколкото <=
, когато се бори на долу.
Може също така да бъде полезно да се използва различна стъпка по подразбиране, а именно -1, когато края на обхвата е по-малък, отколкото началото. По този начин range(5, 2)
връща нещо смислено, а не става един безкраен цикъл.
Обръщане на масив
Масивите имат метод reverse
, който променя масива, като обръща реда, в който се появяват неговите елементи. За това упражнение напишете две функции, reverseArray
и reverseArrayInPlace
. Първата, reverseArray
, взема масива, като аргумент и произвежда нов масив, който има същите елементи в обратен ред. Втората reverseArrayInPlace
, прави това, което метода reverse
прави: променя масива, даден като аргумент, за да обърне неговите елементи. Не може да се използва стандартния метод reverse
.
Мисля си за бележките за страничните ефекти и чистите функции в предишната глава, кой вариант очаквате да бъде по-полезен в повечето ситуации? Кой е по-ефективен?
// Your code here. console.log(reverseArray(["A", "B", "C"])); // → ["C", "B", "A"]; var arrayValue = [1, 2, 3, 4, 5]; reverseArrayInPlace(arrayValue); console.log(arrayValue); // → [5, 4, 3, 2, 1]
Има два очевидни начина за изпълнение на reverseArray
. Първият е просто да минем през входящия масив отпред назад и използваме unshift
метода върху новия масив, за да вмъкнем всеки елемент в неговото начало. Вторият е да минем с цикъл над входящия масив назад и използваме push
метода. Итерациите над масива назад изискват (донякъде неудобна) for
спецификация, като (var i =
array.length - 1; i >= 0; i--)
.
Обръщането на масива на място е по-трудно. Вие трябва да внимавате да не презапишете елементи, от които ще се нуждаете по-късно. Използването на reverseArray
или друго копиране на целия масив (като array.slice(0)
е един добър начин за копиране на масиви) работи, но е измама.
Номерът е да се смени първия с последния елемент, а след това втория с пред последния и т.н. Можете да направите това с цикъл над половината от дължината на масива (използвайте Math.floor
за да закръгляте надолу - не е нужно да се достига средата на елементите в масив с нечетна дължина) и смяна на елемент на позиция i
с един от позиция при положение на array.length - 1 - i
. Можете да използвате локална променлива да държи за кратко един от елементите, презаписвайки го с неговия огледален образ и след това пускане на стойността от локалната променлива в мястото, където огледалния образ да бъде използван.
Списък
Обектите, като широко приложение за стойности, могат да бъдат използвани за изграждане на всички видове структури от данни. Една обща структура от данни е списък (да не се бърка с масив). Списъкът е вложен набор от обекти, като първия обект се позовава на втория , втория - третия и т.н.
var list = { value: 1, rest: { value: 2, rest: { value: 3, rest: null } } };
Получените обекти образуват верига, подобна на тази:
Хубавото на списъците е, че те могат да споделят част от своята структура. Например, ако аз създам две нови стойности {value: 0, rest: list}
и {value: -1, rest: list}
(с list
позоваващ се на променлива, определена по-рано) и двата списъка са независими, но те споделят структура, която прави техните последни три елемента. В допълнение, първоначалният списък също е все още валиден списък с три елемента.
Напишете функция arrayToList
, която изгражда структура от данни, като предишната, на която се дава [1, 2, 3]
, като аргумент и напишете функция listToArray
, която създава масив от списък. Също напишете помощни функции - prepend
, която взема елемент и списък, и създава нов списък, като добавя елемент към предната част на входа списъка и nth
, която взема списък и номер, и връща елемент от дадена позиция в списъка или undefined
, когато няма такъв елемент.
Още не сте готови, напишете също и рекурсивна версия на nth
.
// Your code here. console.log(arrayToList([10, 20])); // → {value: 10, rest: {value: 20, rest: null}} console.log(listToArray(arrayToList([10, 20, 30]))); // → [10, 20, 30] console.log(prepend(10, prepend(20, null))); // → {value: 10, rest: {value: 20, rest: null}} console.log(nth(arrayToList([10, 20, 30]), 1)); // → 20
Изграждане на списък е най-добре отзад напред. Така arrayToList
може да обхожда масива назад (виж предишното упражнение) и за всеки елемент да добавя един обект в списъка. Можете да използвате локална променлива да държи част от списъка, който е направен досега, като използвате модела list = {value: X, rest: list}
за да добавя елемент.
За да обработите списъка (в listToArray
и nth
), може да използвате for
цикъл, като този:
for (var node = list; node; node = node.rest) {}
Можете ли да видите как става това? Във всяка итерация на цикъла, node
сочи към текущия под-списък и тялото му може да чете свойството value
, за да получите текущия елемент. В края на итерацията node
преминава към следващия под-списък. Когато това е null, то сме достигнали края на списъка и цикъла е завършен.
Рекурсивна версия на nth
по същия начин ще погледне все по-малката част от "опашката" на списъка и в същото време ще брои за определяне на индекса, докато достигне нула, при което може да върне свойството value
на node, където той да търси. За да получите нулевият елемент от списъка, можете просто да вземете свойството value
от неговия главен node. За да получите елемент N + 1 вземете N-тия елемент от списъка, който е в списъка на rest
свойството.
Дълбоко сравнение
Оператора ==
сравнява обекти за идентичност. Но понякога, бихме предпочели да сравним стойностите по действителните им свойства.
Напишете функция deepEqual
, кято взема две стойности и връща true, само ако те са една и съща стойност или обекти със същите свойства, чиито стойности също са равни, когато се сравнят с рекурсивното извикване на deepEqual
.
За да разберете дали се сравняват две неща по идентичност (използвайте оператора ===
за това) или за да видите техните свойства, използвайте оператора typeof
. Ако той произвежда "object" и за двете стойности, трябва да направите дълбоко сравнение. Но трябва да вземете едно глупаво изключение в предвид: от една историческа случайност, typeof null
също произвежда "object".
// Your code here. var obj = {here: {is: "an"}, object: 2}; console.log(deepEqual(obj, obj)); // → true console.log(deepEqual(obj, {here: 1, object: 2})); // → false console.log(deepEqual(obj, {here: {is: "an"}, object: 2})); // → true
Вашият тест за това дали се занимавате с реален обект ще изглежда нещо подобно на typeof x == "object" && x != null
. Бъдете внимателни при сравняване на свойства, когато и двата аргумента са обекти. Във всички други случаи може просто да върнете незабавно резултата от прилагането на ===
.
Използвайте for
/in
цикъл да минете над свойствата. Трябва да се провери дали двата обекта са с едни и същи набори от имена на свойства и дали тези свойства имат еднакви стойности. Първият тест може да се направи чрез преброяване на свойствата на двата обекта и връщане на false, ако номерата на свойствата са различни. Ако те са едни и същи, след това отидете на свойствата на единия обект и за всяко от тях потвърдете, че другия обект също има това свойство. Стойностите на свойствата се сравняват с рекурсивно извикване на deepEqual
.
Връщането на правилната стойност от функцията се прави най-добре, когато незабавно върне false, ако забележи разминаване и върне true в края на функцията.