es-discuss 메일 링리스트에서 다음 코드를 실행했습니다.
Array.apply(null, { length: 5 }).map(Number.call, Number);
이것은 생산
[0, 1, 2, 3, 4]
이것이 왜 코드의 결과입니까? 여기서 무슨 일이야?
es-discuss 메일 링리스트에서 다음 코드를 실행했습니다.
Array.apply(null, { length: 5 }).map(Number.call, Number);
이것은 생산
[0, 1, 2, 3, 4]
이것이 왜 코드의 결과입니까? 여기서 무슨 일이야?
답변:
이 "핵"을 이해하려면 몇 가지 사항을 이해해야합니다.
Array(5).map(...)
Function.prototype.apply
인수를 처리 하는 방법Array
여러 인수를 처리하는 방법Number
함수가 인수를 처리 하는 방법Function.prototype.call
합니까그것들은 자바 스크립트의 고급 주제이므로 다소 길어질 것입니다. 위에서부터 시작하겠습니다. 안전 벨트 매세요!
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
속성을 변경합니다 .
이제 우리는 그것을 막았으므로 두 번째 마술을 살펴 보겠습니다.
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
정의된다. 대부분 우리가 신경 쓰지 않는 것들이지만 흥미로운 부분은 다음과 같습니다.
- 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);
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)
잘 알려지지 않은 해킹에 크게 의존하지 않기 때문에이 방법은 더 쉽고 복잡하지 않은 부분입니다.
Number
입력을 다루는 방법Number(something)
( 섹션 15.7.1 )을 수행하면 something
숫자 로 변환 되며 그게 전부입니다. 그 방법은 특히 문자열의 경우 약간 복잡하지만 관심이있는 경우 섹션 9.3 에 작업이 정의되어 있습니다.
Function.prototype.call
call
인 apply
정의의 형제, 섹션 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]
ahaExclamationMark.apply(null, Array(2)); //2, true
. 이유는 반환하지 2
및 true
각각? Array(2)
여기서 단 하나의 논쟁 만 전달하지 않습니까?
apply
하지만 해당 인수는 함수에 전달 된 두 개의 인수로 "분배"됩니다. 첫 번째 apply
예제 에서 더 쉽게 알 수 있습니다 . 첫 번째 console.log
는 실제로 두 개의 인수 (두 배열 항목)를 받았으며 두 번째 console.log
는 배열이 key=>value
첫 번째 슬롯에 매핑되어 있음을 보여줍니다 (답의 첫 번째 부분에서 설명).
log.apply(null, document.getElementsByTagName('script'));
하면 작동하지 않아도되고 일부 브라우저에서는 작동하지 않으며 [].slice.call(NodeList)
NodeList를 배열로 전환해도 작동하지 않습니다.
this
기본값은 Window
엄격하지 않은 모드입니다.
면책 조항 : 이것은 위의 코드에 대한 매우 공식적인 설명입니다. 이것이 내가 그것을 설명하는 방법입니다. 더 간단한 답변을 원하면 위의 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
값은 null
Array 생성자 (문제가되지 않는 것이 어떤 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 });
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.map
Number.call
배열의 각 요소 에서 콜백 함수 (이 경우 )를 호출하고 지정된 this
값 (이 경우 this
값을`Number로 설정 )을 사용합니다.Number.call
)는 색인이고 첫 번째는 this 값입니다.Number
호출 됨을 의미합니다 . 따라서 기본적으로 배열 인덱스에 각각 매핑하는 것과 같습니다 ( 이 경우 인덱스를 변경하지 않고 숫자에서 숫자로 유형 변환을 수행하기 때문에 ).this
undefined
undefined
Number
따라서 위의 코드는 5 개의 정의되지 않은 값을 가져와 각각 배열의 해당 인덱스에 매핑합니다.
우리가 결과를 코드로 얻는 이유입니다.
Array.apply(null,[2])
는 프리미티브 값을 두 번 포함하는 배열이 아닌 길이 2 Array(2)
의 빈 배열 을 만드는 것과 같습니다 . 첫 번째 부분 이후의 메모에서 가장 최근의 편집 내용을 참조하십시오. 충분히 명확하고 명확하지 않은 경우 알려주십시오. undefined
{length: 2}
두 요소로 배열을 가짜로 Array
만듭니다. 존재하지 않는 요소 수에 액세스하는 실제 배열이 없으므로 undefined
삽입됩니다. 좋은 트릭 :)
말했듯이 첫 번째 부분 :
var arr = Array.apply(null, { length: 5 });
5 개의 undefined
값으로 구성된 배열을 만듭니다 .
두 번째 부분은 map
2 개의 인수를 사용하고 동일한 크기의 새 배열을 반환하는 배열 의 함수를 호출하는 것입니다.
첫 번째 인수 map
는 실제로 배열의 각 요소에 적용되는 함수이며, 3 개의 인수를 가져 와서 값을 반환하는 함수일 것으로 예상됩니다. 예를 들면 다음과 같습니다.
function foo(a,b,c){
...
return ...
}
foo 함수를 첫 번째 인수로 전달하면 다음과 같이 각 요소에 대해 호출됩니다.
두 번째 인수 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
코드 에서 함수에 전달한 두 번째 인수 는 실제로 결과에 영향을 미치지 않습니다. 내가 틀렸다면 정정 해주세요.
Number.call
인수를 숫자로 구문 분석하는 특수 함수는 아닙니다. 그냥 === Function.prototype.call
입니다. 만 두 번째 인수는으로 전달되는 기능 this
에 -value는 call
, 관련 - .map(eval.call, Number)
, .map(String.call, Number)
그리고 .map(Function.prototype.call, Number)
모두 동일합니다.
배열은 단순히 '길이'필드와 일부 방법 (예 : 푸시)을 포함하는 객체입니다. 따라서 arr in var arr = { length: 5}
은 기본적으로 필드 0..4가 정의되지 않은 기본값을 갖는 배열과 같습니다 (즉, arr[0] === undefined
참).
두 번째 부분은 이름에서 알 수 있듯이 map은 한 배열에서 새로운 배열로 매핑됩니다. 원래 배열을 통과하고 각 항목에 대해 매핑 기능을 호출하여 그렇게합니다.
남은 것은 mapping-function의 결과가 인덱스임을 확신시키는 것입니다. 트릭은 첫 번째 매개 변수가 'this'컨텍스트로 설정되고 두 번째 매개 변수가 첫 번째 매개 변수가되는 예외를 제외하고는 함수를 호출하는 'call'(*)이라는 메서드를 사용하는 것입니다. 우연히도, 맵핑 기능이 호출 될 때 두 번째 매개 변수는 색인입니다.
마지막으로, 호출되는 메소드는 숫자 "클래스"이며, JS에서 알 수 있듯이 "클래스"는 단순히 함수이며이 숫자 (숫자)는 첫 번째 매개 변수가 값이 될 것으로 예상합니다.
(*)는 함수의 프로토 타입에 있으며 숫자는 함수입니다.
마시
[undefined, undefined, undefined, …]
및 new Array(n)
또는 {length: n}
- 후자의 사람은 드문 드문 그들은 더 요소가 존재하지 않는, 즉. 이것은에 매우 관련이 있으며 map
, 이것이 확률 Array.apply
이 사용 된 이유 입니다.
Array.apply(null, Array(30)).map(Number.call, Number)
일반 객체가 배열 인 것처럼 가장하지 않기 때문에 IMO 를 읽기가 더 쉽습니다.