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.apply
xử lý đối sốArray
xử lý nhiều đối sốNumber
hàm xử lý các đối sốFunction.prototype.call
khô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ư length
biến ma thuật , nhưng ở cốt lõi, nó là một key => value
bả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.length
và 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.map
khô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.toUpperCase
sẽ 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ế) len
là 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 len
cá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 length
tí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.apply
làm việcNhững gì apply
về 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 apply
hoạt động bằng cách đăng nhập arguments
biế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 => value
xạ 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 arguments
biế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 for
vòng lặp đơn giản trên length
các mục, tạo ra list
các giá trị tương ứng ( list
là 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 argArray
trong trường hợp này là một đối tượng có thuộc length
tí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);
Array
xử 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ố ( undefined
chí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 something
thà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.call
call
là apply
anh 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 call
vớ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.call
chỉ là một hàm, tương đương với bất kỳ call
phương thức nào của hàm khác , và như vậy, cũng có một call
phương thức:
log.call === log.call.call; //true
log.call === Function.call; //true
Và call
làm gì? Nó chấp nhận một thisArg
và 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}])
.map
tấ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 2
và true
tươ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 apply
ví dụ đầu tiên . Đầu tiên console.log
sau đó 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.log
cho 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 đó.
this
chỉ 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
.
this
giá trị null
đó không quan trọng cho các nhà xây dựng Array ( this
cũng giống this
như 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 length
tí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 .apply
vì mệnh đề sau trong .apply
:
.apply
là 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 undefined
cá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 undefined
và lần sau tạo ra một mảng trống có độ dài 5. Cụ thể, vì .map
hà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.map
gọ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 this
giá trị được chỉ định (trong trường hợp này là đặt this
giá trị thành `Number).Number.call
) là chỉ mục và đầu tiên là giá trị này.Number
là được gọi với this
as 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 undefined
chỉ mục mảng của nó (vì việc gọi Number
thự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 undefined
hai 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 Array
tạ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 undefined
mà 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 undefined
giá trị.
Phần thứ hai đang gọi map
hà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 map
thự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 map
sẽ đượ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 baz
trong ví dụ trên. Number.call
sẽ 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 map
hà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.call
khô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] === undefined
mang 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 map
và đó 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.