การสร้างช่วงใน JavaScript - ไวยากรณ์แปลก ๆ


129

ฉันพบรหัสต่อไปนี้ในรายชื่อผู้รับจดหมาย es-discuss:

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

สิ่งนี้ผลิต

[0, 1, 2, 3, 4]

เหตุใดจึงเป็นผลลัพธ์ของรหัส เกิดอะไรขึ้นที่นี่


2
IMO Array.apply(null, Array(30)).map(Number.call, Number)อ่านง่ายกว่าเพราะหลีกเลี่ยงการแสร้งว่าวัตถุธรรมดาเป็น Array
fncomp

10
@fncomp กรุณาอย่าใช้ทั้งที่จริงสร้างช่วง ไม่เพียงช้ากว่าวิธีที่ตรงไปตรงมา - มันไม่ง่ายที่จะเข้าใจ เป็นการยากที่จะเข้าใจไวยากรณ์ (ดีจริง ๆ API และไม่ใช่ไวยากรณ์) ที่นี่ซึ่งทำให้คำถามนี้น่าสนใจ แต่ IMO ของรหัสการผลิตแย่มาก
Benjamin Gruenbaum

ใช่ไม่แนะนำให้ใครใช้ แต่คิดว่ามันยังง่ายต่อการอ่านเมื่อเทียบกับเวอร์ชันตามตัวอักษรของวัตถุ
fncomp

1
ฉันไม่แน่ใจว่าทำไมทุกคนต้องการทำเช่นนี้ ระยะเวลาที่ใช้ในการสร้างอาเรย์ด้วยวิธีนี้สามารถทำได้ในแบบเซ็กซี่น้อยลง แต่เร็วกว่ามาก: jsperf.com/basic-vs-extreme
Eric 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=>valuearr.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 เป็นผลลัพธ์ของการเรียกเมธอดภายใน [[Get]] ของ argArray ด้วยอาร์กิวเมนต์ "length"

ซึ่งโดยทั่วไปหมายถึง: 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แต่ในการแสดงออกเราผ่านหลายสิ่งเป็นข้อโต้แย้ง (อาร์เรย์ 5 undefinedเพื่อให้แน่นอน) ส่วนที่ 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]

Tada! เราได้รับอาร์เรย์ของค่าที่ไม่ได้กำหนดจำนวนมากและเราส่งคืนอาร์เรย์ของค่าที่ไม่ได้กำหนดเหล่านี้

ส่วนแรกของการแสดงออก

สุดท้ายเราสามารถถอดรหัสต่อไปนี้:

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

callเป็นapplyของพี่ชายที่กำหนดไว้ในมาตรา 15.3.4.4 แทนที่จะใช้อาเรย์ของอาร์กิวเมนต์มันแค่รับอาร์กิวเมนต์ที่ได้รับและส่งต่อไป

สิ่งต่าง ๆ ที่น่าสนใจเมื่อคุณcallโยงโซ่เข้าด้วยกันมากกว่าหนึ่งข้อเหวี่ยงสิ่งแปลกประหลาดถึง 11:

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โปรดช่วยฉันในการทำความเข้าใจต่อไปนี้ ทำไมมันกลับมา2และtrueตามลำดับ? คุณไม่ผ่านการโต้แย้งเพียงครั้งเดียวนั่นคือArray(2)ที่นี่หรือ
Geek

