자바 스크립트에서 범위 만들기-이상한 구문


129

es-discuss 메일 링리스트에서 다음 코드를 실행했습니다.

Array.apply(null, { length: 5 }).map(Number.call, Number);

이것은 생산

[0, 1, 2, 3, 4]

이것이 왜 코드의 결과입니까? 여기서 무슨 일이야?


2
Array.apply(null, Array(30)).map(Number.call, Number)일반 객체가 배열 인 것처럼 가장하지 않기 때문에 IMO 를 읽기가 더 쉽습니다.
fncomp

10
@fncomp 실제로 범위를 생성하는 데 사용하지 마십시오 . 간단한 접근 방법보다 느릴뿐만 아니라 이해하기가 쉽지 않습니다. 여기서 흥미로운 질문이지만 끔찍한 생산 코드 IMO를 만드는 구문 (구문이 아니라 API)을 이해하기는 어렵습니다.
Benjamin Gruenbaum

그렇습니다. 누군가가 그것을 사용하라고 제안하지는 않지만 객체 리터럴 버전에 비해 읽기가 더 쉽다고 생각했습니다.
fncomp

1
왜 누군가가 이것을하고 싶어하는지 모르겠습니다. 시간은 그것은이 방법은 약간 덜 섹시 할 수 있었다 배열하지만 훨씬 빠른 방법을 생성하는 데 걸리는 : jsperf.com/basic-vs-extreme
에릭 Hodonsky

답변:


263

이 "핵"을 이해하려면 몇 가지 사항을 이해해야합니다.

  1. 왜 우리가하지 않는 이유 Array(5).map(...)
  2. Function.prototype.apply인수를 처리 하는 방법
  3. Array여러 인수를 처리하는 방법
  4. Number함수가 인수를 처리 하는 방법
  5. 무엇 Function.prototype.call합니까

그것들은 자바 스크립트의 고급 주제이므로 다소 길어질 것입니다. 위에서부터 시작하겠습니다. 안전 벨트 매세요!

1. 왜 안돼 Array(5).map?

실제로 배열은 무엇입니까? 정수 키를 포함하고 값에 매핑되는 일반 객체입니다. 그것은 마법의 length변수와 같은 다른 특별한 기능을 가지고 있지만 핵심 key => value은 다른 객체와 마찬가지로 규칙적인 맵입니다. 우리는 배열을 조금 가지고 놀자.

var arr = ['a', 'b', 'c'];
arr.hasOwnProperty(0); //true
arr[0]; //'a'
Object.keys(arr); //['0', '1', '2']
arr.length; //3, implies arr[3] === undefined

//we expand the array by 1 item
arr.length = 4;
arr[3]; //undefined
arr.hasOwnProperty(3); //false
Object.keys(arr); //['0', '1', '2']

우리는 배열의 항목 arr.length수와 key=>value배열이 가진 매핑 수 사이에 고유 한 차이를 얻 습니다 arr.length.

를 통해 배열을 확장 해도 새로운 매핑이 생성 arr.length 되지 않으므로key=>value 배열에 정의되지 않은 값이없고이 키가 없습니다 . 존재하지 않는 속성에 액세스하려고하면 어떻게됩니까? 당신은 얻을 undefined.

이제 머리를 약간 들어 올려 왜 같은 기능 arr.map이 이러한 속성을 거치지 않는지 볼 수 있습니다. 경우 arr[3]단순히 정의되지 않은, 그리고 키가 존재하고, 모든 배열 함수는 다른 값처럼 이상 갈 것입니다 :

//just to remind you
arr; //['a', 'b', 'c', undefined];
arr.length; //4
arr[4] = 'e';

arr; //['a', 'b', 'c', undefined, 'e'];
arr.length; //5
Object.keys(arr); //['0', '1', '2', '4']

arr.map(function (item) { return item.toUpperCase() });
//["A", "B", "C", undefined, "E"]

의도적으로 메소드 호출을 사용하여 키 자체가 결코 존재하지 않았다는 점을 추가로 입증했습니다. 호출 undefined.toUpperCase하면 오류가 발생했지만 그렇지 않았습니다. 그것을 증명하기 위해 :

