Javascript object prototypes

Создание объектов

Объекты можно создавать с помощью литералов объектов, ключевого слова new и (в ECMAScript 5) функции Object.create().

Литералы объектов

Самый простой способ создать объект заключается во включении в программу литерала объекта. Литерал объекта — это заключенный в фигурные скобки список свойств (пар имя/значение), разделенных запятыми. Именем свойства может быть идентификатор или строковый литерал (допускается использовать пустую строку). Значением свойства может быть любое выражение, допустимое в JavaScript — значение выражения (это может быть простое значение или объект) станет значением свойства.

Ниже приводится несколько примеров создания объектов:

В ECMAScript 5 (и в некоторых реализациях ECMAScript 3) допускается использовать зарезервированные слова в качестве имен свойств без кавычек. Однако в целом имена свойств, совпадающие с зарезервированными словами, в ECMA-Script 3 должны заключаться в кавычки. В ECMAScript 5 последняя запятая, следующая за последним свойством в литерале объекта, игнорируется. В большинстве реализаций ECMAScript 3 завершающие запятые также игнорируются, но IE интерпретирует их наличие как ошибку.

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

Создание объектов с помощью оператора new

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

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

Object.create()

Стандарт ECMAScript 5 определяет метод Object.create(), который создает новый объект и использует свой первый аргумент в качестве прототипа этого объекта. Дополнительно Object.create() может принимать второй необязательный аргумент, описывающий свойства нового объекта.

Object.create() является статической функцией, а не методом, вызываемым относительно некоторого конкретного объекта. Чтобы вызвать эту функцию, достаточно передать ей желаемый объект-прототип:

Чтобы создать объект, не имеющий прототипа, можно передать значение null, но в этом случае вновь созданный объект не унаследует ни каких-либо свойств, ни базовых методов, таких как toString() (а это означает, что этот объект нельзя будет использовать в выражениях с оператором +):

Если в программе потребуется создать обычный пустой объект (который, например, возвращается литералом {} или выражением new Object()), передайте в первом аргументе Object.prototype:

Возможность создавать новые объекты с произвольными прототипами (скажем иначе: возможность создавать «наследников» от любых объектов) является мощным инструментом, действие которого можно имитировать в ECMAScript 3 с помощью функции, представленной в примере ниже:

Реализация функции inherit() приобретет больше смысла, как только мы познакомимся с конструкторами в следующей статье. А пока просто считайте, что она возвращает новый объект, наследующий свойства объекта в аргументе

Обратите внимание, что функция inherit() не является полноценной заменой для Object.create(): она не позволяет создавать объекты без прототипа и не принимает второй необязательный аргумент, как Object.create()

Полный пример наследования

Для окончательной организации удобного javascript-наследования на классе, пригодится функция копирования свойств из объекта в другой :

// копирует все свойства из src в dst,
// включая те, что в цепочке прототипов src до Object
function mixin(dst, src){
	// tobj - вспомогательный объект для фильтрации свойств,
	// которые есть у объекта Object и его прототипа
	var tobj = {}
	for(var x in src){
		// копируем в dst свойства src, кроме тех, которые унаследованы от Object
		if((typeof tobj == "undefined") || (tobj != src)){
			dst = src;
		}
	}
	// В IE пользовательский метод toString отсутствует в for..in
	if(document.all && !document.isOpera){
		var p = src.toString;
		if(typeof p == "function" && p != dst.toString && p != tobj.toString &&
		 p != "\nfunction toString() {\n    \n}\n"){
			dst.toString = src.toString;
		}
	}
}

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

// ---- родительский класс ----

function Animal(name, walkSpeed) {
	this.name = name
	this.walkSpeed = walkSpeed
}

// добавляем методы объекта
mixin(Animal.prototype, {

	// пример переменной
	distance: 0,

	// пример метода
	walk: function(time) {
		this.distance = this.distance + time*this.walkSpeed
	},

	toString: function() {
		return this.name+" на расстоянии "+this.distance
	}
})

// ---- класс наследник ----

function Bird(name, walkSpeed, flySpeed) {
	// вызов родительского конструктора
	Bird.superclass.constructor.call(this, name, walkSpeed)

	this.flySpeed = flySpeed
}
extend(Bird, Animal)

