JavaScript'te aralık oluşturma - garip sözdizimi


129

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?


2
IMO'nun 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.
fncomp

10
@fncomp ya lütfen kullanmayın aslında bir dizi oluşturun. Sadece basit yaklaşımdan daha yavaş değil, aynı zamanda anlaşılması da o kadar kolay değil. Buradaki sözdizimini anlamak zordur (gerçekten API ve sözdizimi değil), bu da bunu ilginç bir soru, ancak korkunç üretim kodu IMO yapar.
Benjamin Gruenbaum

Evet, kimsenin onu kullanmasını önermemekle birlikte, nesnenin gerçek versiyonuna göre okunmasının daha kolay olduğunu düşündüm.
fncomp

1
Bunu neden birinin yapmak isteyeceğinden emin değilim. Diziyi bu şekilde oluşturmak için gereken süre, biraz daha az seksi ama çok daha hızlı bir şekilde yapılabilirdi: jsperf.com/basic-vs-extreme
Eric Hodonsky

Yanıtlar:


263

Bu "hack" i anlamak, birkaç şeyi anlamayı gerektirir:

  1. Neden sadece yapmıyoruz Array(5).map(...)
  2. Function.prototype.applyArgümanları nasıl işler
  3. ArrayBirden çok argümanı nasıl işler?
  4. Numberİşlev bağımsız değişkenleri nasıl işler?
  5. Ne Function.prototype.callyapar

Bunlar 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!

1. Neden sadece değil Array(5).map?

Dizi nedir gerçekten? Değerlerle eşleşen tamsayı anahtarları içeren normal bir nesne. Büyülü lengthdeğişken gibi başka özellikleri de vardır , ancak özünde key => valuediğ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.lengthsayısı ile key=>valuedizinin 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=>valueo 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.mapbu ö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.toUpperCaseyapmak 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;
}

( lenherhangi 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 - lendizideki öğeleri tanımlamıyoruz , key => valueeş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:

2. Nasıl Function.prototype.applyçalışır

Ne applyyapar 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, applysadece 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 => valueHaritalama biz geçti dizide var olmayabilir apply, ama kesinlikle var argumentsdeğ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.applytanımlanır. Çoğunlukla umursamadığımız şeyler, ama işte ilginç kısım:

  1. 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:lengthlistlist

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 argArraybir lengthözelliği olan bir nesnedir . Ve şimdi değerlerin neden tanımsız olduğunu görebiliriz, ancak anahtarlar değil arguments: key=>valueEş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);

3. ArrayBirden çok argümanı nasıl işler?

Yani! Bir lengthargüman ilettiğinizde ne olduğunu gördük Array, ancak ifadede, argüman olarak birkaç şey iletiyoruz ( undefinedtam 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.

İfadenin ilk kısmı

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.

4. NumberGirdiye nasıl davranılır

Yapmak Number(something)( bölüm 15.7.1 ) somethingbir 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 .

5. Oyunlar 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.callsadece bir işlevdir, başka herhangi bir işlevin callyöntemine eşdeğerdir ve bu nedenle callkendi üzerinde de bir yöntemi vardır :

log.call === log.call.call; //true
log.call === Function.call; //true

Ve ne yapar call? Bir thisArgve 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}])

Daha sonraki kısım veya .maphepsinin

Daha 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

thisKendimiz 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 , forEachtanı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 .

Sonuç olarak,

İ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]

@Zirak Lütfen aşağıdakileri anlamamda bana yardımcı olun ahaExclamationMark.apply(null, Array(2)); //2, true. Neden 2ve truesırasıyla geri dönüyor ? Array(2)Burada sadece bir argüman geçmiyor musunuz ?
Geek

4
@Geek Yalnızca bir argüman iletiriz 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.loggösterir ve ikincisi , dizinin key=>value1. yuvada bir eşlemesi olduğunu gösterir (cevabın 1. bölümünde açıklandığı gibi).
Zirak

