Пропустить навигацию

Лекция 6. Объекты, прототипы, наследование

JavaScript – это язык программирования, в котором почти все – объекты. Кроме чисел, строк, переменных true и false, и значений null и undefined. Все остальное это объекты. Объект – это замкнутый контейнер со свойствами. И это совсем не то, что принято называть объектом в классических языках программирования. Объекты в JavaScript скорей как словари, где есть название свойства и значение свойства. Функция это объект, массив это объект, объект это объект.

Вот так выглядит объект.

var obj = {};

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

Следующий объект на самом деле содержит какие то свойства.

 var student = {
  “name” : “Dima”,
  “age” : 20
};

Он содержит здесь два свойства: «name» - имя и значение, и возраст и значение. В одном свойстве мы используем в качестве значения строку, во втором – в качестве значения число. Все это выражение объекта, и должно заканчиваться точкой с запятой. Обратиться к этим свойствам очень легко. Метод, который лучше всего использовать – обращение через точку.

student.name; // “Dima”
student.age;    // 20

Мы также можем обратиться через квадратные скобки.

var student = {
  name : “Dima”,
  age : 20,
  “” : “Something!”
};
student[“name”]; // “Dima”
student[“age”];    // 20
student[“”];          // “Something!”

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

Мы можем задавать имя идентификатора через кавычки и без них. Единственное, если имя свойства содержит какие-либо символы, которые не могут быть в языке просто так, например дефис, то кавычки нужны. Например, без кавычек в названии с дефисом будет происходить какое-то вычитание. Если вы хотите использовать в названии свойства символы – придется использовать кавычки, неважно, одинарные или двойные. Я бы посоветовал использовать строку без пробелов, без каких-то символов, это в целом будет удобней.

Объект может в качестве свойства содержать строку, число, и ничто не мешает ему содержать в качестве свойства другой объект. Иными словами, значение свойства объекта может быть что угодно в языке Javascript, в том числе объект. Это очень удобно, т.к. с помощью объектов мы можем создавать очень гибкие структуры. Можно строить разные деревья, просто вкладывать объекты так, как нам нужно под какую то задачу. Обращаться к таким вложенным данным через точку. Если обратиться к свойству, которого нет, то будет андефайнед.

Если нам нужно изменить какое то значение в объекте, то всего лишь нужно присвоить ему какое либо новое значение.

var student = {
    name : “Dima”,
    age : 20
};
student.height;    // undefined

student.name = “Peter”;
student.height = 176;
 
student.name;      // “Peter”
student.height;    // 176


Имя Peter уже обновленное, вместо Dima, и можно получить новое свойство, которое мы присвоили данному объекту. Вы можете обновить значение, или добавить значение если его там нет.

Переходим к одной из основных частей лекции – прототипы. Javascript основан на прототипах, Создан он был с идеей прототипов. Вы видели, как легко создавать объекты. Мы просто создаем нужный нам объект и изменяем его как хотим. Если нам нужно, чтобы какой-то другой объект наследовал свойство чего-то иного, то нам нужно наследовать от объекта. В Javascript мы создаем объект, который наследует свойства от другого объекта.

Вот здесь у нас есть объект Student, у него есть три свойства, type, legs и head. И мы создаем новый объект Megastudent, и говорим что прототипом этого объекта является student. В итоге мы получаем такую ситуацию, что мы можем получить значения свойств прототипа. Несмотря на то, что в Megastudent свойства type нет, при запросе данного свойства мы получаем свойство прототипа. Если мы изменим какое либо свойство Megastudent’а, то оно обновится, но значение данного свойства у прототипа не изменится. И если мы зададим Megastudent’у какое то новое свойство, то оно не появится у прототипа.

Нужно лишь понять как эти прототипы действуют. Это будет понятно после еще одного примера. Если мы в исходном объекте определим какое-то новое свойство, то оно автоматически, моментально, прям в процессе выполнения кода будет присвоено всем объектам, прототипом которых является данный объект. Вот мы добавляем, проверяем.

