Уводзіны

Уступ

JavaScript Garden гэта растучы набор дакументацыі аб найбольш цікавых частках мовы праграмавання JavaScript. Ён дае парады аб тым як прадухіліць частыя і непрадказальныя памылкі, а таксама праблемы з хуткасцю выконвання і дрэннымі практыкамі, якія праграмісты, не з'яўляючыяся экспертамі у JavaScript маглі сустрэць падчас сваіх пошукаў у глыбіні мовы.

JavaScript Garden не ставіць сваёй мэтай навучыць вас мове JavaScript. Былыя веды мовы рэкамендаваныя, каб вы змаглі зразумець пытанні разглядаемыя ў гэтым мануале. Каб зразумець базавыя рэчы мовы, калі ласка прачытайце цудоўны мануал у сетцы распрацоўшчыкаў Mozilla.

Аўтары

Гэты мануал - праца двух выбітных карыстальнікаў Stack Overflow , Ivo Wetzel (Тэкст) і Zhang Yi Jiang (Дызайн).

На дадзены момант падтрымліваецца Tim Ruffles.

Удзельнікі

Хостынг

JavaScript Garden хосціцца на GitHub, але Cramer Development падтрымлівае нас люстэркам на JavaScriptGarden.info. У Беларускамоўнай версіі таксама ёсць сваё люстэрка на GitHub

Пераклад

Перакладзена на Беларускую мову супольнасцю it-mova.

Ліцэнзія

JavaScript Garden апублікаваны пад MIT ліцэнзіяй і хосціцца на GitHub. Калі вы знойдзеце апячатку або памылку - пазначце памылку або адпраўце pull request у сховішча. Вы таксама можаце знайсці нас у JavaScript room на чаце Stack Overflow.

Аб'екты

Выкарыстанне і ўласцівасці аб'ектаў

Усё ў JavaScript дзейнічае як аб'ект, апроч двух выключэнняў — гэта null і undefined.

false.toString(); // 'false'
[1, 2, 3].toString(); // '1,2,3'

function Foo(){}
Foo.bar = 1;
Foo.bar; // 1

Часта распрацоўшчыкі думаюць, што лічбавыя літэралы не могуць быць выкарыстаны як аб'екты. Гэта праз тое, што сінтаксічны аналізатар JavaScript стараецца прывесці натацыю кропка пасля нумара да літэрала з плаваючай кропкай.

2.toString(); // уздымае SyntaxError

Ёсць некалькі падыходаў, якія могуць дазволіць выкарыстаць лікавыя літэралы як аб'екты'.

2..toString(); // другая кропка распазнаецца слушна
2 .toString(); // заўважце прабел з лева ад кропкі
(2).toString(); // 2 распазнаецца першым чынам

Аб'ект як тып дадзеных

Аб'екты ў JavaScript таксама могуць быць выкарыстаныя як хэш-табліцы; яны ў асноўным складаюцца з іменаваных уласцівасцяў з адпаведнымі значэннямі.

Выкарыстоўваючы натацыю літэрала аб'екта — {} — магчыма стварыць просты аб'ект. Гэты новы аб'ект пашырае Object.prototype і не мае сваіх уласцівасцяў якія былі б вызначыныя.

var foo = {}; // новы пусты аб'ект

// новы аб'ект з уласціваццю 'test', якая мае значэнне 12
var bar = {test: 12};

Доступ да ўласцівасцяў

Доступ да ўласцівасцяў аб'екта можа быць здейснены двумя спосабамі, праз кропкавую натацыю або натацыю з квадратнымі дужкамі.

var foo = {name: 'кацяня'}
foo.name; // кацяня
foo['name']; // кацяня

var get = 'name';
foo[get]; // кацяня

foo.1234; // SyntaxError
foo['1234']; // працуе

Натацыі працуюць амаль што ідэнтычна, з адзінай розніцай у тым, што натацыя з квадратнымі дужкамі дазваляе дынамічную устаноўку ўласцівасцяў і выкарыстанне імёнаў уласцівасцяў, якія інакш прывялі б да сінтаксічных памылак.

Выдаленне ўласцівасцяў

Адзіны спосаб выдаліць уласціваць з аб'екта — гэта выкарыстаць аператар delete; пазначэнне уласціваці як undefined або null толькі прыбірае значэнне звязанае з уласцівацю, але не ключ.

var obj = {
    bar: 1,
    foo: 2,
    baz: 3
};
obj.bar = undefined;
obj.foo = null;
delete obj.baz;

for(var i in obj) {
    if (obj.hasOwnProperty(i)) {
        console.log(i, '' + obj[i]);
    }
}

Вышэй прыведзены код вывядзе bar undefined і foo null — толькі baz быў выдалены і таму адсутнічае ў вывадзе.

Натацыя ключэй

var test = {
    'case': 'Я ключавое слова, таму я павінна быць пазначана як радок',
    delete: 'Я таксама ключавое слова, таму і я' // уздымае SyntaxError
};

Уласцівасці аб'ектаў могуць быць пазначаныя як сімваламі, так і ў выглядзе радкоў. Праз яшчэ адну хібу сінтаксічнага аналізатара JavaScript, вышэй прыведзены код кіне SyntaxError у весіях ранейшых за ECMAScript 5.

Гэта памылка ўздымаецца праз тое, што delete - гэта ключавое слова; такім чынам, яно мае быць пазначана як літэрал радка каб забяспечыць, што яно будзе какрэктна інтэрпрэтавана старымі рухавікамі JavaScript.

Прататып

JavaScript не прадастаўляе класічную мадэль спадкаемства; замест гэтага, ён выкарыстоўвае прататыпную мадэль.

Негледзячы на тое, што гэта лічыцца адной з слабасцяў JavaScript, мадэль прататыпнага спадкаемства больш эфэктыўная за класічную. Напрыклад, даволі трывіальна пабудаваць класічную мадэль паверх прататыпнай мадэлі, у той час як адваротнае было б значна больш складаным.

JavaScript гэта адзіная шырока выкарыстоўваемая мова, якая падтрымлівае прататыпнае спадкаемства, таму можа спатрэбіцца час, каб прызвычаіцца да гэтай мадэлі.

Першая вялікая розніца заключаецца ў тым, што JavaScript выкастроўвае прататыпныя ланужкі.

function Foo() {
    this.value = 42;
}
Foo.prototype = {
    method: function() {}
};

function Bar() {}

// Пазначае прататыпам Bar новы асобнік Foo
Bar.prototype = new Foo();
Bar.prototype.foo = 'Hello World';

// Упэнімся, што Bar з'яўляецца дзейсным канструктарам
Bar.prototype.constructor = Bar;

var test = new Bar(); // стварае новы асобнік Bar

// Выніковы ланцужок прататыпаў
test [instance of Bar]
    Bar.prototype [instance of Foo]
        { foo: 'Hello World' }
        Foo.prototype
            { method: ... }
            Object.prototype
          { toString: ... /* і г.д. */ }

У вышэй прыведзеным кодзе аб'ект test атрымае спадчыну і ад Bar.prototype, і ад Foo.prototype; такім чынам, ён будзе мець доступ да функцыі method, якая вызначана ў Foo. А таксама доступ да ўласцівасці value аднаго унікальнага асобніка Foo, які з'яўляецца яго прататыпам. Важна заўважыць, што new Bar() не стварае новы асобнік Foo, але выкарыстоўвае функцыю, пазначаную яго прататыпам; такім чынам, усе асобнікі Bar будуць выкарыстоўваць тую ж уласціваць value.

Пошук уласцівасцяў

Калі адбываецца зварот да ўласцівасці, JavaScript пройдзе па ўсім ланцужку прататыпаў уверх да таго моманту, як знойдзе ўласціваць з запытаным імем.

У той момант, калі дасягнуты верх ланцужка - а менавіта Object.prototype - і ўсё яшчэ не знойдзена адпаведная ўласцівасць, будзе вернута значэнне undefined.

Уласцівасць prototype

Нягледзячы на тое, што ўласцівасць prototype выкарыстоўваецца мовай, каб пабудаваць ланцужок прататыпаў, магчыма прызначыць яму любое значэнне. Аднак, прызначэнне прымітываў будузе праігнараваным.

function Foo() {}
Foo.prototype = 1; // без эфекту

Прызначэнне аб'ектаў, як паказана ў прыкладзе вышэй, будзе працаваць, і дазволіць дынамічна ствараць ланцужкі прататыпаў.

Хуткасць выканання

Пошук уласцівасцяў, якія знаходзяцца высока ў ланцужку прататыпаў, можа негатыўна адбіцца на хуткасці выканання, і гэта можа быць прыкметным у кодзе, у якім чыннік хуткасці крытычны. У выпадку спробы доступа да неіснуючых уласцівасцяў будзе пройдзены ўвесь ланцужок прататыпаў.

