Дуалистичная типовая система JavaScript VS Единая объектная система Python. Краткий обзор

Привет, Хабр!

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

Данная статья является сравнением двух подходов в двух разных ЯП. Каких-либо похожих материалов в рунете я не обнаружил (кроме редких упоминаний о том, что такое различие в целом существует), поэтому с радостью почитаю ваше мнение и/или какие-то материалы, если вы оставите ссылки на них в комментариях.

Теперь перейдем к теме!

Содержание

Какие подходы бывают

Думаю, стоит начать с того, что существуют разные подходы к »‎Объектной архитектуре» в разных языках программирования:

  • Прототипирование как в JS ‎

  • Объектно-ориентированный подход (Python)

  • Объект, как одна из сущностей (Примитивы, объекты и структуры данных в Go, Rust)

И сегодня я хочу ближе рассмотреть то, как реализована типовая и объектная системы в языках Python и JavaScript.

Объектная архитектура в JavaScript

В JavaScript основные типы делятся на две категории: примитивы (о них позже) и объекты. Рассмотрим подробнее, что вообще из себя представляет этот объект.

Объект в языках программирования — это структура данных, которая описывается внутренними свойствами и методами.

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

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

let person = {
  name: "Alex",
  age: 33
}

У нас есть объект person и его атрибуты name и age со значениями «Alex» и 33.

Там еще было что-то про методы… точно! Помимо атрибутов наш объект может иметь методы (проще говоря — функцию, привязанную к этому объекту). Пример кода будет выглядеть примерно так:

let person = {
  name: "Alex",
  age: 33,
  
  sayHello: function() {
    alert('Hello!')
  }
}

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

person.sayHello() // Выведет "Hello!"

Вот мы и получили наш полноценный объект! В целом с определением покончено, давайте посмотрим на архитектуру…

Примитивы и объекты

В JS есть 8 основных типов:

  • number

  • bigInt

  • null

  • undefined

  • symbol

  • string

  • boolean

  • object

Все типы, кроме нашего object, являются примитивами. Подробно на всех останавливаться не будем, скажем лишь то, что их отличает. от объекта. А ключевое различие в том, что примитивы не имею собственных свойств и методов.

let name = "Alex";  // Строка как она есть

Кто-то скажет: «Но мы же можем вызывать методы на примитивах…». Да, можем, но это все благодаря тому, что JavaScript заботливо делает обёртку над примитивами, когда мы хотим вызвать тот или иной метод. Например, мы хотим вывести наше имя в верхнем регистре, тогда мы поступим так:

let name = 'Alex'
alert(name.toUpperCase()) // Под капотом JS делает так: 
                          // alert(String(name).toUpperCase())

Взгляд со стороны ООП

Быстро пробежав по примитивам идём дальше — ООП и классы. И тут мы можем ввести второе определение для объекта.

Объект — это экземпляр какого-либо класса содержащий свойства и методы.

В JS поддержка ООП и классов была введена совсем недавно (2015 год, когда сам язык был создан в 1995). Изначально язык задумывался как процедурный, с поддержкой прототипного программирования (о нём будет далее).
Добавлю лишь, что в JavaScript есть сильная функциональная парадигма, поэтому полезно упомянуть и о ней как об альтернативе объектно-ориентированному стилю.

Классы — это буквально шаблоны для объектов (экземпляров). И классы это именно то (со стороны Python), что будет противопоставляться прототипированию далее в статье.

Примеры и различие в коде:

Скрытый текст

class Person {        // Объявление класса
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  greet() {
    console.log(`Hello, my name is ${this.name}`);
  }
}

const person = new Person('Alice', 30);  // Создание объекта
const person = {    // Классический объект
  name: 'Alice',
  age: 30,
  greet() {
    console.log(`Hello, my name is ${this.name}`);
  }
};

Прототипирование и JavaScript

Прототипирование — мощный инструмент в правильных руках. Именно благодаря такой концепции (на мой взгляд) JS так долго был без классов и прекрасно справлялся со своими задачами. Так о чём же оно?

