Почему необходимо установить конструктор прототипа?


294

В разделе о наследовании в статье MDN Введение в объектно-ориентированный Javascript я заметил, что они установили prototype.constructor:

// correct the constructor pointer because it points to Person
Student.prototype.constructor = Student;  

Служит ли это какой-либо важной цели? Можно ли это пропустить?


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

6
Я только должен был указать на это, этот вопрос теперь связан в статье, которую вы связали!
Мари

7
ничего не надо
nothingisnecessary

1
subclass.prototype.constructorБудет указывать , parent_classесли не писать subclass.prototype.constructor = subclass; То есть использование subclass.prototype.constructor()напрямую даст неожиданный результат.
KuanYu Чу

Ответы:


263

Это не всегда необходимо, но у него есть свое применение. Предположим, мы хотели создать метод копирования для базового Personкласса. Как это:

// define the Person Class  
function Person(name) {
    this.name = name;
}  

Person.prototype.copy = function() {  
    // return new Person(this.name); // just as bad
    return new this.constructor(this.name);
};  

// define the Student class  
function Student(name) {  
    Person.call(this, name);
}  

// inherit Person  
Student.prototype = Object.create(Person.prototype);

Что происходит, когда мы создаем новое Studentи копируем его?

var student1 = new Student("trinth");  
console.log(student1.copy() instanceof Student); // => false

Копия не является экземпляром Student. Это потому, что (без явных проверок) у нас не было бы возможности вернуть Studentкопию из «базового» класса. Мы можем только вернуть Person. Однако, если мы сбросили конструктор:

// correct the constructor pointer because it points to Person  
Student.prototype.constructor = Student;

... тогда все работает как положено:

var student1 = new Student("trinth");  
console.log(student1.copy() instanceof Student); // => true

34
Примечание. constructorАтрибут не имеет никакого особого значения в JS, так что вы можете также назвать его bananashake. Единственное отличие состоит в том, что движок автоматически инициализируется constructorпри f.prototypeкаждом объявлении функции f. Тем не менее, он может быть перезаписан в любое время.
user123444555621

58
@ Pumbaa80 - я получаю вашу точку, но тот факт , что двигатель автоматически инициализирует constructorозначает , что она делает имеют особое значение в JS, в значительной степени по определению.
Уэйн

13
Я просто хочу уточнить, что причина, по которой вы сказали, работает, потому что вы используете return new this.constructor(this.name);вместо return new Person(this.name);. Поскольку this.constructorэто Studentфункция (потому что вы установили ее с помощью Student.prototype.constructor = Student;), copyфункция в конечном итоге вызывает Studentфункцию. Я не уверен, что вы хотели сделать с //just as badкомментарием.
CEGRD

12
@ lwburk, что ты имеешь в виду под "// так же плохо"?
CEGRD

6
Я думаю, я понял. Но что, если Studentконструктор добавил дополнительный аргумент вроде Student(name, id):? Должны ли мы затем переопределить copyфункцию, вызвав Personверсию из нее, а затем скопировав дополнительное idсвойство?
snapfractalpop

76

Служит ли это какой-либо важной цели?

Да и нет.

В ES5 и более ранних версиях сам JavaScript ни constructorдля чего не использовался . Он определил, что объект по умолчанию в prototypeсвойстве функции будет иметь его и будет ссылаться на функцию, и на этом все . Ничто другое в спецификации не ссылается на это вообще.

Это изменилось в ES2015 (ES6), который начал использовать его в отношении иерархий наследования. Например, при создании нового обещания для возврата Promise#thenиспользуется constructorсвойство обещания, для которого вы его вызываете (через SpeciesConstructor ). Он также участвует в подтипировании массивов (через ArraySpeciesCreate ).

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

Можно ли это опустить?

Он там по умолчанию, вам нужно только вернуть его, когда вы замените объект в prototypeсвойстве функции :

Student.prototype = Object.create(Person.prototype);

Если вы этого не сделаете:

Student.prototype.constructor = Student;