У дадатак, пры ітэрацыі па ўласцівасцях аб'екта кожная уласціваць, што ёсць у ланцужку прататыпаў будзе апрацавана.

Расшырэнне ўбудаваных прататыпаў

Адна з дрэнных магчымасцяў, што сустракаецца даволі часта — расшырэнне прататыпа Object.prototype або аднаго з іншых убудаваных тыпаў.

Такая практыка называецца monkey patching і парушае інкапсуляцыю. Хаця папулярныя фрэймворкі, такія як Prototype шырока выкарыстоўваюць гэтую мачымасць, няма добрых матываў для нагрувашчвання ўбудаваных тыпаў дадатковай нестандартнай функцыянальнасцю.

Адзіным добрым матывам расшырэння убудаваных прататыпаў — гэта дадаванне функцыянала, што з'явіўся у новых рухавіках JavaScript; напрыклад, Array.forEach.

У завяршэнне

Вельмі важна разумець, як працуе мадэль прататыпнага спадкаемства да таго, як пісаць код, які яе выкарыстоўвае. Таксама сачыце за даўжынёй ланцужка прататыпаў і драбіце іх, калі ёсць магчымасць, каб пазбегнуць праблем з прадукцыйнасцю. Таксама ўбудаваныя прататыпы ніколі не павінны расшырацца, акрамя як для таго, каб падтрымаць новыя магчымасці JavaScript.

Метад hasOwnProperty

Каб праверыць, ці ёсць у аб'екта ўласцівасць, вызначаная ў ім самім, а не дзе-небудзь у яго ланцужку прататыпаў, неабходна выкарыстаць метад hasOwnProperty, які ўсе аб'екты ўспадкоўваюць ад Object.prototype.

hasOwnProperty — адзіная функцыя ў JavaScript, якая дазваляе атрымаць уласцівасці аб'екта без зварота да ланцужка прататыпаў.

// Сапсуем Object.prototype
Object.prototype.bar = 1;
var foo = {goo: undefined};

foo.bar; // 1
'bar' in foo; // true

foo.hasOwnProperty('bar'); // false
foo.hasOwnProperty('goo'); // true

Толькі hasOwnProperty дасць правільны чаканы вынік. Паглядзіце секцыю аб цыкле for in для падрабязнейшых звестак аб тым, як выкарыстоўваць hasOwnProperty падчас ітэрацыі па ўласцівасцях аб'екта.

hasOwnProperty як уласцівасць

JavaScript не абараняе ўласцівасць hasOwnProperty; такім чынам, ёсць верагоднасць што ў аб'екта можа быць уласцівасць з такім імем, неабходна выкарыстаць знешні hasOwnProperty для карэктнага выніку.

var foo = {
    hasOwnProperty: function() {
        return false;
    },
    bar: 'Тут жывуць драконы'
};

foo.hasOwnProperty('bar'); // заўсёды верне false

// выкарыстайце hasOwnProperty іншага аб'екта
// і перадайце foo у якасці this
({}).hasOwnProperty.call(foo, 'bar'); // true

// Такасама магчыма выкарыстаць hasOwnProperty з Object.prototype
Object.prototype.hasOwnProperty.call(foo, 'bar'); // true

Заключэнне

Выкарыстоўванне hasOwnProperty ёсць адзіным надзейным спосабам, каб праверыць існаванне ўласцівасці ў аб'екце. Рэкамендуецца выкарыстоўваць hasOwnProperty пры ітэрацыі па ўласцівасцях аб'екта, як апісана ў секцыі цыкла for in .

Цыкл for in

Як і аператар in, цыкл for in праходзіць па ўсім ланцужку прататыпаў пры ітэрацыі па ўласцівасцях аб'екта.

// Атруцім Object.prototype
Object.prototype.bar = 1;

var foo = {moo: 2};
for(var i in foo) {
    console.log(i); // вывядзе і bar і moo
}

Праз тое, што немагчыма памяняць паводзіны самаго цыкла for in, неабходна фільтраваць непажаданыя ўласцівасці аб'екта ўнутры цыкла. У версіях ECMAScript 3 і пазней, гэта можна зрабіць праз метад hasOwnProperty.

Пачынаючы з ECMAScript 5, Object.defineProperty можа быць выкарыстана з enumerable роўным false, каб дадаць уласцівасці аб'екту такім чынам, што яны не будуць пералічаны. У такім выпадку было б справядлівым меркаваць, што любая enumerable уласціваць была дададзена адмыслова і прапусціць hasOwnProperty, бо гэта робіць код больш шматслоўным і цяжэйшым для чытання. У бібліятэчным кодзе hasOwnProperty мае быць усё роўна выкарыстаны, бо не варта рабіць здагадкі аб тым, якія enumerable уласцівасці могуць пражываць у ланцужку прататыпаў.

Выкарыстоўванне hasOwnProperty для фільтрацыі

// возьмем foo з прыкладу
for(var i in foo) {
    if (foo.hasOwnProperty(i)) {
        console.log(i);
    }
}

Гэта адзіная правільная версія выкарыстоўвання цыкла. Дзякуючы выкарыстоўванню hasOwnProperty, быдзе выведзена толькі moo. Калі прыбраць hasOwnProperty, код будзе схільны да памылак у выпадку, калі натыўныя прататыпы — напрыклад, Object.prototype — былі змененыя.

У новых версіях ECMAScript уласцівасці, пазначаныя як не enumerable, могуць быць вызначыныя праз Object.defineProperty, змяншаючы рызыку ітэрацыі праз іх без выкарыстання hasOwnProperty. Тым не менш, трэба быць уважлівым пры выкарыстанні старых бібліятэк, такіх як Prototype, якая не выкарыстроўвае новымя магчымасці ECMAScript. Пры выкарыстоўванні гэтай бібліятэкі, цыклы for in, якія не выкарыстоўваюць hasOwnProperty, гарантавана не будуць працаваць.

У заключэнне

Рэкамендавана заўсёды выкарыстоўваць hasOwnProperty як у ECMAScript 3 або ніжэй, так і ў бібліятэчным кодзе. У гэтых асяродках ніколі не варта рабіць здагадкі аб тым, быў зменены натыўны прататып ці не. Пачынаючы з ECMAScript 5, Object.defineProperty дазваляе пазначаць уласцівасці як не enumerable і прапускаць выкарыстоўванне hasOwnProperty у кодзе праграмы.

Функцыі

Выразы і аб'яўленне функцый

У JavaScript функцыі таксама з'яўляюцца аб'ектамі. Гэта значыць іх можна перадаваць і прысвойваць як і любыя іншыя аб'екты. Адзін, часта выкарыстоўваемы варыянт, гэтай магчымасці - перадача ананімнага метада як функцыі зваротнага выкліку іншай, магчыма асінхроннай функцыі.

Аб'яўленне function

function foo() {}

У вышэй прыведзеным прыкладзе функцыя уздымаецца перад тым як пачынаецца выконванне праграмы; Такім чынам, яна даступная паўсюль у зоне бачнасці, у якой яна была аб'яўлена, нават калі выклік адбываецца да фактычнага аб'яўлення ў кодзе.

foo(); // Працуе, бо функцыя будзе створана да выконвання кода
function foo() {}

function як выраз

var foo = function() {};

У гэтым прыкладзе пераменнай foo прысвойваецца ананімная функцыя.

foo; // 'undefined'
foo(); // уздыме TypeError
var foo = function() {};

Праз тое, што var - гэта аб'яўленне якое уздымае імя пераменнай foo перад тым як код будзе выкананы, foo будзе ўжо аб'яўленым калі ён пачне выконвацца.

Але так як прысвойванні адбываюцца толькі пад час выконвання, значэнне foo будзе змоўчанным (undefined) да выконвання адпаведнага кода.

Выразы з іменаванымі функцыямі

Яшчэ адзін выбітны выпадак - прысвойванне іменавай функцыі.

var foo = function bar() {
    bar(); // працуе
}
bar(); // ReferenceError

Тут, bar не даступны ў знешнім скоўпе, бо функцыя толькі прысвойваецца пераменнай foo; аднак, унутры bar, імя даступнае. Так адбываецца праз асаблівасці працы з прастранствамі імён у JavaScript - імя функцыі заўсёды даступнае ў лакальным скоўпе функцыі.

Як працуе this

У JavaScript this азначае канцэптуальна іншую рэч, чым у іншых мовах праграмавання. Існуе роўна пяць варыянтаў таго, да чаго можа быць прывязана this.

Глабальна зона бачнасці

this;

Калі this выкарыстоўваецца ў глабальнай зоне бачнасці - яна спасылаецца на глабальны аб'ект

Выклік функцыі

foo();

