← JavaScript/Встроенные прототипы#117 из 383← ПредыдущийСледующий →+20 XP
Полезно по теме:Гайд: как учить JavaScriptПрактика: JS базаПрактика: async и сетьТермин: Closure

Встроенные прототипы

Ты вызываешь [1, 2, 3].map() — но откуда у массива этот метод? В массиве его нет. JavaScript ищет map в Array.prototype — специальном объекте, который является прототипом для всех массивов. Именно там живут map, filter, reduce и ещё 30+ методов.

На основе предыдущих уроков

  • «Прототипы» — цепочка [[Prototype]], поиск свойств вверх по цепочке
  • «F.prototype» — как функции-конструкторы и prototype связаны
  • «instanceof» — работает через цепочку прототипов
  • Как методы "появляются" у каждого массива

    Когда создаётся массив, его [[Prototype]] указывает на Array.prototype — объект со всеми методами массивов:

    const nums = [1, 2, 3]
    
    // Где живёт метод map?
    console.log(nums.hasOwnProperty('map'))             // false — не в самом массиве
    console.log(Array.prototype.hasOwnProperty('map'))  // true — в прототипе!
    
    // Цепочка: nums → Array.prototype → Object.prototype → null
    console.log(Object.getPrototypeOf(nums) === Array.prototype)             // true
    console.log(Object.getPrototypeOf(Array.prototype) === Object.prototype) // true

    Полная цепочка встроенных прототипов

    // Массив
    // [] → Array.prototype → Object.prototype → null
    
    // Строка
    // '' → String.prototype → Object.prototype → null
    
    // Функция
    // function() {} → Function.prototype → Object.prototype → null
    
    // Object.prototype — вершина всех цепочек
    console.log(Object.prototype.__proto__)  // null

    hasOwnProperty vs in vs Object.hasOwn

    hasOwnProperty проверяет только собственные свойства объекта, оператор in — весь прототип:

    const order = { id: 42, total: 1500 }
    
    console.log('id' in order)                    // true
    console.log('toString' in order)              // true  — из Object.prototype
    console.log(order.hasOwnProperty('id'))       // true
    console.log(order.hasOwnProperty('toString')) // false — не собственное!
    
    // Современный способ (безопаснее — не сломается если hasOwnProperty переопределён):
    console.log(Object.hasOwn(order, 'id'))       // true
    console.log(Object.hasOwn(order, 'toString')) // false

    Object.prototype.toString.call() для определения типа

    typeof не всегда помогает (typeof null === 'object'). Метод Object.prototype.toString возвращает точный тип:

    function getType(value) {
      return Object.prototype.toString.call(value)
    }
    
    console.log(getType([]))           // '[object Array]'
    console.log(getType({}))           // '[object Object]'
    console.log(getType(null))         // '[object Null]'
    console.log(getType(undefined))    // '[object Undefined]'
    console.log(getType(42))           // '[object Number]'
    console.log(getType('строка'))     // '[object String]'
    console.log(getType(/\d+/))       // '[object RegExp]'
    console.log(getType(new Date()))   // '[object Date]'

    Полифилы — добавление методов в прототипы

    Полифил — реализация стандартного метода для старых сред:

    // Полифил для Array.prototype.flat
    if (!Array.prototype.flat) {
      Array.prototype.flat = function(depth = 1) {
        if (depth <= 0) return this.slice()
        return this.reduce(function(acc, item) {
          if (Array.isArray(item)) {
            acc.push(...item.flat(depth - 1))
          } else {
            acc.push(item)
          }
          return acc
        }, [])
      }
    }
    
    const nested = [1, [2, [3, [4]]]]
    console.log(nested.flat())     // [1, 2, [3, [4]]]
    console.log(nested.flat(2))    // [1, 2, 3, [4]]

    Почему нельзя изменять встроенные прототипы в продакшн-коде

    Изменение встроенных прототипов — опасная практика:

  • Конфликты имён — будущие версии JS могут добавить метод с тем же именем, но другим поведением
  • Конфликты между библиотеками — две библиотеки добавят разные реализации одного метода
  • Неожиданное поведение — другие разработчики не ожидают изменений в стандартных объектах
  • // Плохо — изменяем глобальный прототип:
    Array.prototype.last = function() { return this[this.length - 1] }
    
    // Хорошо — используем обычную функцию:
    function last(arr) { return arr[arr.length - 1] }
    
    // Или только для полифилов со строгой проверкой:
    if (!Array.prototype.at) {
      Array.prototype.at = function(index) {
        if (index < 0) return this[this.length + index]
        return this[index]
      }
    }

    Типичные ошибки

    1. Потеря hasOwnProperty — объект создан через Object.create(null):

    // Словарь без прототипа — нет метода hasOwnProperty!
    const dict = Object.create(null)
    dict.name = 'Иван'
    dict.hasOwnProperty('name')  // TypeError: dict.hasOwnProperty is not a function
    
    // Правильно: использовать Object.hasOwn или вызывать из прототипа:
    Object.hasOwn(dict, 'name')                       // true — безопасно
    Object.prototype.hasOwnProperty.call(dict, 'name') // true — старый способ

    2. for...in итерирует унаследованные свойства при изменении прототипа:

    Array.prototype.myMethod = function() {}
    
    const arr = [1, 2, 3]
    for (const key in arr) {
      console.log(key)  // '0', '1', '2', 'myMethod' — лишнее попало!
    }
    
    // Правильно: всегда фильтруй через hasOwn:
    for (const key in arr) {
      if (Object.hasOwn(arr, key)) console.log(key)  // только '0', '1', '2'
    }

    3. Полифил без проверки на существование — перезапишет нативный метод:

    // Плохо: перезаписывает нативный Array.prototype.flat если он есть
    Array.prototype.flat = function() { /* своя реализация */ }
    
    // Хорошо: добавляем только если метода нет
    if (!Array.prototype.flat) {
      Array.prototype.flat = function() { /* полифил */ }
    }

    В реальных проектах

  • Полифилы — core-js, polyfill.io добавляют методы для старых браузеров через прототипы
  • Отладка — Object.prototype.toString.call(value) для точного определения типа
  • Object.create(null) — безопасные словари без конфликтов с прототипом
  • for...in с hasOwnProperty — фильтрация унаследованных свойств при обходе объектов
  • Примеры

    Цепочка прототипов массива, hasOwnProperty vs in, Object.prototype.toString для определения типа

    // 1. Исследуем цепочку прототипов массива
    const cart = ['молоко', 'хлеб', 'яйца']
    
    console.log('--- Цепочка прототипов ---')
    console.log(Object.getPrototypeOf(cart) === Array.prototype)              // true
    console.log(Object.getPrototypeOf(Array.prototype) === Object.prototype)  // true
    console.log(Object.getPrototypeOf(Object.prototype))                      // null — конец цепочки
    
    // 2. hasOwnProperty vs in
    console.log('\n--- Собственные свойства vs прототип ---')
    const product = { name: 'Чай', price: 120 }
    
    console.log(product.hasOwnProperty('name'))     // true
    console.log('name' in product)                  // true
    
    console.log(product.hasOwnProperty('toString')) // false — не собственное
    console.log('toString' in product)              // true  — есть в цепочке!
    
    console.log('map' in product)                   // false — map только у Array
    
    // Безопасная итерация только по своим ключам:
    for (const key in product) {
      if (Object.hasOwn(product, key)) {
        console.log(`${key}: ${product[key]}`)
      }
    }
    // name: Чай
    // price: 120
    
    // 3. Object.prototype.toString — точное определение типа
    console.log('\n--- Определение типа ---')
    function getType(value) {
      return Object.prototype.toString.call(value).slice(8, -1)  // 'Array', 'Date', etc.
    }
    
    const items = [
      ['Алиса', 'String'],
      [42, 'Number'],
      [[], 'Array'],
      [{}, 'Object'],
      [null, 'Null'],
      [undefined, 'Undefined'],
      [new Date(), 'Date'],
      [/\d+/, 'RegExp'],
    ]
    
    items.forEach(([value, expected]) => {
      const actual = getType(value)
      const status = actual === expected ? 'OK' : 'ОШИБКА'
      console.log(`getType(${JSON.stringify(value)}) = '${actual}' (${status})`)
    })

    Встроенные прототипы

    Ты вызываешь [1, 2, 3].map() — но откуда у массива этот метод? В массиве его нет. JavaScript ищет map в Array.prototype — специальном объекте, который является прототипом для всех массивов. Именно там живут map, filter, reduce и ещё 30+ методов.

    На основе предыдущих уроков

  • «Прототипы» — цепочка [[Prototype]], поиск свойств вверх по цепочке
  • «F.prototype» — как функции-конструкторы и prototype связаны
  • «instanceof» — работает через цепочку прототипов
  • Как методы "появляются" у каждого массива

    Когда создаётся массив, его [[Prototype]] указывает на Array.prototype — объект со всеми методами массивов:

    const nums = [1, 2, 3]
    
    // Где живёт метод map?
    console.log(nums.hasOwnProperty('map'))             // false — не в самом массиве
    console.log(Array.prototype.hasOwnProperty('map'))  // true — в прототипе!
    
    // Цепочка: nums → Array.prototype → Object.prototype → null
    console.log(Object.getPrototypeOf(nums) === Array.prototype)             // true
    console.log(Object.getPrototypeOf(Array.prototype) === Object.prototype) // true

    Полная цепочка встроенных прототипов

    // Массив
    // [] → Array.prototype → Object.prototype → null
    
    // Строка
    // '' → String.prototype → Object.prototype → null
    
    // Функция
    // function() {} → Function.prototype → Object.prototype → null
    
    // Object.prototype — вершина всех цепочек
    console.log(Object.prototype.__proto__)  // null

    hasOwnProperty vs in vs Object.hasOwn

    hasOwnProperty проверяет только собственные свойства объекта, оператор in — весь прототип:

    const order = { id: 42, total: 1500 }
    
    console.log('id' in order)                    // true
    console.log('toString' in order)              // true  — из Object.prototype
    console.log(order.hasOwnProperty('id'))       // true
    console.log(order.hasOwnProperty('toString')) // false — не собственное!
    
    // Современный способ (безопаснее — не сломается если hasOwnProperty переопределён):
    console.log(Object.hasOwn(order, 'id'))       // true
    console.log(Object.hasOwn(order, 'toString')) // false

    Object.prototype.toString.call() для определения типа

    typeof не всегда помогает (typeof null === 'object'). Метод Object.prototype.toString возвращает точный тип:

    function getType(value) {
      return Object.prototype.toString.call(value)
    }
    
    console.log(getType([]))           // '[object Array]'
    console.log(getType({}))           // '[object Object]'
    console.log(getType(null))         // '[object Null]'
    console.log(getType(undefined))    // '[object Undefined]'
    console.log(getType(42))           // '[object Number]'
    console.log(getType('строка'))     // '[object String]'
    console.log(getType(/\d+/))       // '[object RegExp]'
    console.log(getType(new Date()))   // '[object Date]'

    Полифилы — добавление методов в прототипы

    Полифил — реализация стандартного метода для старых сред:

    // Полифил для Array.prototype.flat
    if (!Array.prototype.flat) {
      Array.prototype.flat = function(depth = 1) {
        if (depth <= 0) return this.slice()
        return this.reduce(function(acc, item) {
          if (Array.isArray(item)) {
            acc.push(...item.flat(depth - 1))
          } else {
            acc.push(item)
          }
          return acc
        }, [])
      }
    }
    
    const nested = [1, [2, [3, [4]]]]
    console.log(nested.flat())     // [1, 2, [3, [4]]]
    console.log(nested.flat(2))    // [1, 2, 3, [4]]

    Почему нельзя изменять встроенные прототипы в продакшн-коде

    Изменение встроенных прототипов — опасная практика:

  • Конфликты имён — будущие версии JS могут добавить метод с тем же именем, но другим поведением
  • Конфликты между библиотеками — две библиотеки добавят разные реализации одного метода
  • Неожиданное поведение — другие разработчики не ожидают изменений в стандартных объектах
  • // Плохо — изменяем глобальный прототип:
    Array.prototype.last = function() { return this[this.length - 1] }
    
    // Хорошо — используем обычную функцию:
    function last(arr) { return arr[arr.length - 1] }
    
    // Или только для полифилов со строгой проверкой:
    if (!Array.prototype.at) {
      Array.prototype.at = function(index) {
        if (index < 0) return this[this.length + index]
        return this[index]
      }
    }

    Типичные ошибки

    1. Потеря hasOwnProperty — объект создан через Object.create(null):

    // Словарь без прототипа — нет метода hasOwnProperty!
    const dict = Object.create(null)
    dict.name = 'Иван'
    dict.hasOwnProperty('name')  // TypeError: dict.hasOwnProperty is not a function
    
    // Правильно: использовать Object.hasOwn или вызывать из прототипа:
    Object.hasOwn(dict, 'name')                       // true — безопасно
    Object.prototype.hasOwnProperty.call(dict, 'name') // true — старый способ

    2. for...in итерирует унаследованные свойства при изменении прототипа:

    Array.prototype.myMethod = function() {}
    
    const arr = [1, 2, 3]
    for (const key in arr) {
      console.log(key)  // '0', '1', '2', 'myMethod' — лишнее попало!
    }
    
    // Правильно: всегда фильтруй через hasOwn:
    for (const key in arr) {
      if (Object.hasOwn(arr, key)) console.log(key)  // только '0', '1', '2'
    }

    3. Полифил без проверки на существование — перезапишет нативный метод:

    // Плохо: перезаписывает нативный Array.prototype.flat если он есть
    Array.prototype.flat = function() { /* своя реализация */ }
    
    // Хорошо: добавляем только если метода нет
    if (!Array.prototype.flat) {
      Array.prototype.flat = function() { /* полифил */ }
    }

    В реальных проектах

  • Полифилы — core-js, polyfill.io добавляют методы для старых браузеров через прототипы
  • Отладка — Object.prototype.toString.call(value) для точного определения типа
  • Object.create(null) — безопасные словари без конфликтов с прототипом
  • for...in с hasOwnProperty — фильтрация унаследованных свойств при обходе объектов
  • Примеры

    Цепочка прототипов массива, hasOwnProperty vs in, Object.prototype.toString для определения типа

    // 1. Исследуем цепочку прототипов массива
    const cart = ['молоко', 'хлеб', 'яйца']
    
    console.log('--- Цепочка прототипов ---')
    console.log(Object.getPrototypeOf(cart) === Array.prototype)              // true
    console.log(Object.getPrototypeOf(Array.prototype) === Object.prototype)  // true
    console.log(Object.getPrototypeOf(Object.prototype))                      // null — конец цепочки
    
    // 2. hasOwnProperty vs in
    console.log('\n--- Собственные свойства vs прототип ---')
    const product = { name: 'Чай', price: 120 }
    
    console.log(product.hasOwnProperty('name'))     // true
    console.log('name' in product)                  // true
    
    console.log(product.hasOwnProperty('toString')) // false — не собственное
    console.log('toString' in product)              // true  — есть в цепочке!
    
    console.log('map' in product)                   // false — map только у Array
    
    // Безопасная итерация только по своим ключам:
    for (const key in product) {
      if (Object.hasOwn(product, key)) {
        console.log(`${key}: ${product[key]}`)
      }
    }
    // name: Чай
    // price: 120
    
    // 3. Object.prototype.toString — точное определение типа
    console.log('\n--- Определение типа ---')
    function getType(value) {
      return Object.prototype.toString.call(value).slice(8, -1)  // 'Array', 'Date', etc.
    }
    
    const items = [
      ['Алиса', 'String'],
      [42, 'Number'],
      [[], 'Array'],
      [{}, 'Object'],
      [null, 'Null'],
      [undefined, 'Undefined'],
      [new Date(), 'Date'],
      [/\d+/, 'RegExp'],
    ]
    
    items.forEach(([value, expected]) => {
      const actual = getType(value)
      const status = actual === expected ? 'OK' : 'ОШИБКА'
      console.log(`getType(${JSON.stringify(value)}) = '${actual}' (${status})`)
    })

    Задание

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

    Подсказка

    fn(item) даёт ключ группы. Если groups[key] ещё не создан — инициализируй его как []. Затем groups[key].push(item). return this.reduce(..., {}) — начальное значение аккумулятора — пустой объект {}.

    Загружаем среду выполнения...
    Загружаем AI-помощника...