4
@Geek เราส่งอาร์กิวเมนต์เพียงหนึ่งตัวapplyเท่านั้น แต่อาร์กิวเมนต์นั้นคือ "splatted" เป็นสองอาร์กิวเมนต์ที่ส่งผ่านไปยังฟังก์ชัน คุณสามารถดูได้ง่ายขึ้นในapplyตัวอย่างแรก ครั้งแรกconsole.logแสดงให้เห็นว่าแน่นอนเราได้รับสองข้อโต้แย้ง (รายการอาร์เรย์สองรายการ) และรายการที่สองconsole.logแสดงให้เห็นว่าอาร์เรย์มีการkey=>valueแมปในช่องที่ 1 (ตามที่อธิบายไว้ในส่วนที่ 1 ของคำตอบ)
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ค่าnullซึ่งไม่สำคัญสำหรับตัวสร้างอาร์เรย์ ( thisเป็นเช่นเดียวthisกับในบริบทตาม 15.3.4.3.2.a.
  • จากนั้นnew Arrayถูกเรียกว่ากำลังถูกส่งผ่านวัตถุที่มีlengthคุณสมบัติ - ซึ่งทำให้วัตถุนั้นเป็นอาร์เรย์ที่เหมือนกับทุกสิ่งที่มันเป็น.applyเพราะข้อต่อไปนี้ใน.apply:
    • ให้ len เป็นผลลัพธ์ของการเรียกเมธอดภายใน [[Get]] ของ argArray ด้วยอาร์กิวเมนต์ "length"
  • เช่นนี้.applyจะผ่านข้อโต้แย้งจาก 0 ถึง.lengthตั้งแต่การโทร[[Get]]บน{ length: 5 }ด้วยค่าที่ 0-4 อัตราผลตอบแทนundefinedคอนสตรัคอาร์เรย์เรียกว่ามีห้าข้อโต้แย้งที่มีค่าundefined(ได้รับทรัพย์สินที่ไม่ได้ประกาศของวัตถุ)
  • ตัวสร้างอาร์เรย์เรียกว่ามี 0, 2 หรือมากกว่าข้อโต้แย้ง คุณสมบัติความยาวของอาร์เรย์ที่สร้างขึ้นใหม่ถูกตั้งค่าเป็นจำนวนอาร์กิวเมนต์ตามข้อมูลจำเพาะและค่าเป็นค่าเดียวกัน
  • ดังนั้นvar arr = Array.apply(null, { length: 5 });สร้างรายการห้าค่าที่ไม่ได้กำหนด

บันทึก : เห็นความแตกต่างระหว่างที่นี่Array.apply(0,{length: 5})และArray(5)เป็นครั้งแรกที่สร้างห้าครั้งประเภทค่าดั้งเดิมundefinedและหลังการสร้างอาร์เรย์ที่ว่างของความยาว 5. เฉพาะเพราะ.mapพฤติกรรม (8.b)[[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.call) คือดัชนีและตัวแรกคือค่านี้
  • ซึ่งหมายความว่าNumberจะถูกเรียกด้วยthisas undefined(ค่าอาร์เรย์) และดัชนีเป็นพารามิเตอร์ ดังนั้นโดยพื้นฐานแล้วจะเหมือนกับการจับคู่แต่ละรายการundefinedกับดัชนีอาร์เรย์Numberทำการแปลงชนิดในกรณีนี้จากตัวเลขเป็นหมายเลขไม่เปลี่ยนดัชนี)

ดังนั้นรหัสด้านบนจะใช้ค่าห้าค่าที่ไม่ได้กำหนดและแม็พแต่ละค่ากับดัชนีในอาเรย์

นี่คือสาเหตุที่เราได้ผลลัพธ์จากรหัสของเรา


1
สำหรับเอกสาร: ข้อกำหนดสำหรับวิธีการทำงานของแผนที่: es5.github.io/#x15.4.4.19 , Mozilla มีสคริปต์ตัวอย่างที่ทำงานตามข้อกำหนดดังกล่าวได้ที่developer.mozilla.org/en-US/docs/Web/JavaScript/ บุคคลอ้างอิง / …
Patrick Evans

1
แต่ทำไมมันถึงใช้ได้กับArray.apply(null, { length: 2 })และไม่Array.apply(null, [2])เรียกว่าArrayนวกรรมิกที่ส่งผ่าน2เป็นค่าความยาว ซอ
Andreas

@Andreas Array.apply(null,[2])เป็นเหมือนArray(2)ที่สร้างอาร์เรย์ว่างที่มีความยาว 2 และไม่ใช่อาร์เรย์ที่มีค่าดั้งเดิมundefinedสองครั้ง ดูการแก้ไขล่าสุดของฉันในบันทึกย่อหลังจากส่วนแรกแจ้งให้เราทราบว่าชัดเจนเพียงพอหรือไม่หากไม่ฉันจะชี้แจงให้ชัดเจน
Benjamin Gruenbaum

ฉันไม่เข้าใจวิธีการทำงานในครั้งแรก ... หลังจากอ่านครั้งที่สองมันก็สมเหตุสมผล {length: 2}ปลอมอาร์เรย์ด้วยสององค์ประกอบที่ตัวArrayสร้างจะแทรกลงในอาร์เรย์ที่สร้างขึ้นใหม่ เนื่องจากไม่มีอาเรย์จริงเข้าถึงองค์ประกอบที่ไม่ปรากฏundefinedซึ่งจะถูกแทรกเข้าไป Nice trick :)
Andreas