mixin(Bird.prototype, {
	fly: function(time) {
		this.distance = this.distance + time*this.flySpeed
	}
})

Пример создания объекта-наследника:

bird = new Bird("Птыц", 1, 10)

bird.walk(3)

alert(bird) // => Птыц на расстоянии 3

bird.fly(2)

alert(bird) // => Птыц на расстоянии 23

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

При наследовании можно организовать «настоящие» приватные члены класса. Для этого, однако, придется объявлять все методы не отдельно от конструктора, а внутри него:

function extend(Child, Parent) {
	var F = function() { }
	F.prototype = Parent.prototype
	Child.prototype = new F()
	Child.prototype.constructor = Child
	Child.superclass = Parent.prototype
}

// ---- родительский класс ----

function Animal(name, walkSpeed) {

	// объявить приватную переменную
	var speed = walkSpeed

	// объявить открытую переменную
	this.distance = 0

	// добавить метод, использующий private speed
	this.walk = function(time) {
		this.distance = this.distance + time*speed
	}

	// добавить метод, использующий private name
	this.toString = function() {
		return name+" на расстоянии "+this.distance
	}
}


// ---- класс наследник ----

function Bird(name, walkSpeed, flySpeed) {
	// вызов родительского конструктора
	Bird.superclass.constructor.call(this, name, walkSpeed)

	this.fly = function(time) {
		this.distance = this.distance + time*flySpeed
	}
}
extend(Bird, Animal)


bird = new Bird("Птыц", 1, 10)

bird.walk(3)

alert(bird) // => Птыц на расстоянии 3

bird.fly(2)

alert(bird) // => Птыц на расстоянии 23

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

Это свойства, явно объявленные через var, плюс аргументы конструктора.

При таком способе объявления — все свойства и методы записываются не в прототип объекта, а в сам объект.

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

Обычно же эти расходы можно во внимание не принимать

Если Вы использовали ООП в других языках программирования, то наверняка знаете, что чаще делаются не private свойства, а protected, т.е такие, к которым могут получить доступ наследники. Javascript не предоставляет синтаксиса для создания protected свойств, поэтому их просто помечают подчеркиванием в начале.

Например,

function Animal(name) {

	var privateVariable = 0

	this._protectedName = name

	this._protectedMethod = function(..) {
		... alert(privateVariable)..
	}

	this.publicMethod = function() { ... }
}

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

Ограничение доступа к таким «защищенным» свойствам не жесткое и остается на совести программиста.

Пример использования

let someObject = {
 getName: function(){
    // возвращает значение свойства firstName текущего объекта 	
    return this.firstName
  }
}

let someObject2 = {
 getName: function(){
    // возвращает значение свойства firstName текущего объекта и конкатенирует строковое значение	
    return this.firstName + " на крюку повис"
  }
}

// создаем новый объект
let myObj = Object.create( someObject, {   // указываем, что объект прототип соответствует объекту someObject
              firstName: {          // добавляем новое свойство
                value: "Boris"      // указываем значение свойства
              }
            });

console.log( myObj.getName() );     // "Boris"
// воз­вра­ща­ем про­то­тип переданного объ­ек­та и сравниваем его с объектом someObject
console.log(  Object.getPrototypeOf( myObj ) === someObject );    // true

Object.setPrototypeOf( myObj, someObject2 ) // изменяем прототип указанному объекту
console.log( myObj.getName() );     // "Boris на крюку повис"
// воз­вра­ща­ем про­то­тип переданного объ­ек­та и сравниваем его с объектом someObject
console.log( Object.getPrototypeOf( myObj ) === someObject2 );    // true

В этом примере мы иницилизировали две переменные, содержащие объекты, каждый из них содержит одноименный метод возвращающий значение свойства firstName текущего объекта в первом случае и значение свойства firstName и некоторое текстовое содержимое во втором.

Далее с использованием метода create() мы создали новый объект с указанным объектом прототипом someObject и одним собственными (неунаследованным) свойством (ключ (атрибут) value устанавливает значение для свойства).

После этого мы с помощью унаследованного свойства (метода getName) выводим в консоль значение свойства firstName. Затем с помощью метода getPrototypeOf() мы воз­вра­ща­ем про­то­тип переданного объ­ек­та и проверяем соответствует ли он объекту someObject. Результат проверки будет положительный.