Student.face;      // undefined
Megastudent.face;  // undefined
Student.face = “okay”;
Student.face;      // “okay”
Megastudent.face;  // “okay”
Megastudent.face = “awesome”;
Megastudent.face; // “awesome”
Student.face;      // “okay”


Создаем свойство фэйс у student’a, оно появляется и у Megastudent’а. Задаем значение свойства фэйс student’у, оно переходит к Megastudent’у от прототипа. Однако если мы изменим свойство фэйс в Megastudent’е, то в нем оно естественно изменится, в прототипе student останется такое, какое было.

Как это работает: мы обращаемся к свойству, и если оно есть в объекте, в самом объекте, то оно нам возвращается, и всё. Если же его в объекте нет, то JavaScript смотрит в прототип, если в прототипе его нет, то JavaScript смотрит в прототип, и так оно делает до тех пор, пока не дойдет до того объекта, в котором это свойство есть, если оно есть. Эти цепочки не бесконечны, они заканчиваются. В данном примере нам повезло, мы дошли до протипа в котором данное свойство есть, поэтому в данном запросе, мы получим значение 1. И получим его от прапрапрототипа. Если же во всей цепочке нет объекта или прототипа с таким свойством, прототипа нет, прототипа нет, и у последнего прототипа нет, а последний прототип – это системный объект Object.prototype. Это такой же объект, просто обычно он идет в комплекте с JavaScript. Если в нем, этом последнем шаге значения нет, то мы получим undefined. Мы получим undefined на любом из этих уровней. Потому что всегда логика одна и та  же. Если его нет нигде, то мы получаем undefined.

Возвратимся к примеру. Мы создавали свойство face без значения. Оно не было найдено в цепочке прототипов. Теперь присвоим прототипу значение свойства «ОК». После этого оно будет и у объекта Megastudent. Потом мы присваиваем свойство самому Megastudent‘у, и при запросе к нему мы никуда не будем обращаться, т.к. значение свойства есть в самом объекте, и мы его и получаем. У student.face ничего не изменилось. Теперь нам не нужно думать о том, что в JavaScript есть какие то правила, просто представляйте эту цепочку, мы идем к прототипам, прототипам, и ищем свойство везде, как только находим, то возвращаем, если не находим, то undefined.

Как указать прототип?

var Student = {
     type : “human”,
     head : 1,
     legs : 2
};
var Megastudent = Object.create(Student);

Мы создаем объект Megastudent следующим образом. Мы говорим: «Object», системный объект, создай мне объект с прототипом student, указываем объект, который хотим использовать как прототип. И всё – теперь student – это прототип Megastudent, и все предыдущие утверждения будут работать. Нужно использовать вот такой синтаксис, это правильный синтаксис, и он работает.

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

var Student  = {
     type : “human”,
     head : 1,
     legs : 2
};
var Megastudent = Object.create(Student);
Megastudent.head = 2;
Megastudent.head;      // 2
delete Megastudent.head;
Megastudent.head;      // 1


Мы сделали тоже самое, Megastudent унаследовали от student, свойству Megastudent.head присвоили значение 2, несмотря на то, что у student значение head 1, у Megastudent’а значение head будет равно 2. Если же мы удалим у этого объекта свойство head, то у него все равно будет возвращаться значение свойства head, но уже от прототипа, потому что в самом объекте этого свойства нет, мы вернемся к прототипу, в нем это значение 1, поэтому после удаления мы получим 1. Если же мы удалим свойство head из объекта student, т.е. удалим значение в прототипе, то мы будем получать здесь не 1, естественно, а undefined.

This – объяснение.

Есть переменная this, рассмотрим её назначение на небольшом примере.

function getIt() { return this.x; }

Создается простая функция, и в теле будет возвращаться this.х. здесь мы получаем свойство икс объекта this. Теперь создадим объект:

var obj1 = { get : getIt, x : 1 }