arr[5] = undefined;
arr; //["a", "b", "c", undefined, "e", undefined]
arr.hasOwnProperty(5); //true
arr.map(function (item) { return item.toUpperCase() });
//TypeError: Cannot call method 'toUpperCase' of undefined

이제 우리는 내 요점에 도달합니다 Array(N). 15.4.2.2 절 에 프로세스가 설명되어 있습니다. 우리가 신경 쓰지 않는 많은 점보 점보가 있지만 줄 사이를 읽을 수 있다면 (또는 이것을 믿을 수는 있지만 모르는 경우) 기본적으로 다음과 같이 요약됩니다.

function Array(len) {
    var ret = [];
    ret.length = len;
    return ret;
}

( len임의의 값이 아니라 유효한 uint32 인 가정 (실제 사양에서 확인 됨)에서 작동 함 )

이제 왜 Array(5).map(...)작동하지 않는지 알 수 있습니다 len. 배열에서 항목을 정의 하지 않고 key => value매핑을 만들지 않고 단순히 length속성을 변경합니다 .

이제 우리는 그것을 막았으므로 두 번째 마술을 살펴 보겠습니다.

2. Function.prototype.apply작동 원리

어떤 apply일은 기본적으로 배열을하고, 함수 호출의 인수로 풀다된다. 즉, 다음은 거의 동일합니다.

function foo (a, b, c) {
    return a + b + c;
}
foo(0, 1, 2); //3
foo.apply(null, [0, 1, 2]); //3

이제 특수 변수를 apply간단히 기록하여 작동 방식을 쉽게 확인할 수 있습니다 arguments.

function log () {
    console.log(arguments);
}

log.apply(null, ['mary', 'had', 'a', 'little', 'lamb']);
 //["mary", "had", "a", "little", "lamb"]

//arguments is a pseudo-array itself, so we can use it as well
(function () {
    log.apply(null, arguments);
})('mary', 'had', 'a', 'little', 'lamb');
 //["mary", "had", "a", "little", "lamb"]

//a NodeList, like the one returned from DOM methods, is also a pseudo-array
log.apply(null, document.getElementsByTagName('script'));
 //[script, script, script, script, script, script, script, script, script, script, script, script, script, script, script, script, script, script, script, script]

//carefully look at the following two
log.apply(null, Array(5));
//[undefined, undefined, undefined, undefined, undefined]
//note that the above are not undefined keys - but the value undefined itself!

log.apply(null, {length : 5});
//[undefined, undefined, undefined, undefined, undefined]

마지막 두 번째 예에서 내 주장을 쉽게 증명할 수 있습니다.

function ahaExclamationMark () {
    console.log(arguments.length);
    console.log(arguments.hasOwnProperty(0));
}

ahaExclamationMark.apply(null, Array(2)); //2, true

(예, 말장난 의도). key => value매핑은 우리가 넘어 갔다 배열에 존재하지 않았을 수 apply있지만, 확실히 존재하는 arguments변수입니다. 마지막 예제가 작동하는 것과 같은 이유입니다. 전달하는 객체에는 키가 없지만에 있습니다 arguments.

왜 그런 겁니까? 에서 살펴 보자 절 15.3.4.3 , Function.prototype.apply정의된다. 대부분 우리가 신경 쓰지 않는 것들이지만 흥미로운 부분은 다음과 같습니다.

  1. len은 인수 "length"로 argArray의 [[Get]] 내부 메소드를 호출 한 결과입니다.

기본적으로 다음을 의미 argArray.length합니다. 그런 다음 스펙은 항목에 대해 간단한 for루프 를 수행하여 해당 값을 length만듭니다 list( list일부 부두이지만 기본적으로 배열 임). 매우 느슨한 코드 측면에서 :

Function.prototype.apply = function (thisArg, argArray) {
    var len = argArray.length,
        argList = [];

    for (var i = 0; i < len; i += 1) {
        argList[i] = argArray[i];
    }

    //yeah...
    superMagicalFunctionInvocation(this, thisArg, argList);
};

