Согласно Википедии, объектно-ориентированное программирование (ООП) - это парадигма программирования, основанная на концепции «объектов», которые могут содержать данные и код: данные в виде полей (часто называемых атрибутами или свойствами) и код в виде функций (часто называемые методами).

Итак, давайте погрузимся в объекты Javascript и их прототипы, создав сперва простой объект:

const obj = {
  user: "Иван",
  where: "fruntend.com"
}

Добавим метод message с анонимной функцией. this в методе используется для связи переменных внутри метода с объектом.

const obj = {
  user: "Иван",
  where: "fruntend.com",
  message: function () {
    console.log(`${this.user} посетил ${this.where}`)
  }
}

Посмотрим, что нам отобразится, если мы посмотрим на наш объект в консоли (console.log(obj)):

объект, выведенный в консоли

Как видим, у нас кроме наших данных есть ещё одно поле [[Prototype]], являющееся объектом. Посмотрим, что там:

объект, выведенный в консоли и раскрытый прототип

Это и есть прототип объекта obj - некий стандартный объект-шаблон, от которого наш obj наследует свойства и методы.

Получить прототип нашего объекта можно с помощью команды:

console.log(obj.__proto__)

или

Object.getPrototypeOf(obj)

На выходе получим все тот же прототип:

прототип объекта, выведенный в консоли

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

Теперь попробуем понять, что значит "объект-шаблон, от которого наш obj наследует свойства и методы". Согласно определению, объект obj, кроме своего родного метода message, будет иметь методы, существующие в его прототипе. Давайте это проверим. Применим нашему объекту метод из прототипа hasOwnProperty:

console.log(obj.hasOwnProperty())  // => false
console.log(obj.hasOwnProperty('message'))  // => true 

Как видим, это работает!

Теперь попробуем добавить такой же метод нашему объекту и вывести его в консоли:

const obj = {
  user: "Иван",
  where: "fruntend.com",
  message: function () {
    console.log(`${this.user} посетил ${this.where}`)
  },
  hasOwnProperty: function () {
    console.log('метод hasOwnProperty добавлен в объект obj')
  }
}

console.log(obj.hasOwnProperty())  // => 'метод hasOwnProperty добавлен в объект obj'

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

  1. Сперва осуществляется поиск в самом объекте
  2. После, если не находит, в прототипе объекта
  3. После в прототипе прототипа…

Как назначить прототип

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

С помощью Object.create()

const objWithProto = Object.create(obj)

console.log(objWithProto)  // выдаст пустой объект
console.log(objWithProto.message())  // 'Иван посетил fruntend.com' - отработал метод из прототипа

Если выведем в консоли прототип нового объекта (console.log(objWithProto.__proto__)), получим:

прототип нового объекта, выведенный в консоли

С помощью конструктора

Создадим конструктор:

function User (user, where) {
  this.user = user;
  this.where = where
}

Теперь создадим метод для прототипа:

const userPrototype = {
  message () {
    console.log(`${this.user} посетил ${this.where}`)
  }
}

И добавим его к прототипу объекта:

User.prototype = userPrototype;

Добавим конструктор User к прототипу (таким образом возьмутся параметры конструктора, которые будут использованы в методе message прототипа):

User.prototype.constructor = User;

Что осталось сделать - это проверить правильность наших действий:

const myUser = new User('Петр Петров', 'fruntend.com');
myUser.message()  // => 'Петр Петров посетил fruntend.com'

А команда console.log(myUser.__proto__) выдаст:

прототип конструктора