Тут, this усё яшчэ спасылаецца на глабальны аб'ект.

Выклік метада

test.foo();

У гэтым выпадку, this будзе спасылацца на аб'ект test.

Выклік канструктара

new foo();

Выклік функцыі, перад якім прысутнічае ключавое слова new, выступае ў якасці канструктара. Унутры функцыі, this будзе спасылацца на новаствораны аб'ект.

Яўная ўстаноўка this

function foo(a, b, c) {}

var bar = {};
foo.apply(bar, [1, 2, 3]); // масіў разгорнецца ў ніжэйпрыведзенае
foo.call(bar, 1, 2, 3); // вынікам будзе a = 1, b = 2, c = 3

Пры выкарыстоўванні метадаў call або apply з Function.prototype, значэнню this унутры выкліканай функцыі яўна прысвойваецца значэнне першага аргумента адпаведнага выкліка функцыі.

Як вынік, у вышэйпрыведзеным прыкладзе правіла метада не працуе, і this унутры foo будзе мець значэнне bar.

Магчымыя пасткі

Не гледзячы на тое, што большасць гэтых прыкладаў лагічныя, першы можна лічыць яшчэ адным недаглядам мовы, бо ён ніколі не мае практычнага прымянення.

Foo.method = function() {
    function test() {
        // this спасылаецца на глабальны аб'ект
    }
    test();
}

Памылковым меркаваннем будзе тое, што this унутры test будзе спасылацца на Foo; Але на самрэч гэта не так.

Каб атрымаць доступ да Foo з цела test, вы можаце стварыць лакальную пераменную унутры метада што будзе спасылацца на Foo.

Foo.method = function() {
    var self = this;
    function test() {
        // Тут выкарыстоўвайце self замест this
    }
    test();
}

self гэта звычайнае імя пераменнай, але яно часта выкарыстоўваецца для спасылкі на знешні this. У камбінацыі з замыканнямі, яно можа быць выкарыстана для перадачы this навокал.

У ECMAScript 5 можна выкарыстаць метад bind у камбінацыі з ананімнай функцыяй, дзеля таго каб атрымаць аналагічны вынік.

Foo.method = function() {
    var test = function() {
        // this цяпер спасылаецца на Foo
    }.bind(this);
    test();
}

Прысвойванне метадаў

Яшчэ адна рэч якая не працуе ў JavaScript - гэта стварэнне псэўданімаў функцый, то бок прысвойванне значэння метада пераменнай.

var test = someObject.methodTest;
test();

Паводле першага правіла, test цяпер працуе як звычайны выклік функцыі; адпаведна, this унутры больш не будзе спасылацца на someObject.

Поздняе звязванне this можа падацца дрэннай ідэяй, але насамрэч якраз дзякуючы гэтаму працуе спадкаемства прататыпаў.

function Foo() {}
Foo.prototype.method = function() {};

function Bar() {}
Bar.prototype = Foo.prototype;

new Bar().method();

Калі выклікаецца method новага экзэмпляра Bar, this будзе спасылацца на гэты экзэмпляр.

Замыканні і спасылкі

Адна з найбольш магутных магчымасцяў JavaScript — магчымасць ствараць замыканні. Зона бачнасці замыканняў заўсёды мае доступ да знешняй зоны бачнасці, у якой замыканне было аб'яўлена. З той прычыны, што ў JavaScript адзіны механізм працы з зонай бачнасці — гэта зоны бачнасці функцыі, усе функцыі выступаюць у якасці замыканняў.

Эмуляцыя прыватных пераменных

function Counter(start) {
    var count = start;
    return {
        increment: function() {
            count++;
        },

        get: function() {
            return count;
        }
    }
}

var foo = Counter(4);
foo.increment();
foo.get(); // 5

Тут Counter вяртае два замыканні: функцыю increment і функцыю get. Абедзьве функцыі маюць спасылку на зону бачнасці Counter і таму заўсёды маюць доступ да пераменнай count, што была аб'яўлена ў гэтай зоне бачнасці.

Якім чынам гэта працуе

З той прычыны, што ў JavaScript немагчыма спасылацца або прысвойваць зоны бачнасці, немагчыма атрымаць доступ да пераменнай count звонку. Адзіны спосаб узаемадзейнічаць з ім — выкарыстоўваць два замыканні.

var foo = new Counter(4);
foo.hack = function() {
    count = 1337;
};

Вышэйпрыведзены код не памяняе значэнне пераменнай count у зоне бачнасці Counter, бо foo.hack не быў аб'яўлены у гэтай зоне бачнасці. Замест гэтага ён створыць або перазапіша глабальную пераменную count.

Замыканні ўнутры цыклаў

Частая памылка - выкарыстанне замыканняў унутры цыклаў, як быццам бы яны капіруюць значэнне пераменнай індэксу цыкла.

for(var i = 0; i < 10; i++) {
    setTimeout(function() {
        console.log(i);  
    }, 1000);
}

Вышэйпрыведзены код не выведзе нумары ад 0 да 9, ён проста выведзе нумар 10 дзесяць разоў.

Ананімная функцыя захоўвае спасылку на i. У той час, калі функцыя console.log выклікаецца, цыкл for ужо адпрацаваў, а значэнне i ўжо стала 10.

Каб атрымаць пажаданыя паводзіны, неабходна стварыць копію значэння i.

Як абыйсці праблемы спасылкі

Каб стварыць копію значэння пераменнай індэкса цыкла, лепшы спосаб — стварэнне ананімнай абгорткі.

for(var i = 0; i < 10; i++) {
    (function(e) {
        setTimeout(function() {
            console.log(e);  
        }, 1000);
    })(i);
}

Знешняя ананімная функцыя выконваецца імгненна з i ў якасці першага аргумента і атрымае копію значэння i ў якасці параметра e.

Ананімная функцыя, што перадаецца метаду setTimeout, цяпер мае спасылку на e, чыё значэнне не мяняецца на працягу цыкла.

Яшчэ адзін спосаб атрымаць такі вынік — вяртаць функцыю з ананімнай абгорткі, што будзе паводзіць сябе такім жа чынам, як і папярэдні прыклад.

for(var i = 0; i < 10; i++) {
    setTimeout((function(e) {
        return function() {
            console.log(e);
        }
    })(i), 1000)
}

Яшчэ адзін папулярны спосаб дасягнуць гэтага — дадаць яшчэ адзін аргумент выкліку функцыі setTimeout, якая перадасць агрумент функцыі зваротнага выкліку.

for(var i = 0; i < 10; i++) {
    setTimeout(function(e) {
        console.log(e);  
    }, 1000, i);
}

Некаторыя старыя асяродкі JS (Internet Explorer 9 і ніжэй) не падтрымліваюць гэтую магчымасць.

Таксама магчыма выканаць гэта выкарыстоўваючы .bind, якая можа звязаць this і аргументы функцыі. Ніжэйпрыведзены прыклад працуе як папярэднія.

for(var i = 0; i < 10; i++) {
    setTimeout(console.log.bind(console, i), 1000);
}

Аб'ект arguments

У зоне бачнасці любой функцыі JavaScript ёсць доступ да адмысловай пераменнай arguments. Гэтая пераменная утрымлівае спіс усіх аргументаў, што былі перададзеныя функцыі.

Аб'ект arguments не з'яўляецца спадкаемцам Array. Ён мае падабенствы з масівам, напрыклад уласцівасць length. Але ён не ўспадкоўвае Array.prototype, а ўяўляе з сябе Object.

Таму немагчыма выклікаць стандартныя метады push, pop або slice у аб'екта arguments. Тым не менш, ітэрацыя з звычайным цыклам for працуе карэктна. Неабходна канвертаваць яго ў сапраўдны аб'ект Array, каб прымяніць стандартныя метады масіваў.

Канвертацыя ў масіў

Ніжэйпрыведезны код верне новы масіў, які будзе ўтрымліваць усе элементы аб'екта arguments.

Array.prototype.slice.call(arguments);

Такая канвертацыя марудная, яе не рэкамендуецца выкарыстоўваць у крытычных у плане прадукцыйнасці частках кода.

Перадача arguments

Ніжэй прадстаўлены рэкамендаваны спосаб перадачы аргументаў з адной функцыі ў іншую.

function foo() {
    bar.apply(null, arguments);
}
function bar(a, b, c) {
    // тут робім што-небудзь
}

Яшчэ адзін прыём — гэта выкарыстанне call і apply разам, каб ператварыць метады, што выкарыстоўваюць значэнне this як і свае аргументы, у звычайныя функцыі, што выкарыстоўваюць толькі аргументы.

function Person(first, last) {
  this.first = first;
  this.last = last;
}

Person.prototype.fullname = function(joiner, options) {
  options = options || { order: "western" };
  var first = options.order === "western" ? this.first : this.last;
  var last =  options.order === "western" ? this.last  : this.first;
  return first + (joiner || " ") + last;
};

