Es-talk posta listesinde aşağıdaki kodla karşılaştım:
Array.apply(null, { length: 5 }).map(Number.call, Number);
Bu üretir
[0, 1, 2, 3, 4]
Bu neden kodun sonucudur? Burada neler oluyor?
Es-talk posta listesinde aşağıdaki kodla karşılaştım:
Array.apply(null, { length: 5 }).map(Number.call, Number);
Bu üretir
[0, 1, 2, 3, 4]
Bu neden kodun sonucudur? Burada neler oluyor?
Yanıtlar:
Bu "hack" i anlamak, birkaç şeyi anlamayı gerektirir:
Array(5).map(...)
Function.prototype.apply
Argümanları nasıl işlerArray
Birden çok argümanı nasıl işler?Number
İşlev bağımsız değişkenleri nasıl işler?Function.prototype.call
yaparBunlar javascript'te oldukça ileri düzey konulardır, bu yüzden bu çok uzun olacaktır. En baştan başlayacağız. Kemer bağlamak!
Array(5).map
?Dizi nedir gerçekten? Değerlerle eşleşen tamsayı anahtarları içeren normal bir nesne. Büyülü length
değişken gibi başka özellikleri de vardır , ancak özünde key => value
diğer nesneler gibi normal bir haritadır. Biraz dizilerle oynayalım, olur mu?
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']
Dizideki öğe arr.length
sayısı ile key=>value
dizinin sahip olduğu eşleme sayısı arasındaki , bundan farklı olabilen doğal farka ulaşıyoruz arr.length
.
Aracılığıyla diziyi Genişleyen arr.length
vermez herhangi bir yeni oluşturmak key=>value
o dizi tanımsız değerlere sahip olduğunu değil yani, eşleştirmeleri, bu bu anahtarları yok . Var olmayan bir mülke erişmeye çalıştığınızda ne olur? Sen alırsın undefined
.
Şimdi başımızı biraz kaldırabilir ve neden arr.map
bu özelliklerin üzerinden geçemeyen işlevlerin olduğunu görebiliriz . arr[3]
Yalnızca tanımsız olsaydı ve anahtar varsa, tüm bu dizi işlevleri, diğer herhangi bir değer gibi onun üzerinden geçerdi:
//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"]
Anahtarın kendisinin asla orada olmadığını kanıtlamak için kasıtlı olarak bir yöntem çağrısı kullandım: Çağrı undefined.toUpperCase
yapmak bir hatayı ortaya çıkarırdı, ancak olmadı. Bunu kanıtlamak için :
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
Ve şimdi benim noktama geliyoruz: Array(N)
İşler nasıl gidiyor? Bölüm 15.4.2.2 süreci açıklamaktadır. Umursamadığımız bir sürü saçma jumbo var, ancak satır aralarını okumayı başarırsanız (ya da bu konuda bana güvenebilirsiniz, ama güvenmeyin), temelde şuna indirgenir:
function Array(len) {
var ret = [];
ret.length = len;
return ret;
}
( len
herhangi bir değer sayısı değil, geçerli bir uint32 olan varsayım (gerçek spesifikasyonda kontrol edilir) altında çalışır )
Şimdi yapmanın neden Array(5).map(...)
işe yaramayacağını anlayabilirsiniz - len
dizideki öğeleri tanımlamıyoruz , key => value
eşlemeleri oluşturmuyoruz, sadece length
özelliği değiştiriyoruz .
Artık bunu yoldan çıkardığımıza göre, ikinci büyülü şeye bakalım:
Function.prototype.apply
çalışırNe apply
yapar temelde bir dizi almak ve bir işlev çağrısının argüman olarak önüne sermek olduğunu. Bu, aşağıdakilerin hemen hemen aynı olduğu anlamına gelir:
function foo (a, b, c) {
return a + b + c;
}
foo(0, 1, 2); //3
foo.apply(null, [0, 1, 2]); //3
Şimdi, apply
sadece arguments
özel değişkeni günlüğe kaydederek nasıl çalıştığını görme sürecini kolaylaştırabiliriz :
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]
İddiamı sondan ikinci örnekte kanıtlamak kolaydır:
function ahaExclamationMark () {
console.log(arguments.length);
console.log(arguments.hasOwnProperty(0));
}
ahaExclamationMark.apply(null, Array(2)); //2, true
(evet, kelime oyunu). key => value
Haritalama biz geçti dizide var olmayabilir apply
, ama kesinlikle var arguments
değişken. Bu, son örneğin çalışmasının nedeni ile aynıdır: Anahtarlar, geçtiğimiz nesnede yoktur, ancak içinde bulunurlar arguments
.
Neden? En atalım Bölüm 15.3.4.3 , Function.prototype.apply
tanımlanır. Çoğunlukla umursamadığımız şeyler, ama işte ilginç kısım:
- Len, argArray'in [[Get]] dahili yöntemini "length" bağımsız değişkeniyle çağırmanın sonucu olsun.
Hangi temelde şu anlama gelir: argArray.length
. Spesifikasyon daha sonra öğeler for
üzerinde basit bir döngü oluşturarak karşılık gelen değerlerden bir tane oluşturur ( bazı dahili vudu, ancak temelde bir dizidir). Çok, çok gevşek kod açısından: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);
};
Bu durumda tek yapmamız gereken argArray
bir length
özelliği olan bir nesnedir . Ve şimdi değerlerin neden tanımsız olduğunu görebiliriz, ancak anahtarlar değil arguments
: key=>value
Eşlemeleri biz oluşturuyoruz .
Vay canına, bu yüzden bu önceki bölümden daha kısa olmayabilir. Ama bitirdiğimizde pasta olacak, bu yüzden sabırlı olun! Ancak, sonraki bölümden sonra (kısa olacak, söz veriyorum) ifadeyi incelemeye başlayabiliriz. Unutmanız durumunda, soru aşağıdakilerin nasıl çalıştığıdır:
Array.apply(null, { length: 5 }).map(Number.call, Number);
Array
Birden çok argümanı nasıl işler?Yani! Bir length
argüman ilettiğinizde ne olduğunu gördük Array
, ancak ifadede, argüman olarak birkaç şey iletiyoruz ( undefined
tam olarak 5'li bir dizi ). Bölüm 15.4.2.1 bize ne yapacağımızı anlatır. Son paragraf bizim için önemli olan tek şeydir ve gerçekten tuhaf bir şekilde ifade edilmiştir , ancak biraz aşağıya iner:
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! Birkaç tanımsız değer dizisi elde ederiz ve bu tanımsız değerlerin bir dizisini döndürürüz.
Son olarak, aşağıdakileri deşifre edebiliriz:
Array.apply(null, { length: 5 })
5 tanımsız değer içeren bir dizi döndürdüğünü gördük, anahtarların tümü var.
Şimdi, ifadenin ikinci kısmına:
[undefined, undefined, undefined, undefined, undefined].map(Number.call, Number)
Bu daha kolay, kıvrılmamış kısım olacak çünkü belirsiz bilgisayar korsanlarına pek güvenmiyor.
Number
Girdiye nasıl davranılırYapmak Number(something)
( bölüm 15.7.1 ) something
bir sayıya dönüşür ve hepsi bu kadar. Bunu nasıl yapar, özellikle dizeler söz konusu olduğunda biraz kıvrımlıdır, ancak ilgilenmeniz durumunda işlem bölüm 9.3'te tanımlanmıştır .
Function.prototype.call
call
, bölüm 15.3.4.4'teapply
tanımlanan erkek kardeşidir . Bir dizi argüman almak yerine, sadece aldığı argümanları alır ve onları iletir.
Birden fazla zincir oluşturduğunuzda işler ilginçleşir call
, tuhaf olanı 11'e kadar yükseltin:
function log () {
console.log(this, arguments);
}
log.call.call(log, {a:4}, {a:5});
//{a:4}, [{a:5}]
//^---^ ^-----^
// this arguments
Neler olup bittiğini anlayana kadar bu oldukça değerlidir. log.call
sadece bir işlevdir, başka herhangi bir işlevin call
yöntemine eşdeğerdir ve bu nedenle call
kendi üzerinde de bir yöntemi vardır :
log.call === log.call.call; //true
log.call === Function.call; //true
Ve ne yapar call
? Bir thisArg
ve bir dizi argümanı kabul eder ve ana işlevini çağırır. Bunu şu şekilde tanımlayabiliriz apply
(yine, çok gevşek kod, çalışmaz):
Function.prototype.call = function (thisArg) {
var args = arguments.slice(1); //I wish that'd work
return this.apply(thisArg, args);
};
Bunun nasıl azaldığını izleyelim:
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
hepsininDaha bitmedi. Çoğu dizi yöntemine bir işlev sağladığınızda ne olacağını görelim:
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
Kendimiz bir argüman sunmazsak, varsayılan olur window
. Geri aramamıza verilen argümanların sırasına dikkat edin ve bunu tekrar 11'e kadar tuhaf hale getirelim:
arr.forEach(log.call, log);
//'a', [0, ['a', 'b', 'c']]
//'b', [1, ['a', 'b', 'c']]
//'b', [2, ['a', 'b', 'c']]
// ^ ^
Whoa whoa whoa ... hadi biraz geri dönelim. Burada neler oluyor? Biz de görebileceğiniz bölüm 15.4.4.18 , forEach
tanımlanır, şu hoş çok olur:
var callback = log.call,
thisArg = log;
for (var i = 0; i < arr.length; i += 1) {
callback.call(thisArg, arr[i], i, arr);
}
Yani, şunu anlıyoruz:
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);
Şimdi nasıl .map(Number.call, Number)
çalıştığını görebiliriz :
Number.call.call(Number, arr[i], i, arr);
Number.call(arr[i], i, arr);
Number(i, arr);
Bu i
, geçerli dizinin dönüşümünü bir sayıya döndürür .
İfade
Array.apply(null, { length: 5 }).map(Number.call, Number);
İki kısımda çalışır:
var arr = Array.apply(null, { length: 5 }); //1
arr.map(Number.call, Number); //2
İlk bölüm 5 tanımsız öğe dizisi oluşturur. İkincisi bu dizinin üzerinden geçer ve dizinlerini alarak bir dizi öğe dizini ile sonuçlanır:
[0, 1, 2, 3, 4]
ahaExclamationMark.apply(null, Array(2)); //2, true
. Neden 2
ve true
sırasıyla geri dönüyor ? Array(2)
Burada sadece bir argüman geçmiyor musunuz ?
apply
, ancak bu argüman işleve iletilen iki argümana "splatted". Bunu ilk apply
örneklerde daha kolay görebilirsiniz . Birincisi console.log
, aslında iki argüman (iki dizi öğesi) aldığımızı console.log
gösterir ve ikincisi , dizinin key=>value
1. yuvada bir eşlemesi olduğunu gösterir (cevabın 1. bölümünde açıklandığı gibi).
log.apply(null, document.getElementsByTagName('script'));
çalışması için gerekli olmadığına ve bazı tarayıcılarda çalışmayacağına ve [].slice.call(NodeList)
bir NodeList'i bir diziye dönüştürmenin bunlarda da çalışmayacağına dikkat edin.
this
yalnızca varsayılan olarak Window
katı olmayan moddadır.
Yasal Uyarı : Bu, yukarıdaki kod çok resmi açıklamasıdır - Bunun ne kadar ben bunu açıklamak biliyorum. Daha basit bir cevap için - Zirak'ın yukarıdaki harika cevabını kontrol edin. Bu, yüzünüzde daha derinlemesine bir özelliktir ve daha az "aha".
Burada birkaç şey oluyor. Biraz bölünelim.
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
Birinci satırda, dizi yapıcı bir fonksiyonu olarak adlandırılır ile Function.prototype.apply
.
this
Değerdir null
Array yapıcısının (önemli değil olan this
aynı this
15.3.4.3.2.a. göre bağlamında olduğu gibinew Array
, bir length
özelliğe sahip bir nesnenin iletilmesi çağrılır - bu, nesnenin .apply
, aşağıdaki cümle içindeki tüm önemli maddeler gibi bir dizi olmasına neden olur .apply
:
.apply
0 ila bağımsız değişkenler geçirerek .length
arama için, [[Get]]
ilgili { length: 5 }
değerleri 0 ila 4 verim undefined
dizi yapıcı değeri beş bağımsız değişken olarak adlandırılır undefined
(örneğin bir nesnenin bir edilmemiş özelliği alma).var arr = Array.apply(null, { length: 5 });
tanımlanmamış beş değerden oluşan bir liste oluşturur.Not : Buradaki Array.apply(0,{length: 5})
ve arasındaki farka dikkat edin, Array(5)
ilki ilkel değer türünün beş katı undefined
ve ikincisi boş bir 5 uzunluk dizisi yaratıyor. Özellikle, davranışından dolayı .map
(8.b) ve özellikle [[HasProperty]
.
Dolayısıyla, uyumlu bir spesifikasyondaki yukarıdaki kod şununla aynıdır:
var arr = [undefined, undefined, undefined, undefined, undefined];
arr.map(Number.call, Number); // Calculate and return a number based on the index passed
Şimdi ikinci bölüme geçelim.
Array.prototype.map
Number.call
dizinin her bir öğesi için geri çağrı işlevini (bu durumda ) çağırır ve belirtilen this
değeri kullanır (bu durumda this
değeri `Sayı olarak ayarlar ).Number.call
) dizindir ve ilki bu değerdir.Number
, this
as undefined
(dizi değeri) ve parametre olarak dizin ile çağrıldığı anlamına gelir . Dolayısıyla, temelde her undefined
birini kendi dizi indeksine eşlemekle aynıdır (çünkü çağırma Number
tip dönüşümü gerçekleştirir, bu durumda dizini değiştirmeden sayıdan sayıya).Böylece, yukarıdaki kod beş tanımsız değeri alır ve her birini dizideki diziniyle eşler.
Bu yüzden sonucu kodumuza alıyoruz.
Array.apply(null,[2])
, iki kez ilkel değeri içeren bir dizi değil , 2 uzunluğunda boş bir dizi Array(2)
oluşturan gibidir . En son düzenlememi ilk bölümden sonraki notta görün, yeterince açık olup olmadığını bana bildirin, yoksa bunu açıklığa kavuşturacağım. undefined
{length: 2}
kurucunun Array
yeni oluşturulan diziye ekleyeceği iki öğeli bir diziyi taklit eder . Mevcut olmayan öğelere erişen gerçek bir dizi olmadığından undefined
, daha sonra eklenen sonuçları verir . Güzel numara :)
Dediğin gibi, ilk kısım:
var arr = Array.apply(null, { length: 5 });
5 undefined
değerden oluşan bir dizi oluşturur .
İkinci kısım, map
2 argüman alan ve aynı büyüklükte yeni bir dizi döndüren dizinin işlevini çağırmaktır .
Alan ilk argüman map
aslında dizideki her elemana uygulanacak bir fonksiyondur, 3 argüman alan ve bir değer döndüren bir fonksiyon olması beklenir. Örneğin:
function foo(a,b,c){
...
return ...
}
foo fonksiyonunu ilk argüman olarak iletirsek, her eleman için şu çağrılar yapılacaktır:
Alan ikinci argüman map
, ilk argüman olarak ilettiğiniz işleve aktarılıyor. Ama a, b, ne de c olması durumunda foo
olmazdı this
.
İki örnek:
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]
ve daha açık hale getirmek için bir tane daha:
function qux(a,b,c){
return a
}
var newArr4 = arr2.map(qux,9);
//newArr4 is equal to [3,4,5]
Peki ya Number.call?
Number.call
2 bağımsız değişken alan ve ikinci bağımsız değişkeni bir sayıya çözümlemeye çalışan bir işlevdir (ilk bağımsız değişkenle ne yaptığından emin değilim).
map
Geçen ikinci argüman indeks olduğu için, o indekste yeni diziye yerleştirilecek değer indekse eşittir. Tıpkı baz
yukarıdaki örnekteki işlev gibi. Number.call
dizini ayrıştırmaya çalışacak - doğal olarak aynı değeri döndürecektir.
map
Kodunuzdaki işleve ilettiğiniz ikinci bağımsız değişkenin aslında sonuç üzerinde bir etkisi yoktur. Yanılıyorsam düzelt lütfen.
Number.call
bağımsız değişkenleri sayılara ayrıştıran özel bir işlev değildir. Bu sadece === Function.prototype.call
. Sadece İkinci argüman olarak geçirilen fonksiyon this
için-değeri call
, alakalı - .map(eval.call, Number)
, .map(String.call, Number)
ve .map(Function.prototype.call, Number)
tüm eşdeğerdir.
Bir dizi, basitçe 'uzunluk' alanını ve bazı yöntemleri (örneğin, itme) içeren bir nesnedir. Yani dizi var arr = { length: 5}
, temelde 0..4 alanlarının tanımsız olan varsayılan değere sahip olduğu (yani arr[0] === undefined
doğru sonucunu veren) bir dizi ile aynıdır .
İkinci bölüme gelince, map, adından da anlaşılacağı gibi, bir diziden yenisine eşlenir. Bunu, orijinal dizide dolaşarak ve her öğede eşleme işlevini çağırarak yapar.
Geriye kalan tek şey, eşleme fonksiyonunun sonucunun indeks olduğuna sizi ikna etmektir. İşin püf noktası, ilk parametrenin 'bu' bağlam olarak ayarlanması ve ikincisinin ilk param (vb.) Olması gibi küçük bir istisna dışında bir işlevi çağıran 'call' (*) adlı yöntemi kullanmaktır. Tesadüfen, eşleme işlevi çağrıldığında, ikinci param dizindir.
Son olarak, en önemlisi, çağrılan yöntem Sayı "Sınıfı" dır ve JS'de bildiğimiz gibi, "Sınıf" basitçe bir işlevdir ve bu (Sayı), ilk parametrenin değer olmasını bekler.
(*) İşlev prototipinde bulunur (ve Sayı bir işlevdir).
Mashal
[undefined, undefined, undefined, …]
Ve new Array(n)
veya arasında çok büyük bir fark var {length: n}
- ikincisi seyrek , yani elementleri yok. Bu çok alakalı map
ve bu yüzden garip Array.apply
kullanıldı.
Array.apply(null, Array(30)).map(Number.call, Number)
okunması daha kolaydır çünkü düz bir nesnenin bir Dizi gibi görünmesini önler.