따라서이 argArray경우 모방에 필요한 것은 length속성 이있는 객체입니다 . 이제 값이 정의되지 않은 이유를 알 수 있지만 키가 설정되어 있지 않은 경우 arguments: key=>value매핑을 만듭니다 .

휴, 그래서 이것은 이전 부분보다 짧지 않았을 것입니다. 그러나 우리가 끝나면 케이크가 생길 것이므로 인내심을 가지십시오! 그러나 다음 섹션 (짧은 약속) 후에 표현을 해부 할 수 있습니다. 잊어 버린 경우 문제는 다음과 같은 작동 방식입니다.

Array.apply(null, { length: 5 }).map(Number.call, Number);

3. Array여러 인수를 처리하는 방법

그래서! 우리는에 length인수를 전달할 때 어떤 일이 발생하는지 보았지만 Array표현식에서 여러 가지를 인수 ( undefined정확하게 는 5의 배열 )로 전달합니다. 15.4.2.1 절 에 수행 할 작업이 나와 있습니다. 마지막 단락은 우리에게 중요한 전부이며, 정말 이상하게 표현 되지만, 다음과 같이 요약됩니다.

function Array () {
    var ret = [];
    ret.length = arguments.length;

    for (var i = 0; i < arguments.length; i += 1) {
        ret[i] = arguments[i];
    }

    return ret;
}

Array(0, 1, 2); //[0, 1, 2]
Array.apply(null, [0, 1, 2]); //[0, 1, 2]
Array.apply(null, Array(2)); //[undefined, undefined]
Array.apply(null, {length:2}); //[undefined, undefined]

타다! 정의되지 않은 여러 값의 배열을 가져 와서 정의되지 않은 값의 배열을 반환합니다.

표현의 첫 부분

마지막으로 다음을 해독 할 수 있습니다.

Array.apply(null, { length: 5 })

우리는 5 개의 정의되지 않은 값을 가진 배열을 반환하고 키가 모두 존재한다는 것을 알았습니다.

이제 표현의 두 번째 부분으로

[undefined, undefined, undefined, undefined, undefined].map(Number.call, Number)

잘 알려지지 않은 해킹에 크게 의존하지 않기 때문에이 방법은 더 쉽고 복잡하지 않은 부분입니다.

4. Number입력을 다루는 방법

Number(something)( 섹션 15.7.1 )을 수행하면 something숫자 로 변환 되며 그게 전부입니다. 그 방법은 특히 문자열의 경우 약간 복잡하지만 관심이있는 경우 섹션 9.3 에 작업이 정의되어 있습니다.

5. 게임 Function.prototype.call

callapply정의의 형제, 섹션 15.3.4.4는 . 인수 배열을 취하는 대신 수신 된 인수를 가져 와서 전달합니다.

둘 이상의 체인을 연결 call하면 이상한 일이 발생합니다.

function log () {
    console.log(this, arguments);
}
log.call.call(log, {a:4}, {a:5});
//{a:4}, [{a:5}]
//^---^  ^-----^
// this   arguments

이것은 당신이 무슨 일이 일어나고 있는지 파악할 때까지 가치가 있습니다. log.call다른 함수의 call메소드 와 동등한 함수일 뿐이며 call그 자체로 메소드도 있습니다.

log.call === log.call.call; //true
log.call === Function.call; //true

그리고 무엇을 call합니까? 그것은 thisArg많은 인수를 받아들이고 부모 함수를 호출합니다. 우리는 그것을 통해 정의 할 수 있습니다 apply (다시 느슨한 코드는 작동하지 않습니다).

Function.prototype.call = function (thisArg) {
    var args = arguments.slice(1); //I wish that'd work
    return this.apply(thisArg, args);
};

이것이 어떻게 진행되는지 추적합시다.

log.call.call(log, {a:4}, {a:5});
  this = log.call
  thisArg = log
  args = [{a:4}, {a:5}]

  log.call.apply(log, [{a:4}, {a:5}])

    log.call({a:4}, {a:5})
      this = log
      thisArg = {a:4}
      args = [{a:5}]

      log.apply({a:4}, [{a:5}])

후반부 또는 .map전부

아직 끝나지 않았습니다. 대부분의 배열 메소드에 함수를 제공하면 어떻게되는지 보자 :