// Ствараем незвязаную версію "fullname", што можа быць выкарыстана з любым
// аб'ектам, які мае ўласцівасці 'first' і 'last', перададзеным у якасці
// першага параметра. Гэтую абгортку не трэба будзе мяняць, калі колькасць або
// парадак аргументаў fullname зменяцца.
Person.fullname = function() {
  // Result: Person.prototype.fullname.call(this, joiner, ..., argN);
  return Function.call.apply(Person.prototype.fullname, arguments);
};

var grace = new Person("Grace", "Hopper");

// 'Grace Hopper'
grace.fullname();

// 'Turing, Alan'
Person.fullname({ first: "Alan", last: "Turing" }, ", ", { order: "eastern" });

Фармальныя параметры і індэксы аргументаў

Аб'ект arguments стварае гэтэр і сэтэр як да кожнай са сваіх уласцівасцяў, так і да фармальных параметраў функцыі.

У выніку змена значэння фармальнага параметра зменіць таксама адпаведную ўласцівасць аб'екта arguments, і наадварот.

function foo(a, b, c) {
    arguments[0] = 2;
    a; // 2

    b = 4;
    arguments[1]; // 4

    var d = c;
    d = 9;
    c; // 3
}
foo(1, 2, 3);

Міфы і праўда аб прадукцыйнасці

Адзінае, калі arguments не ствараецца, — гэта калі ёсць фармальны аргумент функцыі або пераменная ўнутры яе з такім іменем. Не важна, выкарыстоўваюцца яны ці не.

Як гэтэры, так і сэтэры ствараюцца заўсёды, таму іх выкарыстоўванне не мае амаль ніякага ўплыву на прадукцыйнасць.

Тым не менш, ёсць адна рэч, якая можа жахліва знізіць прадукцыйнасць у сучасных рухавіках JavaScript — гэта выкарыстанне arguments.callee.

function foo() {
    arguments.callee; // робім што-небудзь з функцыяй foo
    arguments.callee.caller; // і з функцыяй, якая выклікала foo
}

function bigLoop() {
    for(var i = 0; i < 100000; i++) {
        foo(); // Звычайна ўстаўляецца...
    }
}

У вышэйпрыведзеным кодзе foo больш не можа быць устаўлена, бо ёй трэба ведаць аб сабе і аб функцыі, што яе выклікала. Гэта не толькі знішчае павышэнне прадукцыйнасці, якое магло адбыцца дзякуючы ўстаўцы, але і парушае інкапсуляцыю, бо функцыя цяпер залежыць ад спецыфічнага кантэксту, які яе выклікае.

Выкарыстоўванне arguments.callee або яго ўласцівасцяў вельмі непажадана.

Канструктары

Канструктары ў JavaScript таксама адрозніваюцца ад большасці іншых моваў. Любы выклік функцыі, якому папярэднічае ключавое слова new з'яўляецца канструктарам.

Унутры канструктара (выкліканай функцыі) - значэнне this спасылаецца на новаствораны аб'ект. Прататыпам новага аб'екта прызначаецца prototype функцыі, што была выклікана ў якасці канструктара.

У выпадку, калі выкліканая функцыя не вяртае яўнага значэння праз return, будзе не яўна вернута значэнне this, то бок новы аб'ект.

function Person(name) {
    this.name = name;
}

Person.prototype.logName = function() {
    console.log(this.name);
};

var sean = new Person();

У гэтым прыкладзе Person выклікаецца ў якасці канструктара, адпаведна prototype створанага аб'екта будзе прывязаны да Person.prototype.

Вярнуць яўнае значэнне праз return, можна толькі калі гэта значэнне - Object.

function Car() {
    return 'ford';
}
new Car(); // новы аб'ект, не 'ford'

function Person() {
    this.someValue = 2;

    return {
        name: 'Charles'
    };
}
new Person(); // вяртае аб'ект ({name:'Charles'}), які не ўтрымлівае someValue

Калі ключавое слова new прапушчана, функцыя не верне аб'ект.

function Pirate() {
    // пазначыць значэнне ў глабальным аб'екце!
    this.hasEyePatch = true;
}
var somePirate = Pirate(); // somePirate == undefined

Гэты прыклад можа спрацаваць у некаторых выпадках, праз тое як працуе this у JavaScript. Значэннем this тут будзе глабальны аб'ект.

Фабрыкі

Каб мець магчымасць прапусціць ключавое слова new, канструктар функцыі мае яўна вяртаць значэнне.

function Robot() {
    var color = 'gray';
    return {
        getColor: function() {
            return color;
        }
    }
}

new Robot();
Robot();

Абодва выклікі Robot вернуць тое ж самае, новы аб'ект, які мае ўласцівасць getColor, што з'яўляецца замыканнем.

Таксама варта адзначыць, што выклік new Robot() не ўплывае на прататып вернутага аб'екта. Хаця прататып будзе прызначаны новастворанаму аб'екту, Robot ніколі не верне гэты аб'ект.

У прыкладзе вышэй, няма розніцы паміж выклікам функцыі з аператарам new або без яго.

Стварэнне новых аб'ектаў з выкарыстаннем фабрык

Часта рэкамендуюць не выкарыстоўваць new бо забыўшыся выкарыстаць яго, можна стварыць памылку.

Каб стварыць новы аб'ект, лепш выкарыстоўваць фабрыку і стварыць новы аб'ект унутры фабрыкі.

function CarFactory() {
    var car = {};
    car.owner = 'nobody';

    var milesPerGallon = 2;

    car.setOwner = function(newOwner) {
        this.owner = newOwner;
    }

    car.getMPG = function() {
        return milesPerGallon;
    }

    return car;
}

Хоць гэты прыклад і спрацуе негледзячы на забытае new, і бясспрэчна выкарыстоўвае прыватныя пераменныя, ён мае некалькі недахопаў.

  1. Ён выкарыстоўвае больш памяці, бо функцыі створаных аб'ектаў не захоўваюццца у прататыпе, а ствараюцца на нова для кожнага аб'екта.
  2. Каб эмуляваць спадкаемства, фабрыка мае скапіраваць метады іншага аб'екта, або пазначыць прататыпам новага аб'екта стары.
  3. Разрыў ланцужка прататыпаў, проста па прычыне забытага ключавога слова new, не адпавядае духу мовы JavaScript.

У заключэнне

Негледзячы на тое, што прапушчанае new можа выліцца ў памылку, гэта не прычына адмовіцца ад выкарыстання прататыпаў. У выніку лепш высвятліць якое рашэнне больш адпавядае патрабаванням праграмы. Асабліва важна выбраць пэўны стыль і паслядоўна выкарыстоўваць яго.

Зоны бачнасці і прасторы імёнаў

Негледзячы на тое, што JavaScript добра працуе з сінтаксісам фігурных дужак для блокаў, у ім няма падтрымкі блочнай зоны бачнасці; усё што ёсць на гэты конт у мове - зона бачнасці функцыі.

function test() { // зона бачнасці
    for(var i = 0; i < 10; i++) { // не зона бачнасці
        // лічым
    }
    console.log(i); // 10
}

Таксама JavaScript не падтрымлівае выразныя прасторы імёнаў, усё аб'яўляецца ў агульнадаступнай прасторы імёнаў.

Для кожнай спасылкі на пераменную, JavaScript пойдзе ўверх па ўсіх зонах бачнасці, пакуль не знойдзе яе. У выпадку, калі ён дойдзе да глабальнай прасторы імён і ўсё яшчэ не знойдзе неабходнае імя, ён уздыме ReferenceError.

Атрута глабальнымі пераменнымі

// скрыпт A
foo = '42';

// скрыпт B
var foo = '42'

Вышэйпрыведзеныя скрыпты маюць розныя вынікі. Скрыпт A аб'яўляе пераменную foo у глабальнай зоне бачнасці, скрыпт B аб'яўляе foo у актуальнай зоне бачнасці.

Паўторымся, гэта абсалютна не той жа самы вынік: не выкарыстоўваенне var можа мець сур'ёзныя наступствы.

// глабальная зона бачнасці
var foo = 42;
function test() {
    // лакальная зона бачнасці
    foo = 21;
}
test();
foo; // 21

З-за таго, што аператар var прапушчаны ўнутры функцыі test, значэнне foo у глабальнай прасторы імён будзе перазапісаным. Хаця першапачаткова гэта можа падацца невялікай праблемай, не выкарыстоўванне var у кодзе на тысячы радкоў, прывядзе да жахлівых, цяжкіх для адладкі памылак.

// глабальная прастора імёнаў
for(var i = 0; i < 10; i++) {
    subLoop();
}