... то Student.prototype.constructorнаследует от Person.prototypeкоторого (предположительно) имеет constructor = Person. Так что это вводит в заблуждение. И, конечно, если вы создаете подклассы для чего-то, что использует это (например, Promiseили Array) и не используете class¹ (который обрабатывает это для вас), вы захотите убедиться, что вы установили это правильно. Итак, в основном: это хорошая идея.

Это нормально, если ничто в вашем коде (или коде библиотеки, который вы используете) не использует его. Я всегда следил, чтобы он был правильно подключен.

Конечно, с ключевым словом ES2015 (он же ES6) classбольшую часть времени мы бы его использовали, нам больше не нужно, потому что оно обрабатывается для нас, когда мы делаем

class Student extends Person {
}

¹ «... если вы подклассифицируете что-то, что использует это (например, Promiseили Array), и не используете class...»  - это возможно сделать, но это настоящая боль (и немного глупая). Вы должны использовать Reflect.construct.


12

TLDR; Не супер необходим, но, вероятно, поможет в долгосрочной перспективе, и это более точно.

ПРИМЕЧАНИЕ: многое отредактировано, так как мой предыдущий ответ был сбивчиво написан, и в нем было несколько ошибок, которые я упустил, пытаясь ответить. Спасибо тем, кто указал на некоторые вопиющие ошибки.

По сути, это правильно подключить подклассы в Javascript. Когда мы создаем подкласс, мы должны сделать несколько забавных вещей, чтобы убедиться, что прототипное делегирование работает правильно, включая перезапись prototypeобъекта. Перезапись prototypeобъекта включает в себя constructor, поэтому нам нужно исправить ссылку.

Давайте быстро рассмотрим, как работают «классы» в ES5.

Допустим, у вас есть функция конструктора и ее прототип:

//Constructor Function
var Person = function(name, age) {
  this.name = name;
  this.age = age;
}

//Prototype Object - shared between all instances of Person
Person.prototype = {
  species: 'human',
}

Когда вы вызываете конструктор для создания экземпляра, говорите Adam:

// instantiate using the 'new' keyword
var adam = new Person('Adam', 19);

newКлючевое слово вызывается с «Персона» в основном будет работать конструктор Person с несколькими дополнительными строками кода:

function Person (name, age) {
  // This additional line is automatically added by the keyword 'new'
  // it sets up the relationship between the instance and the prototype object
  // So that the instance will delegate to the Prototype object
  this = Object.create(Person.prototype);

  this.name = name;
  this.age = age;

  return this;
}

/* So 'adam' will be an object that looks like this:
 * {
 *   name: 'Adam',
 *   age: 19
 * }
 */

Если мы console.log(adam.species), то поиск будет терпеть неудачу в adamслучае, и посмотреть вверх прототипичную цепь к его .prototype, которая Person.prototype- и Person.prototype имеет в .speciesсобственность, так что поиск будет успешным в Person.prototype. Это тогда войдет 'human'.

Здесь Person.prototype.constructorправильно будет указывать Person.

Итак, теперь интересная часть, так называемый «подкласс». Если мы хотим создать Studentкласс, который является подклассом Personкласса с некоторыми дополнительными изменениями, нам нужно убедиться, что он Student.prototype.constructorуказывает на точность ученика.

Это не делает это само по себе. Когда вы создаете подкласс, код выглядит так:

var Student = function(name, age, school) {
 // Calls the 'super' class, as every student is an instance of a Person
 Person.call(this, name, age);
 // This is what makes the Student instances different
 this.school = school
}

var eve = new Student('Eve', 20, 'UCSF');

console.log(Student.prototype); // this will be an empty object: {}

Вызов new Student()здесь вернет объект со всеми желаемыми свойствами. Здесь, если мы проверим eve instanceof Person, он вернется false. Если мы попытаемся получить доступ eve.species, он вернется undefined.

Другими словами, нам нужно подключить делегирование так, чтобы оно eve instanceof Personвозвращало истину, и чтобы экземпляры Studentделегата были правильно Student.prototype, а затем Person.prototype.