Нам не нужны скобки, потому что мы не вызываем эту функцию, она будет просто значением свойства get. Второе свойство – это икс, и значение у него будет 1. Создадим второй такой объект

var obj2 = { get : getIt, x : 2 }

При вызовах получается следующее:

obj1.x  // 1
obj2.x  // 2
obj1.get()  // 1
obj2.get()  // 2

Рассмотрим этот же случай более подробно. Два объекта имеют одинаковое название свойства get, и оба они указывают на одну и ту же функцию, однако эта функция использует ключевое слово this. И this – это неточное значение, мы не можем сказать, на что оно указывает, пока мы не узнаем откуда она вызывается.

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

var obj = {
     base : 8,
     average : function (x, y) {
         return (this.base+x+y)/3;
     };
};

Мы делаем это указание явным, когда нет уже иного выхода.

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

Если вы привыкнете к такому поведению,  то поймете механизм наследования в JavaScript. Все это работает по принципу прототипов, по вот этой цепочке.

Наследование

JavaScript довольно неоднозначный язык программирования, он не является классическим языком, в нем нет классов. Однако это объектно-ориентированный язык, в нем есть объекты, в нем есть наследование, в нем есть свойства этих объектов.

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

Функция еще может быть и конструктором. Это позволяет функции создавать новые объекты. Это та функция, которая производит объекты. И нас интересует создание через функции объектов, так, чтобы они наследовали что-то.

Что же происходит, когда мы запускаем эту строчку.

var alex = new Human();

Human() – это функция. Если мы ее просто вызовем, то, возможно, она даже сделает что-то полезное. Но если мы ее вызовем со словом new, то этот вызов превратится в вызов конструктора, и вызов конструктора будет несколько отличаться от вызова этой функции самой по себе. Когда мы вызываем функцию Human с префиксом new, то происходит следующее – внутри функции создается новый объект, и в нем ничего нет. Далее эта функция смотрит на свой прототип. Грубо говоря, она вспоминает – окей, мой прототип это Human.prototype. Это какой-то другой объект. И любому объекту мы можем задать прототип. Мы можем сказать, теперь у тебя, объект, будет вот этот конкретный прототип. И это именно то, что делает конструктор следующим шагом. Только что созданный объект, ему задается прототип, такой же, как и у самой функции, в данном случае Human. Иными словами, конструктор создает новый объект и говорит, что «этот объект теперь мой брат. Теперь у нас один и тот же прототип».

После этого конструктор возвращает новый объект, и в итоге получается, что alex указывает на только что созданный объект, прототипом которого является прототип функции Human. Или прототип объекта, который является функцией Human. Мы наследуем не от объекта Human, а от прототипа объекта Human. Но это неочевидно. Нужно постоянно думать, ну или просто один раз запомнить, привыкнуть к тому, как ведет себя функция, когда ее вызывают как конструктор. Когда эту функцию вызывают с помощью слова new. Эта функция будет возвращать новый объект с прототипом, таким же, как у себя и будет работать, если сама по себе эта функция Human ничего не возвращает. Однако в ней могут быть строчки «return что-то», или же она может возвращать данные, не являющиеся объектом. Она может возвращать число, или строчку, или дату. В этом случае, если мы вызовем эту функцию с приставкой new, т.е вызовем ее как конструктор, то то, что она возвращает (не объект), будет просто проигнорировано. Эта строчка, возврат необъекта, возврат чего то, не будет работать вообще при этом вызове. 

function Human() {
          return 1;
}

Если же мы эту функцию мы вызовем как обычно, без new, а просто Human, то то, что возвращается, будет возвращено. Это будет обычным вызовом обычной функции. Если же эта функция возвращает объект, как в данном случае:

function Human() {
          return { a:1 };
}


 То в случае использвания конструктора будет возвращён объект ( {a:1} ), и у этого объекта не будет прототипом объект Human, т.е. связь, которая была на предыдущем рисунке, не будет работать. Это все, что нузно знать, когда вы работаете с оператором new.

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