function subLoop() {
    // прастора імёнаў subLoop
    for(i = 0; i < 10; i++) { // аператар var прапушчаны
        // робім чароўныя рэчы!
    }
}

Знешні цыкл скончыцца пасля першага выкліка subLoop, бо subLoop перазапісвае глабальную пераменную i. Выкарыстоўваючы var для другога цыкла for можна было б пазбегнуць памылкі. Ніколі не прапускайце аператар var, акрамя выпадкаў, калі змена дадзеных у знешняй зоне бачнасці ёсць пажаданым вынікам.

Лакальныя пераменныя

Адзіная крыніца лакальных пераменных у JavaScript гэта параметры функцыі і пераменныя аб'яўленыя праз аператар var.

// глабальная зона бачнасці
var foo = 1;
var bar = 2;
var i = 2;

function test(i) {
    // лакальная зона бачнасці функцыі test
    i = 5;

    var foo = 3;
    bar = 4;
}
test(10);

foo і i гэта лакальныя пераменныя унутры зоны бачнасці функцыі test, а вось прызначэнне bar перазапіша глабальныю пераменную з тым жа іменем.

Падыманне

JavaScript падымае аб'яўленні. Гэта азначае, што абодва аб'яўленні аператараў var і function падымуцца на верх іх зоны бачнасці.

bar();
var bar = function() {};
var someValue = 42;

test();
function test(data) {
    if (false) {
        goo = 1;

    } else {
        var goo = 2;
    }
    for(var i = 0; i < 100; i++) {
        var e = data[i];
    }
}

Вышэйпрыведзены код трансфармуецца перад пачаткам выконвання. JavaScript падымае аператары var, як і аб'яўленне function, наверх бліжэйшай зоны бачнасці.

// аператар var перамяшчаецца сюды
var bar, someValue; // па змоўчванню - 'undefined'

// аб'яўленне функцыі таксама падымаецца наверх
function test(data) {
    var goo, i, e; // адсутная блочная зона бачнасці перайшла сюды
    if (false) {
        goo = 1;

    } else {
        goo = 2;
    }
    for(i = 0; i < 100; i++) {
        e = data[i];
    }
}

bar(); // падае з TypeError бо ўсё яшчэ 'undefined'
someValue = 42; // прысвойванні не падымаюцца
bar = function() {};

test();

Адсутнасць блочнай зоны бачнасці не толькі падыме аператар var па-за межы цыкла і яго цела, але таскама зробіць вынік некаторых канструкцый if не-інтуітыўным.

Хоць у арыгінальным кодзе падаецца што канструкцыя if змяняе глабальную пераменную goo, на дадзены момант гэта мяняе лакальную пераменную - пасля таго, як было прыменена падыманне.

Без ведаў аб падыманні, можна падумаць што код ніжэй кіне ReferenceError.

// правярае ці было SomeImportantThing праініцыалізавана
if (!SomeImportantThing) {
    var SomeImportantThing = {};
}

Але канешне, гэта працуе праз тое, што аператар var быў падняты на верх глабальнай зоны бачнасці.

var SomeImportantThing;

// тут нейкі код можа ініцыалізаваць SomeImportantThing, або не

// тут у гэтым можна ўпэўніцца
if (!SomeImportantThing) {
    SomeImportantThing = {};
}

Парадак доступу да пераменных

Усе зоны бачнасці ў JavaScript, уключаючы глабальную зону бачнасці, маюць адмысловае імя this, аб'яўленае ўнутры іх, якое спасылаецца на актуальны аб'ект.

Зоны бачнасці функцый таксама маюць імя arguments, аб'яўленае ў іх, якое спасылаецца на аргументы, што былі перададзеныя ў функцыю.

Напрыклад, калі паспрабаваць атрымаць доступ да пераменнай foo унутры зоны бачнасці функцыі, JavaScript будзе шукаць імя ў наступным парадку:

  1. У выпадку калі прысутнічае канструкцыя var foo у актуальнай зоне бачнасці, ёна і выкарыстоўваецца.
  2. Калі параметр функцыі мае імя foo, ён будзе выкарыстаны.
  3. Калі сама функцыя называецца foo, яна будзе выкарыстана.
  4. Пераходзіць у знешнюю зону бачнасці, і пачынае з пункта #1.

Прасторы імёнаў

Вялікая праблема, звязаная з выкарыстоўваннем глабальнай прасторы імёнаў, гэта высокая верагоднасць перасячэння імёнаў пераменных. У JavaScript, гэта праблема можа быць лёгка пазбегнута праз выкарыстанне ананімных абгортак.

(function() {
    // аўтаномная "прастора імён"

    window.foo = function() {
        // адкрытае замыканне
    };

})(); // імгненнае выкананне функцыі

Ананімныя функцыі з'яўляюцца выразамі; таму каб быць выкліканымі, яны спачатку маюць быць ацэненымі.

( // ацэньваем функцыю ўнутры дужак
function() {}
) // вяртаем аб'ект функцыі
() // выклік выніку ацэнкі

Ёсць і іншыя спосабы ацаніць і імгненна выклікаць выраз функцыі, хаця яны і адрозніваюцца па сінтаксісу, паводзяць сябе аднолькава.

// Яшчэ некалькі спосабаў наўпрост выклікаць функцыю
!function(){}()
+function(){}()
(function(){}());
// і так далей...

Заключэнне

Рэкамендуецца заўсёды выкарыстоўваць ананімную абгортку каб інкапсуліраваць код у яго асабістай прасторы імёнаў. Гэта не толькі абараняе код ад перасячэння імёнаў, але і дапамагае падзяляць праграму на модулі.

Таксама выкарыстанне глабальных пераменных лічыцца дрэннай практыкай. Любое іх выкарыстоўванне - прыкмета дрэнна напісанага кода, схільнага да памылак, і цяжага ў падтрымцы.

Масівы

Ітэрацыі па масівам і ўласцівасці

Хоць масівы ў JavaScript — аб'екты, няма добрых падставаў для таго, каб выкарыстоўваць цыкл for in для ітэрацыі па масівах. Фактычна, ёсць шэраг добрых падстаў супраць гэтага.

З той прычыны, што цыкл for in пералічвае ўсе ўласцівасці, якія ёсць у ланцужку прататыпаў, і таму, што адзіны спосаб выключыць гэтыя значэнні — hasOwnProperty, ітэрацыя атрымліваецца ў 20 разоў марудней за звычайны цыкл for.

Ітэрацыя

Для таго, каб атрымаць найлепшую прадукцыйнасць у ітэрацыі па масіву, лепш выкарыстаць класічны цыкл for.

var list = [1, 2, 3, 4, 5, ...... 100000000];
for(var i = 0, l = list.length; i < l; i++) {
    console.log(list[i]);
}

У вышэйпрыведзеным прыкладзе ёсць яшчэ адзін прыём, з дапамогай якога можна кэшаваць памер масіва: l = list.length.

Негледзячы на тое, што ўласцівасць length вызначана ў самім масіве, пошук гэтай уласцівасці накладвае выдаткі на пошук пры кожнай ітэрацыі цыкла. І хоць новыя рухавікі JavaScript могуць прымяніць аптымізацыю у гэтым выпадку, няма магчымасці дакладна ведаць, ці будзе код выкананы на гэтых новых рухавіках.

Фактычна, адсутнасць кэшавання можа зрабіць выкананне цыкла ў два разы больш марудным, чым з кэшаваным 'length'.

Уласцівасць length

Хоць гэтэр уласцівасці length проста вяртае колькасць элементаў, што знаходзяцца у масіве, сэтэр можа быць выкарыстаны для абразання масіва.

var arr = [1, 2, 3, 4, 5, 6];
arr.length = 3;
arr; // [1, 2, 3]

arr.length = 6;
arr.push(4);
arr; // [1, 2, 3, undefined, undefined, undefined, 4]

Прысвойванне ўласцівасці 'length' меншага значэння абразае масіў. Прысваенне большага значэнне створыць разрэджаны масіў.

У заключэнне

Для лепшай прадукцыйнасці рэкамендуецца заўсёды выкарыстоўваць звычайны цыкл for, і кэшаваць уласціваць length. Выкарыстоўванне for in для ітэрацыі па масіву — прыкмета дрэнна напісанага коду, схільнага да памылак і дрэннай прадукцыйнасці.

Канструктар Array

Праз тое, што канструктар Array неадназначна апрацоўвае свае параметры, крайне рэкамендуецца выкарыстоўваць літэрал - [] - для стварэння масіваў.

[1, 2, 3]; // Вынік: [1, 2, 3]
new Array(1, 2, 3); // Вынік: [1, 2, 3]

[3]; // Вынік: [3]
new Array(3); // Вынік: []
new Array('3') // Вынік: ['3']