НО, поскольку мы называем его newключевым словом, помните, что добавляет этот вызов? Это вызвало бы то Object.create(Student.prototype), как мы установили эти делегативные отношения между Studentи Student.prototype. Обратите внимание, что сейчас Student.prototypeпусто. Поэтому поиск .speciesэкземпляра Studentне удастся, поскольку он делегирует только Student.prototype , а .speciesсвойство не существует Student.prototype.

Когда мы делаем присвоение Student.prototypeк Object.create(Person.prototype), Student.prototypeсам затем делегат Person.prototype, и глядя вверх eve.speciesвернешься , humanкак мы ожидаем. Предположительно мы хотим, чтобы он наследовал от Student.prototype AND Person.prototype. Таким образом, мы должны исправить все это.

/* This sets up the prototypal delegation correctly 
 *so that if a lookup fails on Student.prototype, it would delegate to Person's .prototype
 *This also allows us to add more things to Student.prototype 
 *that Person.prototype may not have
 *So now a failed lookup on an instance of Student 
 *will first look at Student.prototype, 
 *and failing that, go to Person.prototype (and failing /that/, where do we think it'll go?)
*/
Student.prototype = Object.create(Person.prototype);

Сейчас делегация работает, но мы перезаписываем Student.prototypeс оф Person.prototype. Так что, если мы позвоним Student.prototype.constructor, это будет указывать Personвместо Student. Вот почему мы должны это исправить.

// Now we fix what the .constructor property is pointing to    
Student.prototype.constructor = Student

// If we check instanceof here
console.log(eve instanceof Person) // true

В ES5 наше constructorсвойство является ссылкой, которая ссылается на функцию, которую мы написали с намерением стать «конструктором». Помимо того, что newдает нам ключевое слово, конструктор в противном случае является «простой» функцией.

В ES6 constructorтеперь встроен способ, которым мы пишем классы - например, он предоставляется как метод, когда мы объявляем класс. Это просто синтаксический сахар, но он предоставляет нам некоторые удобства, такие как доступ к superклассу, когда мы расширяем существующий класс. Поэтому мы бы написали приведенный выше код так:

class Person {
  // constructor function here
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  // static getter instead of a static property
  static get species() {
    return 'human';
  }
}

class Student extends Person {
   constructor(name, age, school) {
      // calling the superclass constructor
      super(name, age);
      this.school = school;
   }
}

eve instanceof Studentвернулся true. См. Stackoverflow.com/questions/35537995/… для объяснения. Кроме того, когда вы говорите, which is, at the moment, nothingчто вы имеете в виду? У каждой функции есть прототип, поэтому, если я проверю, Student.prototypeэто что-то.
Aseem Bansal

Моя ошибка. Он должен был прочитать 'eve instanceof Person', который вернул бы false. Я исправлю эту часть. Вы правы, что каждая функция имеет свойство прототипа. Однако , без назначения прототипа Object.create(Person.prototype), Student.prototypeпусто. Поэтому, если мы войдем в систему eve.species, он не будет делегировать должным образом до своего суперкласса Person, и он не будет регистрироваться 'human'. Предположительно, мы хотим, чтобы каждый подкласс наследовал от своего прототипа, а также прототипа своего супер-класса.
человек

Чтобы уточнить, which is, at the moment, nothingя имел в виду, что Student.prototypeобъект пуст.
человек

Подробнее о прототипе: без назначения Student.prototypeto Object.create(Person.prototype)- что, если вы помните, одинаково, как все экземпляры Person настроены на делегирование Person.prototype- поиск свойства на экземпляре Studentделегировал бы только Student.prototype . Так eve.speciesчто провалит его поиск. Если мы назначим его Student.prototypeсам, тогда делегат Person.prototype, и поиск eve.speciesвернется human.
человек

Кажется, здесь есть несколько неправильных вещей: «Это необходимо, когда вы пытаетесь эмулировать« подкласс »[...], чтобы при проверке, является ли экземпляр конструктором instance« подкласса », он будет точным». Нет, instanceofне использует constructor. «Однако, если мы посмотрим на студенческий .prototype.constructor, он все равно будет указывать на Персона» Нет, так и будет Student. Я не понимаю смысл этого примера. Вызов функции в конструкторе не является наследованием. «В ES6 конструктор теперь является реальной функцией, а не ссылкой на функцию». Что?
Феликс Клинг