Далее мы с помощью метода setPrototypeOf() изменяем прототип нашему объекту myobj и вызываем унаследованный метод getName.

И наконец с помощью метода getPrototypeOf() мы воз­вра­ща­ем про­то­тип переданного объ­ек­та и проверяем соответствует ли он объекту someObject2. Возвращаемое значение при этом будет соответствовать true.

JavaScript Object

Методы create, getPrototypeOf и setPrototypeOf

На сегодняшний
день свойство __proto__ считается устаревшим и формально
поддерживается только в браузерной среде. Хотя, почти все остальные среды по-прежнему
позволяют им пользоваться. Ему на смену пришли новые методы объекта Object:

  • Object.create(proto,
    ) – используется для создания нового объекта с указанием базового
    (proto) и
    необязательным набором дополнительных дескрипторов свойств – descriptors;

  • Object.getPrototypeOf(obj)
    – возвращает ссылку на базовый объект, либо null, если его нет;

  • Object.setPrototypeOf(obj,
    proto) – назначает базовый объект proto для уже
    существующего объекта obj.

Например,
используя объекты из предыдущего занятия:

let prop = {
         sp {x , y },
         ep {x 100, y 20},
         get coords() {
                   return this.sp.x, this.sp.y, this.ep.x, this.ep.y;
         },
         set coords(coords) {
                   this.sp.x = coords; this.sp.y = coords1;
                   this.ep.x = coords2; this.ep.y = coords3;
         }
};
 
function Rect() {
         this.name = "прямоугольник";
 
         this.draw = function() {
                   console.log("Рисование фигуры: "+this.name);
         }
 
         this.__proto__ = prop;
}

Перепишем
объявление объекта Rect с использованием метода create:

let rect = Object.create(prop, {
         name {value "прямоугольник", writable true},
         draw {value function() {
                            console.log("Рисование фигуры: "+this.name);
                   }
         },
});
 
console.log( rect.coords );
rect.draw();

Чтобы получить
ссылку на базовый класс, можно воспользоваться методом getPrototypeOf:

console.log( Object.getPrototypeOf(rect) === prop );

Наконец, для
замены базового объекта на другой, выполним метод setPrototypeOf:

Object.setPrototypeOf( rect, {} );

Мы здесь указали
пустой объект вместо прежнего prop и теперь свойство coords возвращает
значение undefined.

Создание пользовательских конструкторов, ключевое слово this