У выпадку, калі канструктару Array перадаецца толькі адзін параметр, і калі гэты аргумент тыпу Number, канструктар верне разрэджаны масіў, які мае уласціваць length са значэннем аргумента. Варта адзначыць, што такім чынам будзе зменена толькі значэнне ўласцівасці length масіва; індэксы масіва не будуць праініцыялізаваныя.

var arr = new Array(3);
arr[1]; // undefined
1 in arr; // false, індэкс не праініцыялізаваны

Магчымасць загадзя вызначыць даўжыню масіва карысна толькі ў рэдкіх выпадках, напрыклад, паўтор радка без выкарыстання цыкла.

new Array(count + 1).join(stringToRepeat);

У заключэнне

Літэралы маюць перавагі над канструктарам Array. Яны карацейшыя, маюць больш чысты сінтаксіс і робяць код больш чытэльным.

Тыпы

Роўнасць і параўнанне

У JavaScript роўнасць значэнняў аб'ектаў можна вызначыць двумя спосабамі.

Аператар роўнасці

Аператар роўнасці складаецца з двух сімвалаў 'роўна': ==

JavaScript мае слабую тыпізацыю. Гэта значыць што аператар роўнасці прыводзіць тыпы аб'ектаў, каб параўнаць іх.

""           ==   "0"           // false
0            ==   ""            // true
0            ==   "0"           // true
false        ==   "false"       // false
false        ==   "0"           // true
false        ==   undefined     // false
false        ==   null          // false
null         ==   undefined     // true
" \t\r\n"    ==   0             // true

Вышэй прыведзеная табліца паказвае вынікі прывядзення тыпаў, і гэта галоўная прычына па якой выкаростоўванне == лічыцца дрэннай практыкай. Яно прыводзіць да памылак якія цяжка адсачыць праз складаны механізм прывядзення тыпаў.

Акрамя гэтага, прывядзенне тыпаў таксама ўплывае на вытворчасць; напрыклад, радок мае быць ператвораны ў нумар, перад тым як быць параўнаным з іншым нумарам.

Аператар строгай роўнасці

Аператар строгай роўнасці складаецца з трох сімвалаў 'роўна': ===.

Ён дзейнічае як звычайны аператар роўнасці, за выключэннем таго, што строгая роўнасць не прыводзіць аперанды да агульнага тыпу.

""           ===   "0"           // false
0            ===   ""            // false
0            ===   "0"           // false
false        ===   "false"       // false
false        ===   "0"           // false
false        ===   undefined     // false
false        ===   null          // false
null         ===   undefined     // false
" \t\r\n"    ===   0             // false

Вышэй прыведзеныя вынікі значна больш зразумелыя і даюць магчымасць хутчэй выявіць памылкі ў кодзе. Гэта паляпшае код, а таксама дае прырост вытворчасці, у выпадку калі аперанды розных тыпаў.

Параўнанне аб'ектаў

Хоць абодва аператар == і === называюцца аператарамі роўнасці, яны паводзяць сабе па рознаму калі хоць адзін аперанд тыпа Object.

{} === {};                   // false
new String('foo') === 'foo'; // false
new Number(10) === 10;       // false
var foo = {};
foo === foo;                 // true

Тут абодва аператанда параўноўваюцца на ідэнтычнасць, а не на роўнасць; то бок будзе праверана, ці з'яўляюцца яны адным экзэмплярам аб'екта. Гэтак жа, як is у Python, або параўнанне ўказальнікаў у C.

У заключэнне

Настойліва рэкамендуецца выкарыстоўваць толькі аператар строгай роўнасці. У выпадку, калі тыпы маюць быць прыведзеныя, гэта варта рабіць яўна, а не пакідаць іх на сумленні складаных правілаў прывядзення мовы праграмавання.

Аператар typeof

Аператар typeof (разам з instanceof) магчыма найбольшая хіба мовы JavaScript, таму што ён амаль што цалкам зламаны.

Хаця instanceof усё яшчэ мае абмежаванае ўжыванне, typeof можа быць выкарыстаны толькі з адной мэтай, і гэта дарэчы не праверка тыпа.

Табліца тыпаў JavaScript

Значэнне            Клас       Тып
-------------------------------------
"foo"               String     string
new String("foo")   String     object
1.2                 Number     number
new Number(1.2)     Number     object
true                Boolean    boolean
new Boolean(true)   Boolean    object
new Date()          Date       object
new Error()         Error      object
[1,2,3]             Array      object
new Array(1, 2, 3)  Array      object
new Function("")    Function   function
/abc/g              RegExp     object (function in Nitro/V8)
new RegExp("meow")  RegExp     object (function in Nitro/V8)
{}                  Object     object
new Object()        Object     object

У вышэй прыведзенай табыліцы, Тып паказвае значэнне вернутае аператарам typeof. Як можна пабачыць, гэта значэнне абсалютна не кансістэнтнае.

Клас паказвае значэнне ўнутраннай уласцівасці [[Class]] аб'екта.

Клас аб'екта

Адзіны спосаб атрымаць значэнне [[Class]] аб'екта - выклікаць метад Object.prototype.toString. Ён верне радок у наступным фармаце: '[object ' + valueOfClass + ']', напрыклад [object String] або [object Array]:

function is(type, obj) {
    var clas = Object.prototype.toString.call(obj).slice(8, -1);
    return obj !== undefined && obj !== null && clas === type;
}

is('String', 'test'); // true
is('String', new String('test')); // true

У вышэйпрыведзеным прыкладзе, Object.prototype.toString выклікаецца са значэннем this пазначаным як аб'ект чыё значэнне [[Class]] мае быць атрыманым.

Праверка вызначанасці пераменных

typeof foo !== 'undefined'

Вышэйпрыведзены код праверыць ці было вызначана foo; просты зварот да пераменнай прывядзе да ReferenceError. Гэта адзінае для чаго карысны typeof.

У заключэнне

Каб праверыць тып аб'екта, настойліва рэкамендуецца выкарыстоўваць Object.prototype.toString - гэта адзіны надзейны спосаб. Як паказана ў вышэйпрыведзенай табліцы, некаторыя значэнні вернутыя аператарам typeof не вызначаныя ў спецыфікацыі; такім чынам, яны могуць быць рознымі ў розных рэалізацыях.

Акрамя як для праверкі вызначанасці пераменнай, typeof мае быць пазбегнуты.

Аператар instanceof

Аператар instanceof параўноўвае канструктары двух аперандаў. Гэта карысна толькі для параўнання аб'ектаў не ўбудаваных тыпаў. Выкарыстоўванне на ўбудаваных тыпах не мае сэнсу, як і аператар typeof.

Параўнанне адвольных аб'ектаў

function Foo() {}
function Bar() {}
Bar.prototype = new Foo();

new Bar() instanceof Bar; // true
new Bar() instanceof Foo; // true

// Калі толькі прысвоім Bar.prototype аб'ект функцыі Foo,
// але не самаго экзэмпляра Foo
Bar.prototype = Foo;
new Bar() instanceof Foo; // false

Выкарыстоўванне instanceof з убудаванымі тыпамі

new String('foo') instanceof String; // true
new String('foo') instanceof Object; // true

'foo' instanceof String; // false
'foo' instanceof Object; // false

Варта адзначыць, што instanceof не працуе на аб'ектах, якія паходзяць з розных кантэкстаў JavaScript (напрыклад, розных дакументаў у web-браузеры), бо іх канструктары насамрэч не будуць канструктарамі тых самых аб'ектаў.

У заключэнне

Аператар instanceof мае быць выкарыстаны толькі для працы з аб'ектамі не ўбудаваных тыпаў якія паходзяць з аднаго кантэкста JavaScript. Як і ў выпадку з аператарам typeof, трэба пазбягаць любога іншага яго выкарыстання.

Прывядзенне тыпаў

JavaScript - слаба тыпізаваная мова, таму прывядзенне тыпаў адбываецца паўсюль дзе магчыма.

// Гэтыя равенствы - праўдзівыя
new Number(10) == 10; // аб'ект тыпа Number пераўтвараецца у
                      // лікавы прымітыў, праз няяўны выклік
                      // метада Number.prototype.valueOf

10 == '10';           // Strings пераўтвараецца ў Number
10 == '+10 ';         // Троху вар'яцтва з радкамі
10 == '010';          // і яшчэ
isNaN(null) == false; // null пераўтвараецца ў 0
                      // які вядома ж не NaN

// Гэтыя равенствы - ілжывыя
10 == 010;
10 == '-10';

Каб пазбегнуць вышэйпрыведзеных праблемаў, настойліва ракамендуецца выкарыстоўваць аператар строгай роўнасці. Зрэшты, хоць гэта і пазбаўляе ад многіх распаўсюджаных праблемаў, існуе яшчэ шмат праблемаў, які ўзнікаюць праз слабую тыпізацыю JavaScript.