10

Я бы не согласился. Нет необходимости устанавливать прототип. Возьмите тот же самый код, но удалите строку prototype.constructor. Что-нибудь меняется? Нет. Теперь внесите следующие изменения:

Person = function () {
    this.favoriteColor = 'black';
}

Student = function () {
    Person.call(this);
    this.favoriteColor = 'blue';
}

и в конце кода теста ...

alert(student1.favoriteColor);

Цвет будет синим.

По моему опыту, изменение в prototype.constructor мало что даст, если вы не делаете очень конкретные, очень сложные вещи, которые, вероятно, в любом случае не являются хорошей практикой :)

Редактировать: После того, как вы покопались в сети и немного поэкспериментировали, похоже, что люди настраивают конструктор так, чтобы он «выглядел» как то, что создается с помощью «new». Думаю, я бы сказал, что проблема в том, что javascript является языком-прототипом - не существует такой вещи, как наследование. Но большинство программистов происходят из опыта программирования, который выдвигает наследование как «путь». Таким образом, мы придумываем все виды вещей, чтобы попытаться сделать этот язык-прототип «классическим» языком… например, расширение «классов». Действительно, в приведенном ими примере новый ученик - это человек - он не «расширяется» от другого ученика… ученик - это все о человеке, и кем бы ни был этот ученик. Расширьте ученика, и все, что вы

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


8
Это не наследует прототип цепочки.
Cypher

1
@Cypher медленно хлопать в ладоши , четыре года спустя. Да, цепочка прототипов будет унаследован, независимо от того, вы перезаписать ли в prototype.constructor. Попробуйте проверить это.
Стивен

7
Вам не хватает кода, который наследует прототип. Добро пожаловать в интернет.
Cypher

1
Фрагмент кода @Cypher был основан на коде в связанной статье. Добро пожаловать на чтение вопроса в полном объеме. Ой. Подождите.
Стивен

1
@macher Я имел в виду это как классическое наследство. Плохой выбор формулировки с моей стороны.
Стивен

9

Это огромная ловушка, если вы написали

Student.prototype.constructor = Student;

но тогда, если был Учитель, чей прототип был также Человек, и вы написали

Teacher.prototype.constructor = Teacher;

тогда ученик конструктор теперь учитель!

Изменить: вы можете избежать этого, убедившись, что вы установили прототипы ученика и учителя, используя новые экземпляры класса Person, созданные с использованием Object.create, как в примере Mozilla.

Student.prototype = Object.create(Person.prototype);
Teacher.prototype = Object.create(Person.prototype);

1
Student.prototype = Object.create(...)предполагается в этом вопросе. Этот ответ добавляет только возможную путаницу.
Андре Chalella

3
@ AndréNeves Я нашел этот ответ полезным. Object.create(...)используется в статье MDN, которая породила вопрос, но не в самом вопросе. Я уверен, что многие люди не переходят.
Алекс Росс

Связанная статья, на которую ссылается вопрос, уже использует Object.create (). Этот ответ и Правка не очень актуальны, и по меньшей мере это сбивает с толку :-)
Дренай

1
В более широком смысле, есть ошибки, которые поймают людей, плохо знакомых с прототипами Javascript. Если мы обсуждаем в 2016 году, то вам действительно нужно использовать классы ES6, Babel и / или Typescript. Но если вы действительно хотите вручную создавать классы таким образом, это помогает понять, как цепочки прототипов действительно работают, чтобы использовать их мощь. Вы можете использовать любой объект в качестве прототипа, и, возможно, вы не хотите создавать новый отдельный объект. Более того, еще до того, как HTML 5 получил широкое распространение, Object.create не всегда был доступен, поэтому было проще настроить класс неправильно.
Джеймс Д

5

До сих пор путаница все еще там.

Следуя исходному примеру, поскольку у вас есть существующий объект student1как:

var student1 = new Student("Janet", "Applied Physics");

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

var student2 = new student1.constructor("Mark", "Object-Oriented JavaScript");

Здесь не удастся получить свойства, Studentесли свойство конструктора не установлено. Скорее это создаст Personобъект.