4
(Bazı) talepler nedeniyle artık ses sürümünün keyfini çıkarabilirsiniz: dl.dropboxusercontent.com/u/24522528/SO-answer.mp3
Zirak

1
Bir ana bilgisayar nesnesi olan bir NodeList'i yerel bir yönteme olduğu gibi geçirmenin 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.
RobG

2
Tek düzeltme: thisyalnızca varsayılan olarak Windowkatı olmayan moddadır.
ComFreek

21

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.

  • thisDeğerdir nullArray yapıcısının (önemli değil olan thisaynı this15.3.4.3.2.a. göre bağlamında olduğu gibi
  • Daha sonra new 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:
    • Len, argArray'in [[Get]] dahili yöntemini "length" bağımsız değişkeniyle çağırmanın sonucu olsun.
  • Bu şekilde, .apply0 ila bağımsız değişkenler geçirerek .lengtharama için, [[Get]]ilgili { length: 5 }değerleri 0 ila 4 verim undefineddizi 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).
  • Dizi yapıcısı 0, 2 veya daha fazla argümanla çağrılır . Yeni oluşturulan dizinin uzunluk özelliği, spesifikasyona göre argüman sayısına ve değerler aynı değerlere ayarlanır.
  • Böylece 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ı undefinedve 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.mapNumber.calldizinin her bir öğesi için geri çağrı işlevini (bu durumda ) çağırır ve belirtilen thisdeğeri kullanır (bu durumda thisdeğeri `Sayı olarak ayarlar ).
  • Haritadaki geri aramanın ikinci parametresi (bu durumda Number.call) dizindir ve ilki bu değerdir.
  • Bu Number, thisas undefined(dizi değeri) ve parametre olarak dizin ile çağrıldığı anlamına gelir . Dolayısıyla, temelde her undefinedbirini kendi dizi indeksine eşlemekle aynıdır (çünkü çağırma Numbertip 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.


1
Dokümanlar için: Haritanın nasıl çalıştığına ilişkin spesifikasyon: es5.github.io/#x15.4.4.19 , Mozilla, developer.mozilla.org/en-US/docs/Web/JavaScript/
Patrick Evans

1
Ama neden sadece birlikte çalışıyor Array.apply(null, { length: 2 })ve Array.apply(null, [2])bu da Arraykurucuyu 2uzunluk değeri olarak geçirmeyi çağırıyor ? fiddle
Andreas

@Andreas 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
Benjamin Gruenbaum

İlk çalıştırmada nasıl çalıştığını anlamadım ... İkinci okumadan sonra mantıklı geliyor. {length: 2}kurucunun Arrayyeni 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 :)
Andreas

5

Dediğin gibi, ilk kısım:

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

5 undefineddeğerden oluşan bir dizi oluşturur .

İkinci kısım, map2 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 mapaslı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:

  • a geçerli yinelenen öğenin değeri olarak
  • b geçerli yinelenen öğenin dizini olarak
  • c tüm orijinal dizi olarak

Alan ikinci argüman map, ilk argüman olarak ilettiğiniz işleve aktarılıyor. Ama a, b, ne de c olması durumunda fooolmazdı 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).

mapGeçen ikinci argüman indeks olduğu için, o indekste yeni diziye yerleştirilecek değer indekse eşittir. Tıpkı bazyukarıdaki örnekteki işlev gibi. Number.calldizini ayrıştırmaya çalışacak - doğal olarak aynı değeri döndürecektir.

mapKodunuzdaki 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.


1
Number.callbağı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 thisiçin-değeri call, alakalı - .map(eval.call, Number), .map(String.call, Number)ve .map(Function.prototype.call, Number)tüm eşdeğerdir.
Bergi

0

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] === undefineddoğ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


1
[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ı mapve bu yüzden garip Array.applykullanıldı.
Bergi
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.