Я наткнулся на следующий код в списке рассылки es-Disc:
Array.apply(null, { length: 5 }).map(Number.call, Number);
Это производит
[0, 1, 2, 3, 4]
Почему это результат кода? Что тут происходит?
javascript
ecmascript-5
Бенджамин Грюнбаум
источник
источник
Array.apply(null, Array(30)).map(Number.call, Number)
легче читать, поскольку он избегает притворяться, что простой объект является массивом.Ответы:
Чтобы понять этот «хакерский прием», необходимо понять несколько вещей:
Array(5).map(...)
Function.prototype.apply
обрабатывает аргументыArray
обрабатывает несколько аргументовNumber
функция обрабатывает аргументыFunction.prototype.call
значитЭто довольно сложные темы в javascript, так что это будет более чем довольно долго. Начнем сверху. Пристегнитесь!
1. Почему не просто
Array(5).map
?Что на самом деле за массив? Обычный объект, содержащий целочисленные ключи, которые соответствуют значениям. У него есть и другие особенности, например, волшебная
length
переменная, но по сути это обычнаяkey => value
карта, как и любой другой объект. Давай немного поиграем с массивами?Мы подходим к внутренней разнице между количеством элементов в массиве
arr.length
, и количествомkey=>value
отображений в массиве, которое может отличаться отarr.length
.Расширение массива с помощью
arr.length
не создает никаких новыхkey=>value
сопоставлений, поэтому это не значит, что массив имеет неопределенные значения, у него нет этих ключей . А что происходит, когда вы пытаетесь получить доступ к несуществующему свойству? Вы получитеundefined
.Теперь мы можем немного приподнять голову и понять, почему функции вроде
arr.map
не обходят эти свойства. Если бы онarr[3]
был просто undefined и ключ существовал, все эти функции массива просто перебирали бы его, как любое другое значение:Я намеренно использовал вызов метода, чтобы еще раз доказать, что самого ключа никогда не было: вызов
undefined.toUpperCase
вызвал бы ошибку, но этого не произошло. Чтобы доказать это :А теперь мы подошли к моей мысли: как
Array(N)
дела. В разделе 15.4.2.2 описан процесс. Есть куча ерунды, которая нас не волнует, но если вам удастся читать между строк (или вы можете просто доверять мне в этом, но не делайте этого), все сводится к следующему:(работает в предположении (которое проверяется в фактической спецификации), что
len
это действительный uint32, а не просто любое количество значений)Итак, теперь вы можете понять, почему это
Array(5).map(...)
не сработает - мы не определяемlen
элементы в массиве, мы не создаемkey => value
сопоставления, мы просто изменяемlength
свойство.Теперь, когда у нас это есть, давайте посмотрим на вторую волшебную вещь:
2. Как
Function.prototype.apply
работаетПо
apply
сути, он берет массив и разворачивает его как аргументы вызова функции. Это означает, что следующие вещи практически одинаковы:Теперь мы можем упростить процесс просмотра, как
apply
работает, просто зарегистрировавarguments
специальную переменную:Мое утверждение легко доказать на предпоследнем примере:
(да, каламбур).
key => value
Отображение не может существовать в массиве мы прошли кapply
, но это , конечно , существует вarguments
переменной. По той же причине, по которой работает последний пример: ключи не существуют в переданном нами объекте, но они существуютarguments
.Это почему? Давайте посмотрим на Раздел 15.3.4.3 , где
Function.prototype.apply
определено. В основном нас не волнуют вещи, но вот что интересно:Который в основном означает:
argArray.length
. Затем спецификация переходит к простомуfor
циклу поlength
элементам, создаваяlist
соответствующие значения (list
это какое-то внутреннее вуду, но в основном это массив). С точки зрения очень и очень свободного кода:Итак, все, что нам нужно для имитации
argArray
в этом случае, - это объект соlength
свойством. И теперь мы можем видеть, почему значения не определены, а ключи нетarguments
. Мы создаемkey=>value
сопоставления.Уф, так что это могло быть не короче предыдущей. Но когда мы закончим, будет торт, так что наберитесь терпения! Однако после следующего раздела (который, я обещаю, будет коротким) мы можем приступить к анализу выражения. Если вы забыли, вопрос был в том, как работает следующее:
3. Как
Array
обрабатывает несколько аргументовТак! Мы видели, что происходит, когда вы передаете
length
аргументArray
, но в выражении мы передаем несколько вещей как аргументы (еслиundefined
быть точным, массив из 5 ). В разделе 15.4.2.1 говорится, что делать. Последний абзац - это все, что имеет для нас значение, и он очень странно сформулирован , но все сводится к следующему:Тада! Мы получаем массив из нескольких неопределенных значений и возвращаем массив этих неопределенных значений.
Первая часть выражения
Наконец, мы можем расшифровать следующее:
Мы видели, что он возвращает массив, содержащий 5 неопределенных значений со всеми существующими ключами.
Теперь перейдем ко второй части выражения:
Это будет более легкая и понятная часть, поскольку она не так сильно полагается на малоизвестные хаки.
4. Как
Number
обрабатывается вводВыполнение
Number(something)
( раздел 15.7.1 ) преобразуетсяsomething
в число, вот и все. Как это происходит, немного запутано, особенно в случае строк, но на случай, если вам интересно, операция определена в разделе 9.3 .5. Игры
Function.prototype.call
call
являетсяapply
братом, определенным в разделе 15.3.4.4 . Вместо того, чтобы принимать массив аргументов, он просто берет полученные аргументы и передает их вперед.Все становится интереснее, когда вы соединяете более одного
call
вместе, увеличивая количество странностей до 11:Это вполне достойно, пока вы не поймете, что происходит.
log.call
- это просто функция, эквивалентнаяcall
методу любой другой функции , и поэтомуcall
сама по себе также имеет метод:А что
call
делать? Он принимаетthisArg
несколько аргументов и вызывает свою родительскую функцию. Мы можем определить его черезapply
(опять же, очень свободный код, не сработает):Давайте проследим, как это происходит:
Поздняя часть или
.map
все этоЭто еще не конец. Давайте посмотрим, что произойдет, если вы предоставите функцию большинству методов массива:
Если мы сами не приводим
this
аргумент, по умолчанию используетсяwindow
. Обратите внимание на порядок, в котором аргументы передаются нашему обратному вызову, и давайте снова увеличим его до 11:Эй, эй, эй ... давай вернемся немного назад. Что тут происходит? Мы можем видеть в разделе 15.4.4.18 , где
forEach
определено, в значительной степени происходит следующее:Итак, получаем вот что:
Теперь мы можем увидеть, как
.map(Number.call, Number)
работает:Что возвращает преобразование
i
текущего индекса в число.В заключение,
Выражение
Работает в двух частях:
Первая часть создает массив из 5 неопределенных элементов. Второй проходит по этому массиву и берет его индексы, в результате получается массив индексов элементов:
источник
ahaExclamationMark.apply(null, Array(2)); //2, true
. Почему возвращается2
иtrue
соответственно? Разве вы не передаете только один аргумент, т.е.Array(2)
здесь?apply
, но этот аргумент "разбивается" на два аргумента, передаваемых функции. На первыхapply
примерах это легче увидеть . Первый из нихconsole.log
показывает, что мы действительно получили два аргумента (два элемента массива), а второйconsole.log
показывает, что массив имеетkey=>value
отображение в 1-м слоте (как объясняется в 1-й части ответа).log.apply(null, document.getElementsByTagName('script'));
, не требуется для работы и не работает в некоторых браузерах, а[].slice.call(NodeList)
преобразование NodeList в массив также не будет работать в них.this
умолчанию используется толькоWindow
нестрогий режим.Отказ от ответственности : это очень формальное описание приведенного выше кода - вот как я знаю, как его объяснить. Для более простого ответа - посмотрите отличный ответ Зирака выше. Это более подробное описание вашего лица и меньше «ага».
Здесь происходит несколько вещей. Давайте немного разберемся.
В первой строке конструктор массива вызывается как функция с
Function.prototype.apply
.this
значениеnull
не имеет значения для конструктора массива (this
такое же,this
как в контексте согласно 15.3.4.3.2.a.new Array
вызывается передача объекта соlength
свойством, что приводит к тому, что этот объект становится массивом, как и все, что имеет значение,.apply
из-за следующего предложения в.apply
:.apply
проходят аргументы от 0 до.length
, так как вызова[[Get]]
на{ length: 5 }
со значениями от 0 до 4 выходовundefined
конструктора массива вызывается с пятью аргументов , которые имеют значениеundefined
(получение необъявленного свойства объекта).var arr = Array.apply(null, { length: 5 });
создается список из пяти неопределенных значений.Примечание . Обратите внимание на разницу между
Array.apply(0,{length: 5})
иArray(5)
: первая создает в пять раз больший тип примитивного значения,undefined
а вторая создает пустой массив длиной 5. В частности, из-за.map
поведения (8.b) и в частности[[HasProperty]
.Таким образом, приведенный выше код в соответствующей спецификации такой же, как:
А теперь перейдем ко второй части.
Array.prototype.map
вызывает функцию обратного вызова (в данном случаеNumber.call
) для каждого элемента массива и использует указанноеthis
значение (в данном случае устанавливаяthis
значение на `Number).Number.call
) - это индекс, а первый - это значение.Number
вызывается сthis
asundefined
(значением массива) и индексом в качестве параметра. Таким образом, это в основном то же самое, что сопоставление каждогоundefined
с его индексом массива (поскольку при вызовеNumber
выполняется преобразование типа, в данном случае от числа к числу, не изменяя индекс).Таким образом, приведенный выше код принимает пять неопределенных значений и сопоставляет каждое со своим индексом в массиве.
Вот почему мы получаем результат в нашем коде.
источник
Array.apply(null, { length: 2 })
и неArray.apply(null, [2])
которые также вызватьArray
конструктор проходящий в2
качестве значения длины? fiddleArray.apply(null,[2])
похож наArray(2)
который создает пустой массив длиной 2, а не массив, содержащий примитивное значениеundefined
два раза. Смотрите мое последнее изменение в примечании после первой части, дайте мне знать, достаточно ли оно ясно, а если нет, я проясню это.{length: 2}
подделывает массив с двумя элементами, которыеArray
конструктор вставляет во вновь созданный массив. Поскольку нет реального массива, обращающегося к отсутствующим элементам, вы получаете,undefined
который затем вставляется. Хороший трюк :)Как вы сказали, первая часть:
создает массив из 5
undefined
значений.Вторая часть вызывает
map
функцию массива, которая принимает 2 аргумента и возвращает новый массив того же размера.Первый аргумент, который
map
принимает, на самом деле является функцией, применяемой к каждому элементу в массиве, ожидается, что это будет функция, которая принимает 3 аргумента и возвращает значение. Например:если мы передадим функцию foo в качестве первого аргумента, она будет вызываться для каждого элемента с
Второй аргумент, который
map
принимает, передается функции, которую вы передаете как первый аргумент. Но это не будет ни a, ни b, ни c в случаеfoo
, это будетthis
.Два примера:
и еще один, чтобы было понятнее:
Так что насчет Number.call?
Number.call
- это функция, которая принимает 2 аргумента и пытается преобразовать второй аргумент в число (я не уверен, что она делает с первым аргументом).Поскольку второй
map
передаваемый аргумент - это индекс, значение, которое будет помещено в новый массив по этому индексу, равно индексу. Так же, как функцияbaz
в примере выше.Number.call
попытается проанализировать индекс - он, естественно, вернет то же значение.Второй аргумент, который вы передали
map
функции в своем коде, на самом деле не влияет на результат. Поправьте меня, если я ошибаюсь, пожалуйста.источник
Number.call
не является специальной функцией, которая анализирует аргументы для чисел. Это просто=== Function.prototype.call
. Только второй аргумент, функция, которая передается какthis
-значениеcall
, является релевантным -.map(eval.call, Number)
,.map(String.call, Number)
и.map(Function.prototype.call, Number)
все они эквивалентны.Массив - это просто объект, содержащий поле «длина» и некоторые методы (например, push). Таким образом, arr in
var arr = { length: 5}
в основном совпадает с массивом, в котором поля 0..4 имеют значение по умолчанию, которое не определено (т.е.arr[0] === undefined
дает true).Что касается второй части, map, как следует из названия, отображает один массив в новый. Это достигается путем обхода исходного массива и вызова функции сопоставления для каждого элемента.
Все, что осталось, - это убедить вас, что результатом функции сопоставления является индекс. Уловка состоит в том, чтобы использовать метод с именем 'call' (*), который вызывает функцию с небольшим исключением: первый параметр установлен как контекст 'this', а второй становится первым параметром (и так далее). По совпадению, когда вызывается функция отображения, второй параметр - это индекс.
И последнее, но не менее важное: вызываемый метод - это Number «Class», и, как мы знаем в JS, «Class» - это просто функция, и этот метод (Number) ожидает, что первый параметр будет значением.
(*) находится в прототипе функции (а Number - это функция).
Mashal
источник
[undefined, undefined, undefined, …]
иnew Array(n)
или{length: n}
- последние редки , то есть в них нет элементов. Это очень актуально дляmap
, и поэтомуArray.apply
было использовано нечетное число .