Tôi đã chạy vào đoạn mã sau trong danh sách gửi thư thảo luận es:
Array.apply(null, { length: 5 }).map(Number.call, Number);
Điều này tạo ra
[0, 1, 2, 3, 4]
Tại sao đây là kết quả của mã? Chuyện gì đang xảy ra ở đây vậy?
Tôi đã chạy vào đoạn mã sau trong danh sách gửi thư thảo luận es:
Array.apply(null, { length: 5 }).map(Number.call, Number);
Điều này tạo ra
[0, 1, 2, 3, 4]
Tại sao đây là kết quả của mã? Chuyện gì đang xảy ra ở đây vậy?
Câu trả lời:
Hiểu "hack" này đòi hỏi phải hiểu một số điều:
Array(5).map(...)Function.prototype.applyxử lý đối sốArrayxử lý nhiều đối sốNumberhàm xử lý các đối sốFunction.prototype.callkhôngChúng là những chủ đề khá tiên tiến trong javascript, vì vậy điều này sẽ dài hơn là khá dài. Chúng ta sẽ bắt đầu từ đầu. Thắt dây an toàn!
Array(5).map?Thật là một mảng, thực sự? Một đối tượng thông thường, chứa các khóa nguyên, ánh xạ tới các giá trị. Nó có các tính năng đặc biệt khác, ví dụ như lengthbiến ma thuật , nhưng ở cốt lõi, nó là một key => valuebản đồ thông thường , giống như bất kỳ đối tượng nào khác. Chúng ta hãy chơi với mảng một chút, phải không?
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']
Chúng ta có sự khác biệt vốn có giữa số lượng các mục trong mảng arr.lengthvà số lượng key=>valueánh xạ của mảng có thể khác nhau arr.length.
Mở rộng mảng thông qua arr.length không tạo ra bất kỳ key=>valueánh xạ mới nào , do đó, không phải là mảng có các giá trị không xác định, nó không có các khóa này . Và điều gì xảy ra khi bạn cố gắng truy cập vào một tài sản không tồn tại? Bạn lấyundefined .
Bây giờ chúng ta có thể ngẩng đầu lên một chút và xem tại sao các chức năng như arr.mapkhông đi qua các tính chất này. Nếu arr[3]chỉ đơn thuần là không xác định và khóa tồn tại, tất cả các hàm mảng này sẽ vượt qua nó giống như bất kỳ giá trị nào khác:
//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"]
Tôi cố tình sử dụng một cuộc gọi phương thức để chứng minh thêm rằng điểm chính của khóa không bao giờ có: Gọi undefined.toUpperCasesẽ gây ra lỗi, nhưng không được. Để chứng minh rằng :
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
Và bây giờ chúng ta đến quan điểm của tôi: Làm thế nào Array(N)để mọi thứ. Mục 15.4.2.2 mô tả quy trình. Có một loạt mumbo jumbo mà chúng tôi không quan tâm, nhưng nếu bạn có thể đọc được giữa các dòng (hoặc bạn có thể tin tưởng tôi về điều này, nhưng không), về cơ bản nó sẽ hiểu rõ điều này:
function Array(len) {
var ret = [];
ret.length = len;
return ret;
}
(hoạt động theo giả định (được kiểm tra trong thông số thực tế) lenlà một uint32 hợp lệ và không chỉ là bất kỳ số lượng giá trị nào)
Vì vậy, bây giờ bạn có thể thấy lý do tại sao hoạt động Array(5).map(...)không hiệu quả - chúng tôi không xác định lencác mục trên mảng, chúng tôi không tạo key => valueánh xạ, chúng tôi chỉ đơn giản là thay đổi thuộc lengthtính.
Bây giờ chúng ta đã có được điều đó, hãy nhìn vào điều kỳ diệu thứ hai:
Function.prototype.applylàm việcNhững gì applyvề cơ bản là lấy một mảng và hủy bỏ nó như là một đối số của hàm gọi. Điều đó có nghĩa là những điều sau đây khá giống nhau:
function foo (a, b, c) {
return a + b + c;
}
foo(0, 1, 2); //3
foo.apply(null, [0, 1, 2]); //3
Bây giờ, chúng ta có thể dễ dàng quá trình xem cách applyhoạt động bằng cách đăng nhập argumentsbiến đặc biệt:
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]
Thật dễ dàng để chứng minh yêu cầu của tôi trong ví dụ thứ hai đến cuối cùng:
function ahaExclamationMark () {
console.log(arguments.length);
console.log(arguments.hasOwnProperty(0));
}
ahaExclamationMark.apply(null, Array(2)); //2, true
(vâng, ý định chơi chữ). Ánh key => valuexạ có thể không tồn tại trong mảng mà chúng ta đã chuyển qua apply, nhưng nó chắc chắn tồn tại trong argumentsbiến. Đó là lý do tương tự ví dụ cuối cùng hoạt động: Các khóa không tồn tại trên đối tượng chúng ta vượt qua, nhưng chúng tồn tại trongarguments .
Tại sao vậy? Hãy xem Mục 15.3.4.3 , nơi Function.prototype.applyđược định nghĩa. Hầu hết những điều chúng tôi không quan tâm, nhưng đây là phần thú vị:
- Đặt len là kết quả của việc gọi phương thức bên trong [[Get]] của argArray với đối số "length".
Về cơ bản có nghĩa là : argArray.length. Thông số kỹ thuật sau đó tiến hành thực hiện một forvòng lặp đơn giản trên lengthcác mục, tạo ra listcác giá trị tương ứng ( listlà một số voodoo nội bộ, nhưng về cơ bản nó là một mảng). Về mặt mã rất, rất lỏng lẻo:
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);
};
Vì vậy, tất cả những gì chúng ta cần bắt chước argArraytrong trường hợp này là một đối tượng có thuộc lengthtính. Và bây giờ chúng ta có thể thấy lý do tại sao các giá trị không được xác định, nhưng các khóa không, trên arguments: Chúng tôi tạo rakey=>value ánh xạ.
Phew, vì vậy điều này có thể không ngắn hơn phần trước. Nhưng sẽ có bánh khi chúng ta hoàn thành, vì vậy hãy kiên nhẫn! Tuy nhiên, sau phần sau (sẽ ngắn, tôi hứa) chúng ta có thể bắt đầu mổ xẻ biểu thức. Trong trường hợp bạn quên, câu hỏi là làm thế nào để làm việc sau đây:
Array.apply(null, { length: 5 }).map(Number.call, Number);
Arrayxử lý nhiều đối sốVì thế! Chúng tôi đã thấy những gì xảy ra khi bạn truyền một lengthđối số cho Array, nhưng trong biểu thức, chúng tôi chuyển một số thứ dưới dạng đối số ( undefinedchính xác là một mảng 5 ). Mục 15.4.2.1 cho chúng ta biết phải làm gì. Đoạn cuối là tất cả những gì quan trọng đối với chúng tôi, và nó được diễn đạt rất kỳ quặc, nhưng nó có nghĩa là:
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! Chúng tôi nhận được một mảng của một số giá trị không xác định và chúng tôi trả về một mảng của các giá trị không xác định này.
Cuối cùng, chúng ta có thể giải mã như sau:
Array.apply(null, { length: 5 })
Chúng tôi thấy rằng nó trả về một mảng chứa 5 giá trị không xác định, với tất cả các khóa tồn tại.
Bây giờ, đến phần thứ hai của biểu thức:
[undefined, undefined, undefined, undefined, undefined].map(Number.call, Number)
Đây sẽ là phần dễ dàng hơn, không phức tạp, vì nó không phụ thuộc quá nhiều vào các bản hack tối nghĩa.
Number xử lý đầu vàoLàm Number(something)( phần 15.7.1 ) chuyển đổi somethingthành một số, và đó là tất cả. Làm thế nào nó là một chút phức tạp, đặc biệt là trong các trường hợp của chuỗi, nhưng hoạt động được xác định trong phần 9.3 trong trường hợp bạn quan tâm.
Function.prototype.callcalllà applyanh trai, được định nghĩa trong phần 15.3.4.4 . Thay vì lấy một loạt các đối số, nó chỉ lấy các đối số mà nó nhận được và chuyển chúng về phía trước.
Mọi thứ trở nên thú vị khi bạn kết hợp nhiều hơn một callvới nhau, tạo ra sự kỳ lạ lên đến 11:
function log () {
console.log(this, arguments);
}
log.call.call(log, {a:4}, {a:5});
//{a:4}, [{a:5}]
//^---^ ^-----^
// this arguments
Điều này khá xứng đáng cho đến khi bạn nắm bắt được những gì đang diễn ra. log.callchỉ là một hàm, tương đương với bất kỳ callphương thức nào của hàm khác , và như vậy, cũng có một callphương thức:
log.call === log.call.call; //true
log.call === Function.call; //true
Và calllàm gì? Nó chấp nhận một thisArgvà một loạt các đối số và gọi hàm cha của nó. Chúng tôi có thể xác định nó thông qua apply (một lần nữa, mã rất lỏng lẻo, sẽ không hoạt động):
Function.prototype.call = function (thisArg) {
var args = arguments.slice(1); //I wish that'd work
return this.apply(thisArg, args);
};
Hãy theo dõi làm thế nào điều này đi xuống:
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}])
.maptất cảNó chưa kết thúc. Hãy xem điều gì xảy ra khi bạn cung cấp một hàm cho hầu hết các phương thức mảng:
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
Nếu chúng ta không tự cung cấp một thisđối số, nó sẽ mặc định window. Hãy lưu ý thứ tự mà các đối số được cung cấp cho cuộc gọi lại của chúng tôi và hãy làm cho nó trở nên kỳ lạ đến tận 11 lần nữa:
arr.forEach(log.call, log);
//'a', [0, ['a', 'b', 'c']]
//'b', [1, ['a', 'b', 'c']]
//'b', [2, ['a', 'b', 'c']]
// ^ ^
Whoa whoa whoa ... hãy sao lưu một chút. Những gì đang xảy ra ở đây? Chúng ta có thể thấy trong phần 15.4.4.18 , nơi forEachđược định nghĩa, khá nhiều điều sau đây xảy ra:
var callback = log.call,
thisArg = log;
for (var i = 0; i < arr.length; i += 1) {
callback.call(thisArg, arr[i], i, arr);
}
Vì vậy, chúng tôi nhận được điều này:
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);
Bây giờ chúng ta có thể thấy cách .map(Number.call, Number)hoạt động:
Number.call.call(Number, arr[i], i, arr);
Number.call(arr[i], i, arr);
Number(i, arr);
Trả về phép biến đổi i, chỉ mục hiện tại thành một số.
Cách diễn đạt
Array.apply(null, { length: 5 }).map(Number.call, Number);
Hoạt động trong hai phần:
var arr = Array.apply(null, { length: 5 }); //1
arr.map(Number.call, Number); //2
Phần đầu tiên tạo ra một mảng gồm 5 mục không xác định. Cái thứ hai đi qua mảng đó và lấy các chỉ số của nó, dẫn đến một mảng các chỉ số phần tử:
[0, 1, 2, 3, 4]
ahaExclamationMark.apply(null, Array(2)); //2, true. Tại sao nó trở lại 2và truetương ứng? Không phải bạn chỉ truyền một đối số tức là Array(2)ở đây sao?
apply, nhưng đối số đó được "tách" thành hai đối số được truyền cho hàm. Bạn có thể thấy điều đó dễ dàng hơn trong các applyví dụ đầu tiên . Đầu tiên console.logsau đó cho thấy rằng thực sự, chúng tôi đã nhận được hai đối số (hai mục mảng) và lần thứ hai console.logcho thấy mảng có key=>valueánh xạ trong khe thứ nhất (như được giải thích trong phần 1 của câu trả lời).
log.apply(null, document.getElementsByTagName('script'));là không bắt buộc để hoạt động và không hoạt động trong một số trình duyệt và [].slice.call(NodeList)để biến NodeList thành một mảng cũng sẽ không hoạt động trong đó.
thischỉ mặc định Windowở chế độ không nghiêm ngặt.
Tuyên bố miễn trừ trách nhiệm : Đây là một mô tả rất chính thức về đoạn mã trên - đây là cách tôi biết cách giải thích nó. Để có câu trả lời đơn giản hơn - hãy xem câu trả lời tuyệt vời của Zirak ở trên. Đây là một đặc điểm kỹ thuật sâu hơn trong khuôn mặt của bạn và ít "aha".
Một số điều đang xảy ra ở đây. Hãy phá vỡ nó một chút.
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
Trong dòng đầu tiên, hàm tạo mảng được gọi là hàm với Function.prototype.apply.
thisgiá trị nullđó không quan trọng cho các nhà xây dựng Array ( thiscũng giống thisnhư trong bối cảnh theo 15.3.4.3.2.a.new Arrayđược gọi là được truyền một đối tượng có thuộc lengthtính - điều đó làm cho đối tượng đó là một mảng giống như tất cả những gì nó quan trọng .applyvì mệnh đề sau trong .apply:
.applylà qua các đối số từ 0 đến .length, vì gọi [[Get]]về { length: 5 }với các giá trị từ 0 đến 4 sản lượng undefinedcác nhà xây dựng mảng được gọi với lăm đối số có giá trị làundefined (nhận được một tài sản không khai báo của một đối tượng).var arr = Array.apply(null, { length: 5 });tạo ra một danh sách năm giá trị không xác định.Lưu ý : Lưu ý sự khác biệt ở đây giữa Array.apply(0,{length: 5})và Array(5), lần đầu tiên tạo ra năm lần loại giá trị nguyên thủy undefinedvà lần sau tạo ra một mảng trống có độ dài 5. Cụ thể, vì .maphành vi của (8.b) và cụ thể[[HasProperty] .
Vì vậy, mã ở trên trong một đặc tả tuân thủ giống như:
var arr = [undefined, undefined, undefined, undefined, undefined];
arr.map(Number.call, Number); // Calculate and return a number based on the index passed
Bây giờ đến phần thứ hai.
Array.prototype.mapgọi hàm gọi lại (trong trường hợp này Number.call) trên mỗi phần tử của mảng và sử dụng thisgiá trị được chỉ định (trong trường hợp này là đặt thisgiá trị thành `Number).Number.call) là chỉ mục và đầu tiên là giá trị này.Numberlà được gọi với thisas undefined(giá trị mảng) và chỉ mục là tham số. Vì vậy, về cơ bản giống như ánh xạ từng undefinedchỉ mục mảng của nó (vì việc gọi Numberthực hiện chuyển đổi loại, trong trường hợp này từ số này sang số không thay đổi chỉ mục).Do đó, đoạn mã trên lấy năm giá trị không xác định và ánh xạ từng giá trị vào chỉ mục của nó trong mảng.
Đó là lý do tại sao chúng tôi nhận được kết quả cho mã của chúng tôi.
Array.apply(null,[2])giống như Array(2)tạo ra một mảng trống có độ dài 2 và không phải là một mảng chứa giá trị nguyên thủy undefinedhai lần. Xem bản chỉnh sửa gần đây nhất của tôi trong ghi chú sau phần đầu tiên, cho tôi biết nếu nó đủ rõ ràng và nếu không tôi sẽ làm rõ về điều đó.
{length: 2}giả mạo một mảng với hai phần tử mà hàm Arraytạo sẽ chèn vào mảng vừa tạo. Vì không có mảng thực sự truy cập vào các phần tử không hiện diện undefinedmà sau đó được chèn vào. Thủ thuật hay :)
Như bạn đã nói, phần đầu tiên:
var arr = Array.apply(null, { length: 5 });
tạo ra một mảng gồm 5 undefinedgiá trị.
Phần thứ hai đang gọi maphàm của mảng có 2 đối số và trả về một mảng mới có cùng kích thước.
Đối số đầu tiên mapthực sự là một hàm để áp dụng cho từng phần tử trong mảng, nó được dự kiến là một hàm có 3 đối số và trả về một giá trị. Ví dụ:
function foo(a,b,c){
...
return ...
}
nếu chúng ta truyền hàm foo làm đối số đầu tiên, nó sẽ được gọi cho mỗi phần tử với
Đối số thứ hai mapsẽ được truyền cho hàm mà bạn truyền làm đối số thứ nhất. Nhưng nó sẽ không phải là a, b, hay c trong trường hợp foo, nó sẽ là this.
Hai ví dụ:
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]
và một số khác chỉ để làm cho nó rõ ràng hơn:
function qux(a,b,c){
return a
}
var newArr4 = arr2.map(qux,9);
//newArr4 is equal to [3,4,5]
Vậy còn Number.call thì sao?
Number.call là một hàm có 2 đối số và cố phân tích đối số thứ hai thành một số (tôi không chắc nó làm gì với đối số thứ nhất).
Vì đối số thứ hai mapđang truyền là chỉ mục, nên giá trị sẽ được đặt trong mảng mới tại chỉ mục đó bằng với chỉ mục. Cũng giống như hàm baztrong ví dụ trên. Number.callsẽ cố phân tích chỉ mục - nó sẽ tự nhiên trả về cùng một giá trị.
Đối số thứ hai bạn truyền cho maphàm trong mã của bạn không thực sự có ảnh hưởng đến kết quả. Xin hãy sửa tôi nếu tôi sai.
Number.callkhông có chức năng đặc biệt phân tích đối số thành số. Nó chỉ là === Function.prototype.call. Chỉ số thứ hai, chức năng mà được thông qua như là this-giá trị đến call, có liên quan - .map(eval.call, Number), .map(String.call, Number)và .map(Function.prototype.call, Number)tất cả đều tương đương.
Một mảng chỉ đơn giản là một đối tượng bao gồm trường 'độ dài' và một số phương thức (ví dụ: đẩy). Vì vậy, var arr = { length: 5}mảng trong về cơ bản giống như một mảng trong đó các trường 0..4 có giá trị mặc định không xác định (nghĩa là arr[0] === undefinedmang lại giá trị đúng).
Đối với phần thứ hai, ánh xạ, như tên gọi của nó, ánh xạ từ một mảng sang một mảng mới. Nó làm như vậy bằng cách duyệt qua mảng ban đầu và gọi hàm ánh xạ trên mỗi mục.
Tất cả những gì còn lại là để thuyết phục bạn rằng kết quả của chức năng ánh xạ là chỉ mục. Thủ thuật là sử dụng phương thức có tên 'gọi' (*) để gọi một hàm với ngoại lệ nhỏ là tham số đầu tiên được đặt thành bối cảnh 'this' và thứ hai trở thành tham số thứ nhất (v.v.). Thật trùng hợp, khi hàm ánh xạ được gọi, tham số thứ hai là chỉ mục.
Cuối cùng nhưng không kém phần quan trọng, phương thức được gọi là Số "Lớp" và như chúng ta biết trong JS, "Lớp" chỉ đơn giản là một hàm và phương thức này (Số) mong muốn tham số đầu tiên là giá trị.
(*) được tìm thấy trong nguyên mẫu của Hàm (và Số là một hàm).
MASHAL
[undefined, undefined, undefined, …]và new Array(n)hoặc {length: n}- những cái sau còn thưa thớt , tức là chúng không có yếu tố. Điều này rất phù hợp mapvà đó là lý do tại sao số lẻ Array.applyđược sử dụng.
Array.apply(null, Array(30)).map(Number.call, Number)dễ đọc hơn vì nó tránh giả vờ rằng một đối tượng đơn giản là một Mảng.