Канструктары ўбудаваных тыпаў

Канструктары ўбудаваных тыпаў, напрыклад, Number і String паводзяць сябе па рознаму, у залежнасці ад таго, выклікаюцца яны з ключавым словам new або без яго.

new Number(10) === 10;     // False, Object і Number
Number(10) === 10;         // True, Number і Number
new Number(10) + 0 === 10; // True, праз неяўнае прывядзенне

Выкарыстанне ўбудаванага тыпу, такога як Number у якасці канструкта створыць новы экзэмпляр аб'екта Number, але пры адсутнасці ключавога слова new функцыя Number будзе паводзіць сябе як канвертар.

У дадатак, выкарытоўванне літэралаў, або значэнняў якія не з'яўляюцца аб'ектамі прывядзе да дадатковых прывядзенняў тыпаў.

Лепшы варыянт - гэта яўнае прывядзенне да аднаго з трох магчымых тыпаў.

Прывядзенне да радка

'' + 10 === '10'; // true

Праз даданне да значэння пустога радка, яно лёгка прыводзіцца да радка.

Прывядзенне да лікавага тыпу

+'10' === 10; // true

Выкарыстоўваючы унарны аператар плюс, магчыма пераўтварыць значэнне ў нумар.

Прывядзенне да булевага тыпу

Выкарыстоўваючы аператар адмаўленне (!) двойчы, значэнне можна прыведзена да лагічнага (булевага) тыпу.

!!'foo';   // true
!!'';      // false
!!'0';     // true
!!'1';     // true
!!'-1'     // true
!!{};      // true
!!true;    // true

Ядро

Чаму не варта выкарыстоўваць eval

Функцыя eval выконвае радок JavaScript коду ў лакальнай зоне бачнасці.

var number = 1;
function test() {
    var number = 2;
    eval('number = 3');
    return number;
}
test(); // 3
number; // 1

Тым не менш, eval выконваецца ў лакальнай прасторы імён толькі ў тым выпадку, калі яна была выклікана наўпрост і імя выкліканай функцыі — eval.

var number = 1;
function test() {
    var number = 2;
    var copyOfEval = eval;
    copyOfEval('number = 3');
    return number;
}
test(); // 2
number; // 3

Лепш пазбягаць выкарыстоўвання eval. 99,9% яе «выкарыстанняў» можа быць дасягнута без яе.

Схаваны eval

Абедзве функцыі тайм-аўты setTimeout і setInterval могуць прымаць радок у якасці першага аргумента. Гэты радок будзе заўсёды выконвацца ў глабальнай прасторы імёнаў, бо eval не выклікаецца наўпрост у дадзеным выпадку.

Праблемы з бяспекаю

Таксама eval мае праблемы з бяспекаю, бо ён выконвае любы перададзены код. Таму яе ніколі не варта выкарыстоўваць з радкамі, што паходзяць з ненадзейных крыніцаў.

Заключэнне

Лепш ніколі не выкарыстоўваць eval. Любы код, што выкарыстоўвае яе, спрэчны ў плане якасці, карэктнасці, прадукцыйнасці і бяспекі. Калі для таго, каб нешта працавала, патрэбны eval, не трэба прымаць гэта рашэнне ў першую чаргу. Лепшым рашэннем будзе тое, што не будзе выкарыстоўваць eval.

undefined і null

JavaScript мае два розныя значэнні для 'нічога' - гэта null і undefined, пры гэтым апошняе больш карыснае.

Значэнне undefined

undefined — гэта тып з роўна адным значэннем: undefined.

Мова таксама аб'яўляе глабальную пераменную, што мае значэнне undefined; Гэта пераменная таксама называецца undefined. Тым не менш, гэта пераменная, а не канстанта, ці ключавое слова. Гэта азначае, што яе значэнне можа быць з лёгкасцю перазапісаным.

Ніжэй пералічаныя некалькі выпадкаў, калі вяртаецца undefined:

  • Доступ да (немадыфікаванай) глабальнай пераменнай undefined.
  • Доступ да аб'яўленай, але яшчэ не ініцыалізаванай пераменнай.
  • Няяўна вернутае значэнне функцыі праз адсутнасць аператара return.
  • З аператара return, які нічога яўна не вяртае.
  • У выніку пошуку неіснуючай уласцівасці аб'екта.
  • Параметры функцыі, якім яўна не было прысвоена значэнне.
  • Усё, чаму было прысвоена значэнне undefined.
  • Любы выраз у форме void(expression).

Апрацоўка зменаў значэння undefined

З той прычыны, што глабальная пераменная undefined утрымлівае толькі копію актуальнага значэння undefined, прысвойванне ёй новага значэння не мяняе значэнне тыпа undefined.

Таксама для таго, каб параўнаць што-небудзь з значэннем undefined, спачатку трэба атрымаць значэнне undefined.

Звыклая тэхніка абароны ад магчымага перазапісвання пераменнай undefined — дадатковы параметр у ананімнай абгортцы, што выкарыстоўвае адсутны аргумент.

var undefined = 123;
(function(something, foo, undefined) {
    // цяпер undefined у лакальнай зоне бачнасці
    // зноў спасылаецца на значэнне `undefined`

})('Hello World', 42);

Гэтага ж выніку можна дасягнуць праз аб'яўленне ўнутры абгорткі.

var undefined = 123;
(function(something, foo) {
    var undefined;
    ...

})('Hello World', 42);

Адзіная розніца ў тым, што гэта апошняя версія будзе большай на 4 байты пры мініфікацыі, а ў першым унутры ананімнай абгорткі не будзе аператара var.

Выкарыстоўванне null

Хаця undefined у кантэксце мовы JavaScript у асноўным выкарыстоўваецца ў сэнсе традыйнага null, сам null (і літэрал і тып) з'яўляецца яшчэ адным тыпам дадзеных.

