Protótipos intermediários
Introdução
Javascript é uma linguagem com herança prototípica, os atributos não encontrados de um objeto são procurados no objeto vinculado como o protótipo do primeiro. O segundo objeto pode ter um terceiro vinculado pelo próprio protótipo e assim sucessivamente. É como se fosse uma lista encadeada. O protótipo dos arrays possuem os seguintes atributos:
// corrente.js
var a = []
var pa = Object.getPrototypeOf(a)
Object.getOwnPropertyNames(pa).join('\n')
O protótipo do objeto Object é o oceano onde todas as cadeias de protótipos
deságuam. E o protótipo deste objeto é null.
// oceano.js
var a = []
var pa = Object.getPrototypeOf(a)
var ppa = Object.getPrototypeOf(pa)
var o = {}
var po = Object.getPrototypeOf(o)
ppa === po
Vários objetos diferentes podem possuir o mesmo protótipo, o código nele presente passa a poder ser acessado por todos esses filhos. É um mecanismo similar às classes em orientação a objeto, porém com protótipos a herança acontece por objetos manuseáveis durante a execução do programa.
MDN
Abaixo vários links para documentação relacionada:
Tipo vetor
Array é um dos objetos especializados do javascript, seu protótipo aponta para
o protótipo base do Object e possui diversos métodos para lidar com seus
elementos, como reduce ou sort. Mesmo assim é possível adicionar atributos
não numéricos a um objeto do tipo array:
// tipoVetor1.js
var a = []
a.a = a
a.b = 'b'
a[-1] = 'negativo'
// a['2'] = 'third'
a.push('first')
a.push('second')
for(let i in a){
console.log(`a[${i}] = ${a[i]}`)
}
console.log(a.join(', '))
Podemos criar algo similar a um vetor a partir de objetos mimetizando seus atributos iniciais e seu protótipo:
var mkArray = () => Object.assign(
Object.create(
Object.getPrototypeOf([])
),
{ 'length': 0 }
)
var a = mkArray()
a.push('first')
a.push('second')
a['2'] = 'third'
for(let i in a){
console.log(`a[${i}] = ${a[i]}`)
}
console.log(a.join(', '))
console.log(Object.getOwnPropertyDescriptors([]).length)
console.log(Object.getOwnPropertyDescriptors(a).length)
Repare que o atributo length possui um comportamento dissonante, ainda é
necessário alterar algumas das suas descrições:
var mkArray = () => Object.defineProperty(
Object.create(
Object.getPrototypeOf([])
),
'length',
{
'value': 0,
'writable': true, // not default
'configurable': false, //default
'enumerable': false //default
}
)
var a = mkArray()
console.log(Object.getOwnPropertyDescriptors([]).length)
console.log(Object.getOwnPropertyDescriptors(a).length)
Herança
Para adicionar o comportamento de soma podemos colocar o método nos vetores, isto implica que cada um dos objetos terá sua cópia da função. Para aliviar o uso da memória podermos colocar esta função num objeto externo e apenas a referencia no atributo do vetor. Nesse sentido um objeto externo privilegiado seria o protótipo, onde a busca por referência já é feita automaticamente.
// heranca.js
var a1 = [1, 1, 1]
a1.sum = function sum () {
return this.reduce((a, b) => a + b, 0)
}
console.log(`a1.sum() = ${a1.sum()}`)
var tools4Arrays = {}
tools4Arrays.sum = function sum () {
return this.reduce((a, b) => a + b, 0)
}
var a2 = [2,2,2]
a2.sum = tools4Arrays.sum
console.log(`a2.sum() = ${a2.sum()}`)
var a3 = [3, 3, 3]
Object.getPrototypeOf(a3).sum = function() {
return this.reduce((a, b) => a + b, 0)
}
console.log(`a3.sum() = ${a3.sum()}`)
console.log(`now everyone has sum() = ${[4, 4, 4].sum()}`)
Contudo alterar o protótipo de um objeto que outra parte da aplicação faz uso pode impactar silenciosamente na execução do programa.
Mix In
Um alternativa seria, ao invés de alterar o protótipo diretamente, adicionar um elo na cadeia que contenha os atributos necessários e este elo ter seu protótipo apontado para o protótipo original, desta forma os atributos do objeto intrometido são consultados se não existir este atributo no objeto inicial, por sua vez se não for localizado neste intermediário a busca segue através da cadeia prototípica.