function Human(name) {
     this.name = name;
}
Human.prototype.say = function(what) {
      alert(this.name + “ : “ + what);
}
var alex = new Human(“Alex”);
alex // { name : “Alex”}
alex.say(“hello!”);      // Alex : hello!
Human(“Galex”);
name                     // “Galex”


Она будет возвращать пустой объект, и прототипом этого объекта будет прототип Human. (хотя его еще и нет). Я же хочу, чтобы новосозданный человек-объект обладал именем, которое я ему передам. Тут очень удобно то, что когда я вызываю конструктор с ключевым словом new, то в этом вызове можно использовать this. this, как вы помните, это контекст вызова, и если мы вызываем функцию из какого то объекта, то this будет указывать на этот объект. Если же мы вызываем функцию как конструктор, то this будет указывать на новосозданный объект. т.е. this будет указывать на тот объект который мы только что создали. Получается, если я создал новый объект, то я могу задать ему какое то свойство. Например this.name = name. И то значение свойства, которое мы передаем в конструктор, будет записано в свойство name новосозданного объекта, который этот конструктор впоследствии и вернет. Теперь у нас есть функция Human, и мы будем использовать ее как конструктор. Чтобы показать связь с прототипом, я создам в прототипе функции Human (в прототипе объекта Human) новую функцию. Я сделаю это следующим образом.

Human.prototype.say = function(what) {
  alert(this.name + “ : “ + what);
}

Эта функция будет якобы имитировать речь этого человека. т.е. передавать слово. Я буду что-то передавать в эту функцию say(), а Human будет выводить это на экран с помощью console.log. Имя я могу указать с помощью this. Здесь происходит иное, функция say() будет функцией объекта. Мы ее будем вызывать у объекта, который создадим с помощью конструктора Human. По тому, как мы  будем вызывать функцию say() у этого объекта, мы также сможем обращаться к свойствам этого объекта с помощью this. Поэтому эта строчка здесь сработает (this.name). Дальше мы соединяем строки с помощью комбинации « + “ : “ » и тот текст который мы будем передавать в эту функцию (what). Это все что нам нужно от этой функции. У нас есть конструктор и у нас есть прототип этой функции.

Теперь сделаем так:

var alex = new Human(“Alex”);

Сейчас, если мы заглянем в объект alex, то мы увидим, что в нем хранится свойство name со значением Alex. Теперь мы можем вызвать у этого объекта функцию. Мы знаем, что в самом объекте этой функции нет, иначе она бы отображалась. Но мы знаем что этот объект унаследован от того же прототипа, который является прототипом Human. а у прототипа Human есть свойство say. поэтому я могу сделать так:

alex.say(“hello!”);  // Alex : hello!

Как видите, вызывается эта функция. Human.prototype.say и здесь this указывает на то, откуда мы вызвали этот say. мы вызвали его от alex’a. Поэтому this.name у alex’a это Alex. И у нас получилось Alex : hello!. Вот собственно как это работает. Тут есть одна проблема. Да, мы сделали эту функцию как конструктор, мы ее изначально задумывали как функцию, которую мы будем вызываться помощью new. Но нам ничего не мешает вызвать эту функцию без new.

Попробуем вызвать функцию без new

Human(“Galex”);

Если мы вызовем эту функцию, то this не будет указывать на новосозданный объект, потому что нет новосозданного объекта. Мы вызываем функцию без new, и новый объект не создается. Эта функция просто исполняется, строчка за строчкой, и единственной строчкой здесь заявляется this.name = name. Я запустил эту функцию, и вот что произошло: вызвалась строчка this.name = “Galex”;.  Т.к. мы в момент вызова находимся не в каком-то объекте, а в глобальном объекте, в самом наружнем объекте Javascript. В этом глобальном объекте появилось новое свойство name. И мы можем его посмотреть, просто вызвав его вот так: name. Грубо говоря, получилась глобальная переменная. И это нежелательно допускать в вашем коде. Большой минус глобального объекта – это может привести к ошибкам в реальности, т.к. в браузере, кроме вашего кода, может быть запущено еще много другого кода.