2

Получил хороший пример кода того, почему действительно необходимо установить конструктор прототипа.

function CarFactory(name){ 
   this.name=name;  
} 
CarFactory.prototype.CreateNewCar = function(){ 
    return new this.constructor("New Car "+ this.name); 
} 
CarFactory.prototype.toString=function(){ 
    return 'Car Factory ' + this.name;
} 

AudiFactory.prototype = new CarFactory();      // Here's where the inheritance occurs 
AudiFactory.prototype.constructor=AudiFactory;       // Otherwise instances of Audi would have a constructor of Car 

function AudiFactory(name){ 
    this.name=name;
} 

AudiFactory.prototype.toString=function(){ 
    return 'Audi Factory ' + this.name;
} 

var myAudiFactory = new AudiFactory('');
  alert('Hay your new ' + myAudiFactory + ' is ready.. Start Producing new audi cars !!! ');            

var newCar =  myAudiFactory.CreateNewCar(); // calls a method inherited from CarFactory 
alert(newCar); 

/*
Without resetting prototype constructor back to instance, new cars will not come from New Audi factory, Instead it will come from car factory ( base class )..   Dont we want our new car from Audi factory ???? 
*/

Ваш createNewCarметод создает фабрики !? Также это выглядит так, как будто это следовало использовать var audiFactory = new CarFactory("Audi")вместо наследования.
Берги

Ваш пример использует this.constructorвнутренне, поэтому не удивительно, что его нужно установить. У вас есть пример без него?
Дмитрий Зайцев

1

В наши дни не нужно использовать классы с сахаром или использовать «New». Используйте объектные литералы.

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

const Person = {
  name: '[Person.name]',
  greeting: function() {
    console.log( `My name is ${ this.name || '[Name not assigned]' }` );
  }
};
// Person.greeting = function() {...} // or define outside the obj if you must

// Object.create version
const john = Object.create( Person );
john.name = 'John';
console.log( john.name ); // John
john.greeting(); // My name is John 
// Define new greeting method
john.greeting = function() {
    console.log( `Hi, my name is ${ this.name }` )
};
john.greeting(); // Hi, my name is John

// Object.assign version
const jane = Object.assign( Person, { name: 'Jane' } );
console.log( jane.name ); // Jane
// Original greeting
jane.greeting(); // My name is Jane 

// Original Person obj is unaffected
console.log( Person.name ); // [Person.name]
console.log( Person.greeting() ); // My name is [Person.name]

Это стоит прочитать :

Основанные на классах объектно-ориентированные языки, такие как Java и C ++, основаны на концепции двух разных сущностей: классов и экземпляров.

...

Язык на основе прототипов, такой как JavaScript, не делает этого различия: он просто имеет объекты. Основанный на прототипе язык имеет понятие прототипного объекта, объекта, используемого в качестве шаблона для получения начальных свойств для нового объекта. Любой объект может указывать свои собственные свойства, либо при его создании, либо во время выполнения. Кроме того, любой объект может быть связан как прототип для другого объекта, что позволяет второму объекту совместно использовать свойства первого объекта.


1

Это необходимо, когда вам нужна альтернатива toStringбез обезьяноподготовки:

//Local
foo = [];
foo.toUpperCase = String(foo).toUpperCase;
foo.push("a");
foo.toUpperCase();

//Global
foo = [];
window.toUpperCase = function (obj) {return String(obj).toUpperCase();}
foo.push("a");
toUpperCase(foo);

//Prototype
foo = [];
Array.prototype.toUpperCase = String.prototype.toUpperCase;
foo.push("a");
foo.toUpperCase();

//toString alternative via Prototype constructor
foo = [];
Array.prototype.constructor = String.prototype.toUpperCase;
foo.push("a,b");
foo.constructor();

//toString override
var foo = [];
foo.push("a");
var bar = String(foo);
foo.toString = function() { return bar.toUpperCase(); }
foo.toString();