function log () {
    console.log(this, arguments);
}

var arr = ['a', 'b', 'c'];
arr.forEach(log);
//window, ['a', 0, ['a', 'b', 'c']]
//window, ['b', 1, ['a', 'b', 'c']]
//window, ['c', 2, ['a', 'b', 'c']]
//^----^  ^-----------------------^
// this         arguments

직접 this인수를 제공하지 않으면 기본값은 window입니다. 콜백에 인수가 제공되는 순서를 기록하고 다시 11로 이상하게합시다.

arr.forEach(log.call, log);
//'a', [0, ['a', 'b', 'c']]
//'b', [1, ['a', 'b', 'c']]
//'b', [2, ['a', 'b', 'c']]
// ^    ^

우와 우와 우와 .. 조금 백업하자. 무슨 일이야? 우리가 볼 수있는 섹션 15.4.4.18 , forEach정의, 다음이 거의 발생 :

var callback = log.call,
    thisArg = log;

for (var i = 0; i < arr.length; i += 1) {
    callback.call(thisArg, arr[i], i, arr);
}

그래서 우리는 이것을 얻습니다.

log.call.call(log, arr[i], i, arr);
//After one `.call`, it cascades to:
log.call(arr[i], i, arr);
//Further cascading to:
log(i, arr);

이제 우리는 어떻게 .map(Number.call, Number)작동 하는지 볼 수 있습니다 :

Number.call.call(Number, arr[i], i, arr);
Number.call(arr[i], i, arr);
Number(i, arr);

i현재 인덱스 의 변환을 숫자로 반환합니다 .

결론적으로,

표현식

Array.apply(null, { length: 5 }).map(Number.call, Number);

두 부분으로 작동합니다.

var arr = Array.apply(null, { length: 5 }); //1
arr.map(Number.call, Number); //2

첫 번째 부분은 5 개의 정의되지 않은 항목으로 구성된 배열을 만듭니다. 두 번째는 해당 배열을 넘고 인덱스를 가져 와서 요소 인덱스 배열을 만듭니다.

[0, 1, 2, 3, 4]

@Zirak 다음을 이해하도록 도와주세요 ahaExclamationMark.apply(null, Array(2)); //2, true. 이유는 반환하지 2true각각? Array(2)여기서 단 하나의 논쟁 만 전달하지 않습니까?
Geek

4
@Geek에 하나의 인수 만 전달 apply하지만 해당 인수는 함수에 전달 된 두 개의 인수로 "분배"됩니다. 첫 번째 apply예제 에서 더 쉽게 알 수 있습니다 . 첫 번째 console.log는 실제로 두 개의 인수 (두 배열 항목)를 받았으며 두 번째 console.log는 배열이 key=>value첫 번째 슬롯에 매핑되어 있음을 보여줍니다 (답의 첫 번째 부분에서 설명).
Zirak

4
(일부) 요청 으로 인해 이제 오디오 버전을 즐길 수 있습니다. dl.dropboxusercontent.com/u/24522528/SO-answer.mp3
Zirak

1
호스트 객체 인 NodeList를 기본 메소드에 전달 log.apply(null, document.getElementsByTagName('script'));하면 작동하지 않아도되고 일부 브라우저에서는 작동하지 않으며 [].slice.call(NodeList)NodeList를 배열로 전환해도 작동하지 않습니다.
RobG

2
한 번의 수정 : this기본값은 Window엄격하지 않은 모드입니다.
ComFreek

21

면책 조항 : 이것은 위의 코드에 대한 매우 공식적인 설명입니다. 이것이 내가 그것을 설명하는 방법입니다. 더 간단한 답변을 원하면 위의 Zirak의 훌륭한 답변을 확인하십시오. 이것은 당신의 얼굴에 더 깊이 있고 더 적은 "aha"입니다.


몇 가지 일이 여기서 일어나고 있습니다. 조금 해보자.

var arr = Array.apply(null, { length: 5 }); // Create an array of 5 `undefined` values

arr.map(Number.call, Number); // Calculate and return a number based on the index passed