Например на странице запущены разные счетчики посещений, таймеры и проч. Мы не знаем какие глобальные переменные создаются с помощью этих других кодов. Потому что мы их не писали, и чаще всего мы не смотрим, что там происходит. Избегайте создания глобальных переменных, лучше создайте какой-то глобальный объект, и все что вам нужно, чтобы было доступно вашей программе везде, можно хранить в этом глобальном объекте. Но тут мы создали довольно банальное поле для глобального объекта с именем name. И этого можно избежать, если просто запомнить, что Human это конструктор, его нужно вызывать только с new, и тогда таких проблем не будет.

Или можно сделать небольшой трюк. Можно переписать функцию следующим образом.

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

function Human(name) {
  if (! (this instanceof Human)){return new Human(name); }
  this.name = name;
}

Instanceof

Оператор instanceof используется для проверки, принадлежит ли объект данному типу.

Синтаксис

var isInstance = object instanceof ObjectType

Аргументы

object – Объект

ObjectType – Конструктор(тип) для сравнения

В левой части оператора instanceof указывается проверяемый объект, а с правой - функция-конструктор для проверки. Оператор instanceof учитывает наследование.

instanceof – команда, которая проверяет, является ли левый аргумент, т.е. this, есть ли в его цепочке прототипов правый аргумент. Иными словами, наследует ли this что-то от Human. И если не наследует, то можно с уверенностью сказать, что это был простой вызов (глобальный неправильный вызов). Потому что если это будет нормальный вызов конструктора с ключевым словом new, то this будет указывать на только что созданный объект, а он имеет в своей цепочке прототипов объект Human. instanceof проверит, есть ли в цепочке прототипов левого аргумента, прототип правого аргумента. И блоком if мы как раз проверяем, вызвали ли мы функцию без ключевого слова new. И вместо того чтобы сделать this.name = name, мы сделаем правильный вызов, т.е. return. В ином случае делаем то, что обычно и делали в конструкторе. Теперь эта функция «умная».

Если теперь сделать тоже самое, т.е. вызвать

Human(“Galex”);

то вместо того, чтобы в глобальном объекте создать свойство name, и записать туда Galex. Мы получим объект. Вызовется строчка из условия.

Наследование

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

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

var parent = { name : “Peter” };   // parent {name : “Peter”}
var child = Object.create(parent); // child {}
child.name                        // “Peter”

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

Можно вообще забыть конструкторы, можно вручную создавать какие-то ваши объекты, вручную прописывать им прототипы. В результате вы получите то же самое ­– вы получите наследование одних свойств одних объектов в другие объекты. Все эти ухищрения - это лишь один подход к той же простой концепции, что нет классов, есть только объекты, и объекты имеют прототипы.

Cookies

Значения переменных в JS уничтожаются при закрытии браузера или обновлении страницы. Возможность сохранить данные для использования их даже после прекращения работы сценария реализуется с помощью технологии Cookie.

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

Cookie сохраняют маленькие фрагменты данных под уникальными именами. У куки есть срок хранения, после его достижения куки уничтожаются. Куки сохраняются на компьютере пользователя в длинной строки текста, связанной с сайтом или доменом. Друг от друга они отделяются точкой с запятой (;). Именно этот разделитель даёт возможность найти в списке конкретный куки.

Влияние на безопасность:

Куки – это фрагменты текстовой информации, сохраняемой на компьютер клиента.

Хотя куки хранятся на жестком диске, они не имеют доступа к остальной хранящейся там информации.

Не являясь исполняемыми программами, куки не могут стать источниками вирусов или червей.

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

В некоторых браузерах куки могут быть запрещены. Для решения данной проблемы можно проверить доступность куки с помощью метода navigator.cookieEnabled. Он возвращает true в случае доступности куки и false – если к ним нет доступа.