5

อย่างที่คุณพูดตอนแรก:

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

สร้างอาร์เรย์ 5 undefinedค่า

ส่วนที่สองคือการเรียกใช้mapฟังก์ชันของอาร์เรย์ซึ่งใช้เวลา 2 ข้อโต้แย้งและส่งกลับอาร์เรย์ใหม่ที่มีขนาดเท่ากัน

อาร์กิวเมนต์แรกที่mapรับเป็นจริงฟังก์ชั่นที่จะใช้กับแต่ละองค์ประกอบในอาเรย์นั้นคาดว่าจะเป็นฟังก์ชั่นที่ใช้ 3 ข้อโต้แย้งและส่งกลับค่า ตัวอย่างเช่น:

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

ถ้าเราผ่านฟังก์ชั่น foo เป็นอาร์กิวเมนต์แรกมันจะถูกเรียกสำหรับแต่ละองค์ประกอบด้วย

  • เป็นค่าขององค์ประกอบซ้ำแล้วซ้ำอีกในปัจจุบัน
  • b เป็นดัชนีขององค์ประกอบที่วนซ้ำในปัจจุบัน
  • c เป็นอาร์เรย์ต้นฉบับทั้งหมด

อาร์กิวเมนต์ที่สองที่mapรับจะถูกส่งผ่านไปยังฟังก์ชันที่คุณส่งผ่านเป็นอาร์กิวเมนต์แรก แต่มันจะไม่เป็น A, B, C หรือในกรณีที่มันจะเป็นfoothis

สองตัวอย่าง:

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}จึงเหมือนกับ Array ที่ฟิลด์ 0..4 มีค่าเริ่มต้นซึ่งไม่ได้กำหนด (เช่นarr[0] === undefinedให้ผลเป็นจริง)
ส่วนที่สองแผนที่ตามชื่อหมายถึงแผนที่จากอาร์เรย์หนึ่งไปยังใหม่ มันทำได้โดยการเข้าไปในอาเรย์ดั้งเดิมและเรียกใช้ฟังก์ชั่นการแมปในแต่ละรายการ

สิ่งที่เหลืออยู่คือการโน้มน้าวใจคุณว่าผลลัพธ์ของการทำแผนที่ฟังก์ชั่นคือดัชนี เคล็ดลับคือการใช้วิธีการที่เรียกว่า 'call' (*) ซึ่งเรียกใช้ฟังก์ชันโดยมีข้อยกเว้นเล็กน้อยที่ param แรกถูกตั้งค่าให้เป็นบริบท 'this' และที่สองกลายเป็น param แรก (เป็นต้น) เมื่อมีการเรียกใช้การแม็พฟังก์ชันพารามิเตอร์ที่สองคือดัชนี

สุดท้าย แต่ไม่ท้ายสุดวิธีที่เรียกใช้คือ Number "Class" และอย่างที่เรารู้ใน JS คือ "Class" เป็นเพียงฟังก์ชั่นและอันนี้ (Number) คาดหวังว่าพารามิเตอร์แรกจะเป็นค่า

(*) พบในต้นแบบของ Function (และ Number เป็นฟังก์ชัน)

Mashal


1
มีความแตกต่างอย่างมากระหว่าง[undefined, undefined, undefined, …]และnew Array(n)หรือ{length: n}- อันหลังเบาบางนั่นคือพวกเขาไม่มีองค์ประกอบ สิ่งนี้มีความเกี่ยวข้องมากmapและนั่นเป็นสาเหตุที่ทำให้Array.applyมีการใช้เลขคี่
Bergi
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.