//Object prototype as a function
Math.prototype = function(char){return Math.prototype[char]};
Math.prototype.constructor = function() 
  {
  var i = 0, unicode = {}, zero_padding = "0000", max = 9999;
  
  while (i < max) 
    {
    Math.prototype[String.fromCharCode(parseInt(i, 16))] = ("u" + zero_padding + i).substr(-4);

    i = i + 1;
    }    
  }

Math.prototype.constructor();
console.log(Math.prototype("a") );
console.log(Math.prototype["a"] );
console.log(Math.prototype("a") === Math.prototype["a"]);


Что это должно делать? foo.constructor()??
Ry-

0

РЕДАКТИРОВАТЬ, я был на самом деле не так. Комментирование строки не меняет ее поведение вообще. (Я проверял это)


Да, это необходимо. Когда вы делаете

Student.prototype = new Person();  

Student.prototype.constructorстановится Person. Следовательно, вызов Student()будет возвращать объект, созданный Person. Если вы тогда делаете

Student.prototype.constructor = Student; 

Student.prototype.constructorсбрасывается обратно в Student. Теперь, когда вы вызываете Student()его Student, он выполняет , который вызывает родительский конструктор Parent(), он возвращает правильно унаследованный объект. Если вы не произвели сброс Student.prototype.constructorдо вызова, вы получите объект, для которого не было бы установлено ни одно из свойств Student().


3
Конструкция-прототип может стать личностью, но это уместно, поскольку она наследует все свойства и методы от личности. Создание нового Student () без установки файла prototype.constructor соответствующим образом вызывает его собственный конструктор.
Стивен

0

Дана простая функция конструктора:

function Person(){
    this.name = 'test';
}


console.log(Person.prototype.constructor) // function Person(){...}

Person.prototype = { //constructor in this case is Object
    sayName: function(){
        return this.name;
    }
}

var person = new Person();
console.log(person instanceof Person); //true
console.log(person.sayName()); //test
console.log(Person.prototype.constructor) // function Object(){...}

По умолчанию (из спецификации https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/constructor ) все прототипы автоматически получают свойство с именем constructor, которое указывает на функцию на который является собственностью. В зависимости от конструктора, к прототипу могут быть добавлены другие свойства и методы, что не является обычной практикой, но все же допускается для расширений.

Итак, просто отвечая: нам нужно убедиться, что значение в prototype.constructor установлено правильно, как предполагается в спецификации.

Должны ли мы всегда правильно устанавливать это значение? Это помогает при отладке и делает внутреннюю структуру согласованной со спецификацией. Мы должны определенно, когда наш API используется третьими сторонами, но не совсем, когда код наконец выполняется во время выполнения.


0

Вот один пример из MDN, который я нашел очень полезным для понимания его использования.

В JavaScript у нас есть, async functionsкоторый возвращает объект AsyncFunction . AsyncFunctionне является глобальным объектом, но его можно получить с помощью constructorсвойства и использовать его.

function resolveAfter2Seconds(x) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(x);
    }, 2000);
  });
}

// AsyncFunction constructor
var AsyncFunction = Object.getPrototypeOf(async function(){}).constructor

var a = new AsyncFunction('a', 
                          'b', 
                          'return await resolveAfter2Seconds(a) + await resolveAfter2Seconds(b);');

a(10, 20).then(v => {
  console.log(v); // prints 30 after 4 seconds
});

-1

Это не обязательно. Чемпионы ООП стараются превратить прототипическое наследование JavaScript в классическое наследование - это лишь одна из многих традиционных вещей. Единственное что следующее

Student.prototype.constructor = Student; 

делает, что теперь у вас есть ссылка на текущий "конструктор".

В ответе Уэйна, который был помечен как правильный, вы можете сделать то же самое, что и следующий код

Person.prototype.copy = function() {  
    // return new Person(this.name); // just as bad
    return new this.constructor(this.name);
};  

с кодом ниже (просто замените this.constructor на Person)

Person.prototype.copy = function() {  
    // return new Person(this.name); // just as bad
    return new Person(this.name);
}; 

Слава Богу, что с классическим наследованием ES6 пуристы могут использовать нативные операторы языка, такие как class, extends и super, и нам не нужно видеть подобные как prototype.constructor исправления и родительские ссылки.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.