첫 번째 줄 에서 배열 생성자는을 사용하여 함수호출됩니다Function.prototype.apply .

  • this값은 nullArray 생성자 (문제가되지 않는 것이 어떤 this동일하다 this15.3.4.3.2.a.에 따른 문맥으로
  • 그런 다음 속성이 new Array있는 객체를 전달 length한다고합니다.이 객체 .apply는 다음 절의 다음과 같은 이유로 중요한 객체와 같은 배열이 됩니다 .apply.
    • len은 인수 "length"로 argArray의 [[Get]] 내부 메소드를 호출 한 결과입니다.
  • 이와 같이, .apply0에서 인수를 통과 .length호출 이후 [[Get]]{ length: 5 }값으로 0~4 수율 undefined배열 생성자 값이 다섯 개 인자로 호출한다 undefined(물체의 속성 미표시 점점).
  • 배열 생성자 는 0, 2 개 이상의 인수로 호출 됩니다. 새로 구성된 배열의 길이 속성은 사양에 따른 인수 수와 값이 같은 값으로 설정됩니다.
  • 따라서 var arr = Array.apply(null, { length: 5 });5 개의 정의되지 않은 값의 목록이 작성됩니다.

: 여기 공지 간의 차이 Array.apply(0,{length: 5})Array(5), 제 작성 다섯 번 프리미티브 값 유형 undefined, 즉 길이 (5)의 빈 어레이를 생성하고, 후자 인해 .map동작 (8.b) S ' 구체적와 [[HasProperty].

따라서 규격에 맞는 위의 코드는 다음과 같습니다.

var arr = [undefined, undefined, undefined, undefined, undefined];
arr.map(Number.call, Number); // Calculate and return a number based on the index passed

이제 두 번째 부분으로 넘어갑니다.

  • Array.prototype.mapNumber.call배열의 각 요소 에서 콜백 함수 (이 경우 )를 호출하고 지정된 this값 (이 경우 this값을`Number로 설정 )을 사용합니다.
  • 지도에서 콜백의 두 번째 매개 변수 (이 경우 Number.call)는 색인이고 첫 번째는 this 값입니다.
  • 이는 as (배열 값) 및 인덱스를 매개 변수로 사용하여 Number호출 됨을 의미합니다 . 따라서 기본적으로 배열 인덱스에 각각 매핑하는 것과 같습니다 ( 이 경우 인덱스를 변경하지 않고 숫자에서 숫자로 유형 변환을 수행하기 때문에 ).thisundefinedundefinedNumber

따라서 위의 코드는 5 개의 정의되지 않은 값을 가져와 각각 배열의 해당 인덱스에 매핑합니다.

우리가 결과를 코드로 얻는 이유입니다.


1
문서 :지도 작동 방식에 대한 사양 : es5.github.io/#x15.4.4.19 에는 Mozilla에 developer.mozilla.org/en-US/docs/Web/JavaScript/의
패트릭 에반스

1
그러나 왜 길이 값으로 전달 되는 생성자를 호출 Array.apply(null, { length: 2 })하지 않을까요? 바이올린Array.apply(null, [2])Array2
Andreas

@Andreas Array.apply(null,[2])는 프리미티브 값을 두 번 포함하는 배열이 아닌 길이 2 Array(2) 배열 을 만드는 것과 같습니다 . 첫 번째 부분 이후의 메모에서 가장 최근의 편집 내용을 참조하십시오. 충분히 명확하고 명확하지 않은 경우 알려주십시오. undefined
Benjamin Gruenbaum

나는 첫 번째 실행에서 작동하는 방식을 이해하지 못했습니다 ... 두 번째 독서 후 의미가 있습니다. 생성자가 새로 만든 배열에 삽입 할 {length: 2}두 요소로 배열을 가짜로 Array만듭니다. 존재하지 않는 요소 수에 액세스하는 실제 배열이 없으므로 undefined삽입됩니다. 좋은 트릭 :)
Andreas

5

말했듯이 첫 번째 부분 :

var arr = Array.apply(null, { length: 5 }); 

5 개의 undefined값으로 구성된 배열을 만듭니다 .

두 번째 부분은 map2 개의 인수를 사용하고 동일한 크기의 새 배열을 반환하는 배열 의 함수를 호출하는 것입니다.

첫 번째 인수 map는 실제로 배열의 각 요소에 적용되는 함수이며, 3 개의 인수를 가져 와서 값을 반환하는 함수일 것으로 예상됩니다. 예를 들면 다음과 같습니다.

function foo(a,b,c){
    ...
    return ...
}

foo 함수를 첫 번째 인수로 전달하면 다음과 같이 각 요소에 대해 호출됩니다.

  • 현재 반복 된 요소의 값으로 a
  • 현재 반복 된 요소의 색인으로 b
  • 전체 원래 배열로 c

두 번째 인수 map는 첫 번째 인수로 전달한 함수에 전달됩니다. 그러나의 경우 A, B,도 C되지 않을 것 foo, 그것이 될 것입니다 this.

두 가지 예 :

function bar(a,b,c){
    return this
}
var arr2 = [3,4,5]
var newArr2 = arr2.map(bar, 9);
//newArr2 is equal to [9,9,9]

function baz(a,b,c){
    return b
}
var newArr3 = arr2.map(baz,9);
//newArr3 is equal to [0,1,2]

또 다른 하나는 더 명확하게하기 위해 :

function qux(a,b,c){
    return a
}
var newArr4 = arr2.map(qux,9);
//newArr4 is equal to [3,4,5]

그렇다면 Number.call은 어떻습니까?

Number.call 는 2 개의 인수를 취하고 두 번째 인수를 숫자로 구문 분석하려고하는 함수입니다 (첫 번째 인수로 무엇을하는지 확실하지 않습니다).

map전달 되는 두 번째 인수 는 인덱스이므로 해당 인덱스의 새 배열에 배치 될 값은 인덱스와 같습니다. baz위 예제 의 함수와 같습니다 . Number.call인덱스를 구문 분석하려고 시도합니다. 자연스럽게 동일한 값을 반환합니다.

map코드 에서 함수에 전달한 두 번째 인수 는 실제로 결과에 영향을 미치지 않습니다. 내가 틀렸다면 정정 해주세요.


1
Number.call인수를 숫자로 구문 분석하는 특수 함수는 아닙니다. 그냥 === Function.prototype.call입니다. 만 두 번째 인수는으로 전달되는 기능 this에 -value는 call, 관련 - .map(eval.call, Number), .map(String.call, Number)그리고 .map(Function.prototype.call, Number)모두 동일합니다.
Bergi

0

배열은 단순히 '길이'필드와 일부 방법 (예 : 푸시)을 포함하는 객체입니다. 따라서 arr in var arr = { length: 5}은 기본적으로 필드 0..4가 정의되지 않은 기본값을 갖는 배열과 같습니다 (즉, arr[0] === undefined참).
두 번째 부분은 이름에서 알 수 있듯이 map은 한 배열에서 새로운 배열로 매핑됩니다. 원래 배열을 통과하고 각 항목에 대해 매핑 기능을 호출하여 그렇게합니다.

남은 것은 mapping-function의 결과가 인덱스임을 확신시키는 것입니다. 트릭은 첫 번째 매개 변수가 'this'컨텍스트로 설정되고 두 번째 매개 변수가 첫 번째 매개 변수가되는 예외를 제외하고는 함수를 호출하는 'call'(*)이라는 메서드를 사용하는 것입니다. 우연히도, 맵핑 기능이 호출 될 때 두 번째 매개 변수는 색인입니다.

마지막으로, 호출되는 메소드는 숫자 "클래스"이며, JS에서 알 수 있듯이 "클래스"는 단순히 함수이며이 숫자 (숫자)는 첫 번째 매개 변수가 값이 될 것으로 예상합니다.

(*)는 함수의 프로토 타입에 있으며 숫자는 함수입니다.

마시


1
사이에 큰 차이가있다 [undefined, undefined, undefined, …]new Array(n)또는 {length: n}- 후자의 사람은 드문 드문 그들은 더 요소가 존재하지 않는, 즉. 이것은에 매우 관련이 있으며 map, 이것이 확률 Array.apply이 사용 된 이유 입니다.
Bergi
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.