Выкарыстоўваецца ў некаторых унутранных механізмах JavaScript (напрыклад аб'яўленне канца ланцужка прататыпаў пазначаючы Foo.prototype = null), але амаль што ва ўсіх выпадках ён можа быць заменены на undefined.

Аўтаматычная ўстаўка кропкі з коскай

Хаця JavaScript мае C-падобны сінтакс, ён не прымушае выкарыстоўваць кропку з коскай у кодзе, таму ёсць магчымасць прапускаць іх.

Але JavaScript — не мова без кропак з коскай. Насамрэч яны патрэбны ёй, каб разумець зыходны код. Таму парсер JavaScript аўтаматычна ўстаўляе іх паўсюль, дзе сустракае памылку адсутнасці кропкі з коскай.

var foo = function() {
} // памылка разбора, парсер чакаў кропку з коскай
test()

Адбываецца ўстаўка, парсер спрабуе зноў.

var foo = function() {
}; // памылкі няма, парсер працягвае
test()

Аўтаматычная ўстаўка кропкі з коскай лічыцца адной з найвялікшых архітэктурных памылак у мове, бо можа змяніць паводзіны кода.

Як яно працуе

Ніжэйпрыведзены код не мае кропак з коскай, таму парсер вырашае дзе іх уставіць.

(function(window, undefined) {
    function test(options) {
        log('testing!')

        (options.list || []).forEach(function(i) {

        })

        options.value.test(
            'long string to pass here',
            'and another long string to pass'
        )

        return
        {
            foo: function() {}
        }
    }
    window.test = test

})(window)

(function(window) {
    window.someLibrary = {}

})(window)

Ніжэй — вынік гульні парсера ў адгадванне.

(function(window, undefined) {
    function test(options) {

        // Не ўстаўлена, радкі былі аб'яднаныя
        log('testing!')(options.list || []).forEach(function(i) {

        }); // <- устаўлена

        options.value.test(
            'long string to pass here',
            'and another long string to pass'
        ); // <- устаўлена

        return; // <- устаўлена, разбіў аператар return на два блока
        { // парсер лічыць гэты блок асобным
            foo: function() {}
        }; // <- устаўлена
    }
    window.test = test; // <- устаўлена

// Радкі зноў аб'ядналіся
})(window)(function(window) {
    window.someLibrary = {}; // <- устаўлена

})(window); //<- устаўлена

Парсер кардынальна памяняў паводзіны кода. У пэўных выпадках ён прымае памылковыя рашэнні.

Вядучыя дужкі

У выпадку вядучай дужкі парсер не уставіць кропку з коскай.

log('testing!')
(options.list || []).forEach(function(i) {})

Гэты код ператворыцца ў радок.

log('testing!')(options.list || []).forEach(function(i) {})

Вельмі верагодна, што log не вяртае функцыю; Таму вышэйпрыведзены код справакуе TypeError з заявай, што undefined не з'яўляецца функцыяй.

Заключэнне

Крайне рэкамендуецца ніколі не прапускаць кропку з коскай. Таксама заўсёды рэкамендуецца ставіць дужкі на той жа лініі, што і адпаведныя канструкцыі, і ніколі не прапускаць іх у аднарадковых канструкцыях if / else. Гэтыя меры не толькі павысяць кансістэнтнасць кода, але таксама прадухіляць ад таго, што парсер JavaScript зменіць паводзіны кода.

Аператар delete

У JavaScript немагчыма выдаліць глабальныя пераменныя, функцыі і некаторыя іншыя рэчы, што маюць атрыбут DontDelete.

Глабальны код і код функцый.

Калі пераменная або функцыя аб'яўленыя ў глабальнай зоне бачнасці, або зоне бачнасці функцыі то яна будзе ўласцівасцю або аб'екта актывацыі, або глабальнай зоны бачнасці. Такія ўласцівасці маюць набор атрыбутаў, адзін з іх — DontDelete. Пераменныя і функцыі, аб'яўленыя ў глабальнай зоне бачнасці і зоне бачнасці функцыі, заўсёды ствараюцца з уласцівасцю DontDelete, а таму не могуць быць выдаленыя.

// глабальная пераменная:
var a = 1; // мае ўласцівасць DontDelete
delete a; // false
a; // 1

// звычайная функцыя:
function f() {} // мае ўласціваць DontDelete
delete f; // false
typeof f; // "function"

// перапрызначэнне не дапамагае:
f = 1;
delete f; // false
f; // 1

Яўна прызначаныя ўласцівасці

Яўна прызначаныя ўласцівасці могуць быць лёгка выдаленыя.

// яўна прызначаныя ўласцівасці:
var obj = {x: 1};
obj.y = 2;
delete obj.x; // true
delete obj.y; // true
obj.x; // undefined
obj.y; // undefined

У вышэйпрыведзеным прыкладзе, obj.x і obj.y могуць быць выдаленыя, бо не маюць атрыбута DontDelete. Таму і прыкад ніжэй працуе:

// Гэта працуе правільна, акрамя IE:
var GLOBAL_OBJECT = this;
GLOBAL_OBJECT.a = 1;
a === GLOBAL_OBJECT.a; // true — глабальная пераменная
delete GLOBAL_OBJECT.a; // true
GLOBAL_OBJECT.a; // undefined

Гэта прыём, каб выдаліць a. this спасылаецца на глабальны аб'ект, і мы яўна аб'яўляем пераменную a як яго ўласцівасць, што дазваляе нам выдаліць яе.

IE (прынамсі 6-8) мае хібы, таму вышэйпрыведзены код там працаваць не будзе.

Аргументы функцый і ўбудаваныя ўласцівасці

Звычайныя аргументы функцыі, аб'ект arguments, а таксама убудаваныя ўласцівасці таксама маюць атрыбут DontDelete.

// аргументы функцыі і ўласцівасці:
(function (x) {

  delete arguments; // false
  typeof arguments; // "object"

  delete x; // false
  x; // 1

  function f(){}
  delete f.length; // false
  typeof f.length; // "number"

})(1);

У заключэнне

Аператар delete часта паводзіць сябе нечакана, таму адзінае надзейнае выкарыстанне delete — выдаленне яўна прызначаных уласцівасцяў.

Рэшта

setTimeout і setInterval

Дзякуючы асінхроннасці JavaScript магчыма запланаваць выкананне функцыі з дапамогай метадаў setTimeout і setInterval.

function foo() {}
var id = setTimeout(foo, 1000); // вяртае Number > 0

Калі функцыя setTimeout выклікана, яна вяртае ID таймаўта і плануе выкананне foo прыблізна праз тысячу мілісекунд. foo будзе выканана аднойчы.

Улічваючы вырашэнні таймера рухавіка Javascript, які выконвае код, аднапаточнасць JavaScript і тое, што іншы код, які выконваецца можа блакаваць паток, нельга быць упэўненым, што вы атрымаеце затрымку, пазначанаю ў выкліку setTimeout.

Функцыя, якая была перададзена як першы параметр, будзе выклікана глабальным аб'ектам, гэта азначае, што this унутры выкліканай функцыі спасылаецца на глабальны аб'ект.

function Foo() {
    this.value = 42;
    this.method = function() {
        // this спасылаецца на глабальны аб'ект
        console.log(this.value); // выведзе undefined
    };
    setTimeout(this.method, 500);
}
new Foo();

Паслядоўныя выклікі з дапамогай setInterval

У той час калі setTimeout выконвае функцыю толькі адзін раз, setInterval - як бачна з назвы - выконвае функцыю кожныя X milliseconds, але яе выкарыстанне не пажадана.

Код, які выконваецца, блакуе выклік з таймаўтам, у той час setInterval будзе планаваць выклікі зададзенай функцыі. Гэта можа, асабліва з маленькімі інтэрваламі, прывесці да стварэння чаргі выклікаў функцый.

function foo(){
    // нешта, што блакуе на 1 секунду
}
setInterval(foo, 100);

У прыведзеным кодзе foo будзе выклікана аднойчы і заблакуе выкананне на адну секунду.

У той час калі foo блакуе код, setInterval будзе планаваць наступныя яе выклікі А калі выкананне foo скончана, ужо дзесяць наступных выклікаў будуць чакаць выканання.

Праца з магчыма блакуючым кодам

Найбольш простае і кіруемае рашэнне гэта выкарыстоўваць setTimeout унутры самой функцыі.

function foo(){
    // нешта, што блакуе на 1 секунду
    setTimeout(foo, 100);
}
foo();

Гэты падыход не толькі інкапсулюе выклік setTimeout, але таксама прадухіляе стварэнне чаргі выклікаў і дае дадатковы кантроль. Цяпер foo можа сама вырашыць хоча яна выконвацца яшчэ раз ці не.

Ручная чыстка таймаўтаў

Чыстка таймаўтаў і інтэрвалаў здзяйсняецца перадачай адпаведнага ID у clearTimeout або clearInterval, гледзячы якая set функцыя была выкарыстана да гэтага.

var id = setTimeout(foo, 1000);
clearTimeout(id);

Чыстка ўсіх таймаўтаў

Так як няма ўбудаванага метада для выдалення ўсіх таймаўтаў і/або інтэрвалаў, для гэтага неабходна выкарыстоўваць брутфорс.

// выдаліць "усе" таймаўты
for(var i = 1; i < 1000; i++) {
    clearTimeout(i);
}

Але могуць быць таймаўты, якія не закрануты гэтым адвольным нумарам. Іншы шлях ажыццяўлення гэтага - прыняць, што ID таймаўта павялічваецца на адзін пасля кожнага выкліку setTimeout.

// выдаліць "усе" таймаўты
var biggestTimeoutId = window.setTimeout(function(){}, 1),
i;
for(i = 1; i <= biggestTimeoutId; i++) {
    clearTimeout(i);
}

Не гледзячы на тое, што зараз гэта працуе на ўсіх асноўных браўзерах, тое, што ID павінны быць арганізаваны такім шляхам не пазначана ў спецыфікацыі і можа змяніцца. Таму замест гэтага рэкамендуецца сачыць за ўсімі ID таймаўтаў, каб яны маглі быць выдалены паасобку.

Схаванае выкарыстанне eval

setTimeout і setInterval таксама могуць прымаць радок у якасці першага параметра. Гэту магчымасць ніколі не трэба выкарыстоўваць, бо ўнутрана вызываецца eval.

function foo() {
    // будзе выклікана
}

function bar() {
    function foo() {
        // ніколі не будзе выклікана
    }
    setTimeout('foo()', 1000);
}
bar();

Паколькі eval не выклікана напрамую, радок, які перададзены setTimeout будзе выкананы ў глабальным скоўпе; такім чынам, не будзе выкарыстана лакальная пераменная foo са скоўпа bar.

Адсюль вынікае рэкамендацыя не выкарыстоўваць радок для перадачы аргументаў у функцыю, якая будзе вызывацца адной з таймаўт функцый.

function foo(a, b, c) {}

// НІКОЛІ так не рабіце
setTimeout('foo(1, 2, 3)', 1000)

// Замест выкарыстоўвайце ананімныя функцыі
setTimeout(function() {
    foo(1, 2, 3);
}, 1000)

У заключэнне

Радок ніколі не павінен быць выкарыстаны як параметр setTimeout ці setInterval. Відавочны знак сапраўды благога кода гэта калі функцыя, якая будзе выклікана, патрабуе аргументы. Трэба перадаваць ананімную функцыю, якая будзе адказваць за выклік патрэбнай функцыі.

Больш таго, трэба пазбягаць выкарыстання setInterval, таму што яе планавальнік не блакуецца выкананнем JavaScript.