Прототипирование — это механизм, с помощью которого одни объекты могут наследовать свойства и методы у других.

Тут наверное надо было бы сказать про то, что такое наследование, но думаю в целом из контекста будет понятно. (Буду на это надеяться)

Каждый объект в JavaScript имеет скрытое свойство [[Prototype]], которое указывает на другой объект — прототип. Если объекту требуется доступ к свойству или методу, которого у него самого нет, JavaScript пытается найти это свойство или метод в его прототипе. Если свойство не найдено, продолжается поиск по цепочке прототипов до тех пор, пока не будет найдено свойство или не закончится цепочка (которая в конечном итоге ведет к Object.prototype). Тяжело, будем разбираться.

Приведу классический пример: Создадим объект animal и объект dog, для которого объект animal будет прототипом (иначе говоря: dog наследуется от animal).

const animal = {
  eats: true,
  walk() {
    console.log("Animal walks");
  }
};

const dog = {
  bark() {
    console.log("Dog barks");
  }
};

Object.setPrototypeOf(dog, animal) // Устанавливаем для dog прототип animal
dog.bark();  // Dog barks
dog.walk();  // Animal walks (наследовано от прототипа)

Подробнее про прототипы советую почитать на иных ресурсах, тут лишь отмечу, что прототипирование может составлять конкуренцию ООП в других языках и кстати о них…

Объектная архитектура в Python

Теперь поговорим о языке, в котором прямая альтернатива прототипированию — Объектно-ориентированный подход.

Тут непременно стоит сказать фразу, которую должен знать каждый программист, который хоть раз сталкивался с этим языком: »‎В Python всё — это объекты». ‎ Если говорить конкретнее, то объектами является действительно любая сущность: строки, числа, списки, словари, классы, функции и т.д. При этом любые операции под капотом являются методами этих объектов. Пример:

5 + 10  # Это эквивалентно вызову 5.__add__(10)

На примере мы видим магический метод __add__, который обеспечивает операцию сложения. Не стоит углубляться в тонкости, главное понять, то, что я озвучил выше: любые операции под капотом являются методами объектов.

Самые внимательные уже заметили, что речь шла именно про метод, который в свою очередь должно относиться непосредственно к какому-либо классу. Вот тут и надо отметить, что вся объектная архитектура в Python построена вокруг ООП.

Все встроенные типы данных в языке — это классы. (Именно благодаря этому возможен пример выше)

print(type(41)) # Вывод: 

Так как в прошлой части речь шла про наследование, благодаря прототипированию, то давайте рассмотрим его в ООП:

class Animal:
    def __init__(self):
        self.eats = True

    def walk(self):
        print("Animal walks")

class Dog(Animal):  # Так наследуются классы в Python
    def bark(self):
        print("Dog barks")

dog = Dog() # Создание экземлпяра объекта
dog.bark()  # Dog barks
dog.walk()  # Animal walks (наследовано от Animal)

Конечно, классы и прототипы отличаются в первую очередь синтаксисом (поскольку это совершенно разные механизмы), но главное отличие: это экземпляры.

Повторюсь, в Python всё — объекты, поэтому тут нет отдельного типа для объектов, как в JS.

Что же лучше?

Однозначно, конечно, сказать нельзя, ведь это разные подходы к объектам в целом.
С одной стороны мне нравятся объекты в JS (даже чисто визуально), с другой при выборе прототипирование или ООП — я всецело буду за ООП. Напомню, что прототипирование и объекты в JS — это больше про «кирпичики в нашей программе», когда классы в свою очередь являются «шаблонами» (соответственно их экземпляры и есть эти «кирпичики»)
Думаю, что выбор из двух подходов в праве делать каждый разработчик сам по своим предпочтениям.

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

Моё небольшое сравнение подошло к концу, благодарю всех, кто дочитал до этого момента и надеюсь, что этот материал был для вас полезный (или как минимум интересный).
С радостью приму обратную связь в комментариях!

© Habrahabr.ru