Конструктор — это функция, которая создает пустой объект и определяет его свойства и методы. Т.е. конструктор наполняет пустой объект данными. Конструкторы — эквивалент типов данных (классов) в ООП языках программирования (C++, C#, Java).

В других языках программирования можно создавать свои типы данных (классы), однако в JS такой возможности нет, можно оперировать лишь теми типами данных, которые заложены в язык: примитивами (Строка, Число, Булев тип), тривиальными типами null и undefined, сложными типами (Объект, Массив) и специальным типом Функция.

Ключевое слово this — это то, что отличает конструктор от обычной функции. Конструктор использует в своем теле ключевое слово this, чтобы в объект добавить какие-то свойства. Ключевое слово this в момент запуска функции всегда ссылается на пустой объект, который был создан с помощью оператора new.

Как мы отличаем, что объекты относятся к какому-то типу данных (классу)? Если 2 объекта имеют одинаковый набор свойств и методов, значит эти объекты относятся к одному типу (классу).

Функция-конструктор для создания объектов Point

function Point(x, y) { // Например, Point в других ЯП был бы отдельным типом данных Point (наравне с Object, Array etc.)
	this.x = x; // Значение x будет записано в свойство x объекта this
	this.y = y;
}

Создание 3х экземпляров класса Point
В большинстве ЯП мы бы сказали, что эти переменные типа Point. Но в случае JS эти переменные типа Object, в которых есть по 2 свойства. Поскольку набор свойств этих объектов одинаков, можно условно говорить о том, что они принадлежат к одному типу (классу).

var leftTop = new Point(, );
var topPoint = new Point(15, 30);
var rightBottom = new Point(30, 30);

Свойства и методы экзмепляра и свойства и методы конструктора

function Point(x, y) {
	
	// Свойства экземпляра
	this.x = x;
	this.y = y;
	
	// Метод экземпляра
	this.print = function () {
		console.log( this.x + ', ' + this.y );
	}
	
	// Свойства и методы экзмепляра создаются для каждого объекта, созданного с помощью конструктора, и принадлежат каждому конкретному объекту. Для этого требуется больше производительности и места в памяти.
}

Свойство функции-конструктора (аналог статического свойства в других ЯП)

Метод функции-конструктора (аналог статического метода в других ЯП)

Point.getOrigin = function () {
	return new Point(, ) // Использовать this недопустимо
}

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

Создание экземпляров и работы с их свойствами и методами

var p1 = new Point(100, 200);
p1.x = 300;
p1.y = 400;
p1.print(); // '300, 400'

var p2 = new Point(100 ,200);
p2.print(); // '100, 200'

Работа со свойствами и методами конструктора

Point.maxPointCount = 10;
Point.getOrigin().print(); // '0, 0'

Цикл «for…in»

Для перебора всех свойств объекта используется цикл . Этот цикл отличается от изученного ранее цикла .

Синтаксис:

К примеру, давайте выведем все свойства объекта :

Обратите внимание, что все конструкции «for» позволяют нам объявлять переменную внутри цикла, как, например, здесь. Кроме того, мы могли бы использовать другое имя переменной

Например, часто используется вариант

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

Упорядочены ли свойства объекта? Другими словами, если мы будем в цикле перебирать все свойства объекта, получим ли мы их в том же порядке, в котором мы их добавляли? Можем ли мы на это рассчитывать?

Короткий ответ: свойства упорядочены особым образом: свойства с целочисленными ключами сортируются по возрастанию, остальные располагаются в порядке создания. Разберёмся подробнее.

В качестве примера рассмотрим объект с телефонными кодами:

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

Но если мы запустим код, мы увидим совершенно другую картину:

  • США (1) идёт первым
  • затем Швейцария (41) и так далее.

Телефонные коды идут в порядке возрастания, потому что они являются целыми числами: .

Целочисленные свойства? Это что?

Термин «целочисленное свойство» означает строку, которая может быть преобразована в целое число и обратно без изменений.

То есть, – это целочисленное имя свойства, потому что если его преобразовать в целое число, а затем обратно в строку, то оно не изменится. А вот свойства или таковыми не являются:

…С другой стороны, если ключи не целочисленные, то они перебираются в порядке создания, например:

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

Пример:

Теперь код работает так, как мы задумывали.

«Простейший» объект

Как мы знаем, объекты можно использовать как ассоциативные массивы для хранения пар ключ/значение.

…Но если мы попробуем хранить созданные пользователями ключи (например, словари с пользовательским вводом), мы можем заметить интересный сбой: все ключи работают как ожидается, за исключением .

Посмотрите на пример:

Если пользователь введёт , присвоение проигнорируется!

И это не должно удивлять нас. Свойство особенное: оно должно быть либо объектом, либо , а строка не может стать прототипом.

Но мы не намеревались реализовывать такое поведение, не так ли? Мы хотим хранить пары ключ/значение, и ключ с именем не был сохранён надлежащим образом. Так что это ошибка!

Конкретно в этом примере последствия не так ужасны, но если мы присваиваем объектные значения, то прототип и в самом деле может быть изменён. В результате дальнейшее выполнение пойдёт совершенно непредсказуемым образом.

Что хуже всего – разработчики не задумываются о такой возможности совсем. Это делает такие ошибки сложным для отлавливания или даже превращает их в уязвимости, особенно когда JavaScript используется на сервере.

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

Как же избежать проблемы?

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

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

Свойство – не обычное, а аксессор, заданный в :

Так что при чтении или установке вызывается соответствующий геттер/сеттер из прототипа , и именно он устанавливает/получает свойство .

Как было сказано в начале этой секции учебника, – это способ доступа к свойству , это не само свойство .

Теперь, если мы хотим использовать объект как ассоциативный массив, мы можем сделать это с помощью небольшого трюка:

создаёт пустой объект без прототипа ( будет ):

Таким образом не будет унаследованного геттера/сеттера для . Теперь это свойство обрабатывается как обычное свойство, и приведённый выше пример работает правильно.

Мы можем назвать такой объект «простейшим» или «чистым словарным объектом», потому что он ещё проще, чем обычные объекты .

Недостаток в том, что у таких объектов не будет встроенных методов объекта, таких как :

…Но обычно это нормально для ассоциативных массивов.

Обратите внимание, что большая часть методов, связанных с объектами, имеют вид. К примеру,

Подобные методы не находятся в прототипе, так что они продолжат работать для таких объектов:

Методы чтения и записи свойств

Выше уже говорилось, что свойство объекта имеет имя, значение и набор атрибутов. В ECMAScript 5 значение может замещаться одним или двумя методами, известными как методы чтения (getter) и записи (setter). Свойства, для которых определяются методы чтения и записи, иногда называют свойствами с методами доступа, чтобы отличать их от свойств с данными, представляющих простое значение.

Когда программа пытается получить значение свойства с методами доступа, интерпретатор вызывает метод чтения (без аргументов). Возвращаемое этим методом значение становится значением выражения обращения к свойству. Когда программа пытается записать значение в свойство, интерпретатор вызывает метод записи, передавая ему значение, находящее справа от оператора присваивания. Этот метод отвечает за «установку» значения свойства. Значение, возвращаемое методом записи, игнорируется.

В отличие от свойств с данными, свойства с методами доступа не имеют атрибута writable. Если свойство имеет оба метода, чтения и записи, оно доступно для чтения/записи. Если свойство имеет только метод чтения, оно доступно только для чтения. А если свойство имеет только метод записи, оно доступно только для записи (такое невозможно для свойств с данными) и попытки прочитать значение такого свойства всегда будут возвращать undefined.

Самый простой способ определить свойство с методами доступа заключается в использовании расширенного синтаксиса определения литералов объектов:

Свойства с методами доступа определяются как одна или две функции, имена которых совпадают с именем свойства и с заменой ключевого слова function на get и/или set.

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

Для представления координат X и Y в нем имеются обычные свойства с данными, а также свойства с методами доступа, позволяющие получить эквивалентные полярные координаты точки:

Для примера рассмотрим следующий объект, представляющий декартовы координаты точки на плоскости. Для представления координат X и Y в нем имеются обычные свойства с данными, а также свойства с методами доступа, позволяющие получить эквивалентные полярные координаты точки:

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

в теле функции this будет ссылаться на объект точки. Благодаря этому метод чтения свойства r может ссылаться на свойства x и y, как this.x и this.y.

Свойства с методами доступа наследуются так же, как обычные свойства с данными, поэтому объект p, определенный выше, можно использовать как прототип для других объектов точек. В новых объектах можно определять собственные свойства x и y, и они будут наследовать свойства r и theta.

Как организовано наследование стандартных объектов

В JavaScript всё (функции, массивы, регулярные выражения и т.д.) является объектами за исключением шести примитивных типов данных (string, number, boolean, null, undefined, symbol). Все объекты в JavaScript имеют в самой верхней точке прототипной цепи наследования . Объект не имеет прототипа.

// попробуем получить прототип объекта Object.prototype с помощью метода getPrototypeOf
Object.getPrototypeOf(Object.prototype); // null
// попробуем получить прототип объекта Object.prototype с помощью свойства __proto__
Object.prototype.__proto__; // null

имеет в качестве конструктора функцию .

// получим конструктор объекта Object.prototype
Object.prototype.constructor; // ƒ Object() {  }

Конструктор (функция-конструктор) – это функция, посредством которой в JavaScript можно создавать объекты. Функция-конструктор вызывается с использованием ключевого слова . Чтобы данную функцию можно было отличить от обычной функции, ей назначают имя, начинающееся с большой буквы.

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

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

Теперь рассмотрим, как в JavaScript организован некоторый тип данных (стандартный объект), например дата.

Создание даты (объект типа Date) осуществляется с помощью функции-конструктора .

// присвоенние переменной nowDate текущей даты (ссылки на объект типа Date)
var nowDate = new Date();

Дата, хранящаяся в переменной , имеет в качестве прототипа объект . А объект имеет в качестве прототипа .

В результате созданный объект типа , ссылка на который присвоена переменной , имеет доступ к методам , , и др. Всех этих методов нет в текущем объекте, но они доступны в нём из-за наследования.

Методы и находятся в прототипе , а метод – в .

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Adblock
detector