// mixin.js
var protoOfArray = Object.getPrototypeOf([])
var tools4Arrays = Object.defineProperty(
Object.create(protoOfArray),
'sum',
{
'value': function sum () {
return this.reduce((a, b) => a + b, 0)
},
'writable': false,
'configurable': false,
'enumerable': true
}
)
var a3 = Object.setPrototypeOf(new Array(0), tools4Arrays)
a3[0] = 3
a3[1] = 3
a3[2] = 3
console.log(`a3.sum() = ${a3.sum()}`)
console.error(`just a3 has sum() = ${[4, 4, 4].sum()}`)
Os atributos originais na cadeia protótipa continuam acessíveis (se não forem
sobrescritas), como por exemplo sort. Tudo se passa como se este objeto fosse
um array com alguns atributos curto-circuitados na hierarquia das heranças.
Construtores
O conceito de construtor está presente desde as primeiras especificações e lembra
o comportamento de uma Classe, ele define uma função que gera novos objetos e se
convenciona usar letra inicial maiúscula. A função construtura deve possuir o
protótipo igual aos objetos que cria e estes devem acessar um atributo
constructor que aponta para esta função.
// constructor.js
var protoOfArray = Object.getPrototypeOf([])
var tools4Arrays = Object.defineProperty(
Object.create(protoOfArray),
'sum',
{
'value': function sum () {
return this.reduce((a, b) => a + b, 0)
},
'enumerable': true
}
)
tools4Arrays = Object.defineProperty(
tools4Arrays,
'constructor',
{
'value': ExtArr
}
)
function ExtArr (n) {
return Object.setPrototypeOf(new Array(n), tools4Arrays)
}
ExtArr.prototype = tools4Arrays
var a3 = ExtArr(3)
a3[0] = 3
a3[1] = 3
a3[2] = 3
console.log(`Array.isArray(a3) = ${Array.isArray(a3)}`)
console.log(`a3 instanceof Array = ${a3 instanceof Array}`)
console.log(`a3 instanceof ExtArr = ${a3 instanceof ExtArr}`)
No código acima já obtemos que a3 instanceof ExtArr é verdadeira, onde ExtArr
é a função construtora que fabrica novos objetos. De forma análoga Array é a
função construtora de vetores, Object é a função construtora de objetos e
função construtora das funções anteriores é a Function, que é função
construtora de si mesma e uma instância de Object.

A função ExtArr constrói novos vetores estendidos com os atributos contidos em
tools4Arrays. Quando tenta-se acessar a3.constructor o atribulo não é
encontrado no próprio objeto, depois segue pelo protótipo e em tools4Arrays
encontra e o retorna. Ao se buscar a3.map o atributo não é encontrado em
tools4Arrays e segue para Array.prototype.
Funções como map e filter geram novos objetos, eles seguem a cadeia protótipa
procurando por construtores que possuem o atributo especial Symbol.species que
aponta para a função construtora que deve ser usada. Sem configurações adicionais
o atributo Array[Symbol.species], que aponta para o próprio construtor Array,
e gera um novo vetor sem as os atributos extras de tools4Arrays. Para alterar
isso deve-se adicionar o construtor ExtArr o atributo Symbol.species
apontando para o construtor desejado:

// constructor.js
// (...)
ExtArr[Symbol.species] = ExtArr
console.log(`a3.filter has sum() = ${a3.filter(() => true).sum()}`)
console.log(`a3.map has sum() = ${a3.map(x => x).sum()}`)
Symbol.species pode apontar para qualquer outra função, neste caso as funções map e filter vão utilizarão o construtor pela mesma assinatura do Array, se for chamada com um argumento numérico n retorna um vetor vazio de comprimento n, se for de outro tipo ou múltiplos argumentos retorna um vetor com estes argumentos. Para atingir isso basta recolher e repassar os argumentos com o operador spread, da seguinte forma:
// mkExtArr.mjs
/*
** Array(3) = [ , , ]
** Array(3, 3, 3) = [3, 3, 3]
*/
// (...)
const ExtArr = function ExtArr (...n) {
return Object.setPrototypeOf(new Array(...n), megaPack)
}
// (...)
Empacotando tudo
Se quisermos incorporar diversas funções no protótipo extra podemos utilizar o Object.defineProperty com reduce da seguinte maneira:
// zipProperty.mjs
function zipProperty (proto, toolsBag) {
return Object.entries(toolsBag).reduce((a, [key, value]) => Object.defineProperty(
a,
key,
{ value,'enumerable': true }
), proto)
}
Assim refatoramos a função que estende os vetor em javascript com novos atributos para apenas:
// mkExtArr.mjs
function mkExtArr (toolsBag) {
const tools4Arrays = Object.defineProperty(
zipProperty(Object.getPrototypeOf([]), toolsBag),
'constructor',
{
'value': ExtArr
}
)
function ExtArr (...n) {
return Object.setPrototypeOf(new Array(...n), tools4Arrays)
}
ExtArr.prototype = tools4Arrays
ExtArr[Symbol.species] = ExtArr
return ExtArr
}
Conclusão
Esta técnica é equivalente a Subclassing Arrays e com Symbol.species para
restaurar o funcionamento dos map. Veja mais aqui e aqui.
Em outas linguagens há algum conceito de dados que é transversal a toda a sintaxe,
como listas para lisp ou objetos para scala. Os tipos especializados de objetos
em javascript, como Arrays ou Set, possui uma representação interna otimizada,
que mesmo sendo instâncias de objetos, intimida a busca por uma superfície suave
de especialização. Ainda por cima as classes vem bagunçar conceitualmente tudo
ainda mais.
Para incorporar o código pode utilizar algo assim:
<script type="module">
import mkExtArr from 'https://raw.githubusercontent.com/itacirgabral/blog/master/6/mkExtArr.mjs'
const SArray = mkExtArr({
'isNumeric': function isNumeric () {
return this.every(Number.isFinite)
},
'sum': function sum () {
return this.isNumeric() ? this.reduce((a, b) => a + b, 0) : new TypeError('should be stric numeric')
},
'average': function average () {
return this.isNumeric() ? this.sum() / this.length : new TypeError('should be stric numeric')
}
})
window.SArray = SArray
console.log(`statisticalArray(1, 2, 3).average() = ${statisticalArray(1, 2, 3).average()}`)
</script>
Ou visitar o github