Функции със стойности по подразбиране на параметрите

Функциите в JavaScript са уникални с това, че позволяват произволен брой подадени параметри, независимо от броя на параметрите обявени в дефиницията на функцията. Това позволява да се дефинират функции, които могат да се справят с различни номера на параметри, често само с попълване на стойности по подразбиране, когато параметри не са предвидени. В този раздел се обяснява, как параметрите по подразбиране работят преди и след ECMAScript 6, заедно с важна информация относно обекта на аргументите, използвайки изрази, като параметри и друга TDZ.

Симулиране на стойности по подразбиране на параметрите в ECMAScript 5

В ECMAScript 5 и по-рано, най-вероятно се е използвал следният модел за създаване на функции с default стойности на параметрите:

function makeRequest(url, timeout, callback) {				
    timeout = timeout || 2000;
    callback = callback || function() {};
    // останалата част от функцията
}
                

В този пример, timeout и callback са всъщност по избор, тъй като са им дадени default стойности, ако не е предвиден параметър. Логическия оператор или (||) винаги се връща на втория операнд, когато първия е falsе. Тъй наречените функционални параметри, които не са изрично предвидени, са настроени на undefined, логическия оператор или (||), често се използва за осигуряване на стойности по подразбиране за липсващи параметри. Има един недостатък на този подход, обаче, в който валидна стойност за timeout може да е 0, което ще се замести с 2000, защото 0 е falsе.

В този случай, по-безопасно е да се провери вида на аргумента, използвайки typeof, като в примера:

function makeRequest(url, timeout, callback) {
  timeout = (typeof timeout !== "undefined") ? timeout : 2000;
  callback = (typeof callback !== "undefined") ? callback : function() {};
    // останалата част от функцията
}
            

Макар, че този подход е по-безопасен, той все още изисква доста допълнителен код за много основни операции. Популярни JavaScript библиотеки са пълни с подобни модели, понеже това представлява общ модел.

Default стойности на параметрите в ECMAScript 6

ECMAScript 6 прави по-лесно осигуряването на стойности за параметрите чрез предоставяне на инициализации, които се използват, когато параметъра не е официално подаден. Например:

function makeRequest(url, timeout = 2000, 
callback = function() {}) {
    // останалата част от функцията
}		
				

Тази функция само очаква първия параметър винаги да бъде приет. Другите два параметъра имат default стойности, което прави тялото на функцията, много по-малко, защото не е нужно да се добавя всеки код за проверка на липсващата стойност.

Когато makeRequest() се извиква с всичките три параметъра, default не се използват. Например:

// използва default timeout и callback
makeRequest("/foo");

// използва default callback
makeRequest("/foo", 500);

// не използва  default
makeRequest("/foo", 500, function(body) {
    doSomething(body);
});			
				

ECMAScript 6 счита url за задължителен, ето защо "/foo" е подаден във всички три извиквания на makeRequest(). Двата параметъра със default стойности не се считат за задължителни.

Възможно е да се определят default стойности за всички аргументи, включително и тези, които се появяват преди аргументите без default стойности в декларацията на функцията. Например, това е добре:

function makeRequest(url, timeout = 2000, callback) {
    // останалата част от функцията
}		
				

В този случай, default стойността на timeout ще бъде използвана само в случай ако няма втори аргумент или ако втория аргумент е изрично приет, като undefined, както в този пример:

// използва default timeout
makeRequest("/foo", undefined, function(body) {
    doSomething(body);
});

// използва default timeout
makeRequest("/foo");

// не използва default timeout
makeRequest("/foo", null, function(body) {
    doSomething(body);
});			
				

В случай на default стойности на параметрите, стойност от null, се счита за валидна, което означава, че при третото извикване на makeRequest(), default стойността на timeout няма да се използва.

Как default стойностите на параметрите се отразяват на обекта на аргументите

Само имайте в предвид, че поведението на arguments обекта е различно, когато са налице default стойности на параметрите. В ECMAScript 5 при nonstrict mode, arguments обекта се отразява на промените в обявените параметри на функцията. Ето част от код, който илюстрира как работи това:

function mixArgs(first, second) {
    console.log(first === arguments[0]);
    console.log(second === arguments[1]);
    first = "c";
    second = "d";
    console.log(first === arguments[0]);
    console.log(second === arguments[1]);
}
mixArgs("a", "b");
				

Това е изхода:

			

true
true
true
true

arguments обекта винаги се актуализира в nonstrict mode за да отрази промените в обявените параметри. По този начин, на first и second са възложени нови стойности arguments[0] и arguments[1], които се актуализират съответно, което кара всички === сравнения да решат вярно.

В ECMAScript 5 при strict mode, обаче, се елиминира този объркващ аспект на arguments обекта. В strict mode, arguments обекта не отразява промените в обявените параметри. Ето отново mixArgs() функцията, но в strict mode:

function mixArgs(first, second) {
    "use strict";
    console.log(first === arguments[0]);
    console.log(second === arguments[1]);
    first = "c";
    second = "d"
    console.log(first === arguments[0]);
    console.log(second === arguments[1]);
}
mixArgs("a", "b");
				

Извикването на mixArgs() извежда:

			

true
true
falsе
falsе

Този път смяната на first и second няма ефект върху arguments, така че продукцията се държи нормално, както сме очаквали.

В arguments обекта на функция използваща ECMAScript 6, стойностите по подразбиране на параметрите, обаче, винаги се държат по същия начин, както ECMAScript 5 в strict mode, независимо дали функцията работи в strict mode. Наличието на default стойности на параметрите задейства arguments обекта да остане откъснат от обявените параметри. Това е малка но важна подробност, защото е начин, по който arguments обекта може да се използва. Помислете за следното:

// не е в strict mode
function mixArgs(first, second = "b") {
    console.log(arguments.length);
    console.log(first === arguments[0]);
    console.log(second === arguments[1]);
    first = "c";
    second = "d"
    console.log(first === arguments[0]);
    console.log(second === arguments[1]);
}
mixArgs("a");	
			    

Това е изхода:

			

1
true
falsе
falsе
falsе

В този пример, arguments.length е 1 защото само един аргумент е подаден в mixArgs(). Това също означава, че arguments[1] е undefined, което е най-очакваното поведение, когато само един аргумент е подаден към функцията. Това означава, че first е равен на arguments[0]. Промяната на first и second няма никакъв ефект върху arguments. Това се случва, както при nonstrict, така и при strict mode, така че, може да разчитате arguments да отразяват винаги първоначалното състояние на повикване.

Default Parameter Expressions

Може би най-интересната особеност на default стойностите на параметрите, е че default стойността не е необходимо да бъде примитивна стойност. Можете, например, да изпълните функция за извличане на default стойността от параметъра, като тази:

function getValue() {
    return 5;
}
function add(first, second = getValue()) {
    return first + second;
}
console.log(add(1, 1));     // 2
console.log(add(1));        // 6	
				

Тук, ако последният аргумент не е предвиден, се извиква функцията getValue() за да изтеглим правилната default стойност. Имайте в предвид, че getValue() се извиква само, когато add() се извиква без втори параметър, но не и когато декларацията на функцията е първия разбор. Това означава, че ако getValue() е написана по различен начин, може потенциално да върне различна стойност. Например:

let value = 5;
function getValue() {
    return value++;
}
function add(first, second = getValue()) {
    return first + second;
}
console.log(add(1, 1));     // 2
console.log(add(1));        // 6
console.log(add(1));        // 7	
				

В този пример, value започва, като пет и постепенно се увеличава всеки път, когато getValue() се извиква. Първото извикване на add(1) връща 6, второто извикване на add(1) връща 7, защото value се увеличава. Тъй като, default стойността за second се изчислява само, когато функцията се извика, промени на тази стойност могат да се направят по всяко време.

worning
Бъдете внимателни, когато използвате функция извикана, като default стойност на параметри. Ако сте пропуснали скобите, като second = getValue в последния пример, вие подавате референция към функцията, а не резултата от нейното извикване.

Това поведение довежда до друга интересна възможност. Можем да използваме предшестващият default параметър, като по-късен параметър. Ето един пример:

function add(first, second = first) {
    return first + second;
}
console.log(add(1, 1));     // 2
console.log(add(1));        // 2				
				

В този код, на параметъра second е дадена default стойност на first, което означава, че се подава само един аргумент, давайки на двата аргумента една и съща стойност. Така add(1, 1) ще връща 2 само, когато add(1) връща 2. Вземайки тази крачка на пред, може да се подаде first, за да получим стойността на second, както следва:

function getValue(value) {
    return value + 5;
}
function add(first, second = getValue(first)) {
    return first + second;
}
console.log(add(1, 1));     // 2
console.log(add(1));        // 7	
				

Този пример, определя second да е равно на стойността върната от getValue(first), така че докато add(1, 1) все още връща 2, add(1) връща 7 (1 + 6).

Способноста за референтни параметри от default параметри работи само за предшестващите аргументи, така че по-ранните аргументи нямат достъп до по-късните аргументи. Например:

function add(first = second, second) {
    return first + second;
}
console.log(add(1, 1));     // 2
console.log(add(1));        // хвърля грешка	
				

Извикването на add(1) хвърля грешка, защото second е дефиниран след first и следователно е недостъпна, като default стойност. За да се разбере, защо това се случва е важно да се преразгледат темпоралните мъртви зони.

Default стойности на параметрите в темпоралните мъртви зони

Глава 1 въведе темпоралната мъртва зона (TDZ), която се отнася до let и const и default стойностите на параметрите също имат TDZ, където параметрите не могат да бъдат достъпни. Подобно на let декларацията, всеки параметър създава нов идентификатор на свързване, който не може да бъде ползван преди инициализацията без да хвърля грешка. Инициализацията на параметъра се случва, когато се извиква функцията с подаване на стойност за параметъра или с помощта на default стойност на параметъра.

За да получим default стойност на параметъра в TDZ, нека отново помислим върху този пример от “Default Parameter Expressions”:

function getValue(value) {  
    return value + 5;
}
function add(first, second = getValue(first)) {
    return first + second;
}
console.log(add(1, 1));     // 2
console.log(add(1));        // 7	
				

Извикването на add(1, 1) и add(1) ефективно изпълнява следния код за създаване на first и second стойности на параметрите:

// JavaScript representation of call to add(1, 1)
let first = 1;
let second = 1;
// JavaScript representation of call to add(1)
let first = 1;
let second = getValue(first);
				

Когато функцията add() се изпълнява за първи път, обвързването на first и second се добавя към специфични параметри на TDZ (подобно на това, както се държи let). Така, че second може да се инициализира със стойността на first, защото first винаги се инициализира по това време, докато обратното не е вярно. Сега помислете за тази пренаписана add() функция:

function add(first = second, second) {
    return first + second;
}
console.log(add(1, 1));         // 2
console.log(add(undefined, 1)); // хвърля грешка	
				

Извикванията на add(1, 1) и add(undefined, 1) в този пример, ще разгледаме зад кулисите на кода:

// JavaScript representation of call to add(1, 1)
let first = 1;
let second = 1;
// JavaScript representation of call to add(undefined, 1)
let first = second;
let second = 1;
				

В този пример, извикването на add(undefined, 1) хвърля грешка, защото second все още не е инициализиран, когато first се инициализира. В този момент, second е в TDZ и всички препратки към second хвърлят грешка. Това е отражение на поведението на let обвързванията в Глава 1.

info
Параметрите на функции имат свой собствен обхват и тяхна собствена TDZ , която е отделно от обхвата на тялото на функцията. Това означава, че default стойността на параметъра не може да получи достъп до всички променливи, декларирани във вътрешността на функцията.

Работа с анонимни параметри

Досега параметрите в тази глава са само за гарантираните параметри, които са посочени в дефиницията на функцията. Обаче, функциите в JavaScript не ограничават броя на параметрите, които могат да бъдат подадени спрямо броя на дефинираните параметри. Винаги може да се мине с по-малко или повече параметри, от колкото официално е определено. Default стойностите на параметрите правят ясно, кога дадена функция може да приеме по-малко параметри и ECMAScript 6 иска да оправи проблема с подаване на повече параметри от дефинираните.

Анонимни параметри в ECMAScript 5

В началото, JavaScript предоставя на arguments обекта начин да се инспектира, за да се представят всички параметри на функциите, които са подадени без непременно определяне на всеки параметър по отделно . Докато инспектирането на arguments работи добре в повечето случаи, този обект може да работи малко по-тромаво. Например, да разгледаме този код, който инспектира arguments обекта:

function pick(object) {
    let result = Object.create(null);
    // започва от втория
    for (let i = 1, len = arguments.length; i < len; i++) {
        result[arguments[i]] = object[arguments[i]];
    }
    return result;
}
let book = {
    title: "Understanding ECMAScript 6",
    author: "Nicholas C. Zakas",
    year: 2015
};
let bookData = pick(book, "author", "year");
console.log(bookData.author);   // "Nicholas C. Zakas"
console.log(bookData.year);     // 2015	
				

Тази функция имитира pick() метода от Underscore.js библиотеката, която връща копие на даден обект с някои специфични свойства на под-множества от оригиналния обект. Този пример дефинира само един аргумент и очаква първият аргумент да бъде обект, от който се копират свойства. Всеки друг аргумент е име на свойство, което трябва да се копира в резултата.

Има няколко неща, които трябва да се отбележат за тази pick() функция. Първо, никак не е ясно, че функцията може да се справи с повече от един параметър. Може да се дефинират още няколко параметъра, но те винаги ще се оказват недостатъчни да покажат, че тази функция може да взема произволен брой параметри. Второ, понеже първият параметър е назован и се използва директно, когато търсим свойства за копиране трябва да започнем в arguments обекта от индекс 1 вместо от 0. Спомнете си за използването на подходящи индекси за arguments, които не е задължително да са трудни, но това е още едно нещо за следене.

ECMAScript 6 въвежда rest (останали) параметри за да ви помогне с тези въпроси.

Rest параметри

Rest параметрите са отбелязани с три точки (...) предшестващи името на параметъра. Това име става Array съдържащ останалата част от параметрите, подадени към функцията, която е мястото, от където името “rest" параметри произлиза. Например, pick() може да бъде пренаписана с използването на rest параметри, като тези:

function pick(object, ...keys) {
    let result = Object.create(null);
    for (let i = 0, len = keys.length; i < len; i++) {
        result[keys[i]] = object[keys[i]];
    }
    return result;
}	
				

В тази версия на функцията, keys е rest параметър, който съдържа всички параметри, подадени след обекта (за разлика от arguments, които съдържа всички параметри включително и първия). Това означава, че можете да обхождате keys от началото до края без да се притеснявате. Като бонус, можете да кажете на разглежданата функция да се справи с произволен брой параметри

info
Rest параметрите не влияят на length свойството на функцията, което показва броя на имената на параметрите за функцията. Стойността на length за pick() в този пример е 1, защото само object се брои към тази стойност.

Ограничения на Rest параметрите

Има две ограничения за rest параметрите. Първото ограничение е, че може да има само един rest параметър и rest параметъра трябва да бъде последен. Например, този код не работи:

// Syntax error:Не може да има параметър след rest параметъра
function pick(object, ...keys, last) {
    let result = Object.create(null);
    for (let i = 0, len = keys.length; i < len; i++) {
        result[keys[i]] = object[keys[i]];
    }
    return result;
}	
				

Тука параметъра last е след rest параметъра keys, което е причина за синтактична грешка.

Второто ограничение е, че rest параметрите не могат да се използват в обекти, като буквални setter. Това означава, че този код също ще предизвика синтактична грешка.

let object = {
    // Syntax error: Не може да се използва rest параметър в setter
    set(...value) {
        // направи нещо
    }
};	
				

Това ограничение съществува, защото object literal setters са ограничени до един аргумент. Rest параметрите по дефиниция са безкраен брой аргументи, така че не се допускат в този контекст.

Как Rest параметрите влияят на аргументите на обекта

Rest параметрите са предназначени да заменят arguments в ECMAScript. Първоначално ECMAScript 4 премахва arguments и добавя rest параметри, за да даде възможност на неограничен брой аргументи да бъдат подадени към функции. ECMAScript 4 никога не се появява, но тази идея се съдържа и въвежда отново в ECMAScript 6 въпреки, че arguments не са отстранени от езика.

arguments обекта, работи заедно с rest параметрите чрез отразяване на аргументите, които са подадени към функцията, когато се извиква, както е показано в тази програма:

function checkArgs(...args) {
    console.log(args.length);
    console.log(arguments.length);
    console.log(args[0], arguments[0]);
    console.log(args[1], arguments[1]);
}
checkArgs("a", "b");	
				

Извикването на checkArgs() извежда:

			

2
2
a a
b b

arguments обекта винаги правилно отразява параметрите, които са подадени в една функция, независимо от използването на rest параметри.

Това е всичко, което трябва да знаете за rest параметрите за да започнете да ги използвате.

Повишени възможности на Function конструктора

Function конструктора е рядко използвана част от JavaScript, която ви позволява динамично да създавате нова функция. Аргументите на конструктора са параметри и тяло на функцията, всички като strings. Ето един пример:

var add = new Function("first", "second", "return first + second");

console.log(add(1, 1));     // 2
					
				

ECMAScript 6 увеличава възможностите на Function конструктора да позволява default и rest параметри. Вие трябва само да добавите знак за равенство и стойност на имената на параметрите, както следва:

var add = new Function("first", "second = first", "return first + second");
console.log(add(1, 1));     // 2
console.log(add(1));        // 2	
				

В този пример, на second параметъра се дава стойността на first, когато се подава само един параметър. Синтаксиса е същия, като при декларация на функция, която не използва Function конструктор.

За rest параметрите просто добавете три точки ... преди последния параметър, като този:

var pickFirst = new Function("...args", "return args[0]");
console.log(pickFirst(1, 2));   // 1
				

Този код създава функция, която използва само един единствен rest параметър и връща първия аргумент, който е подаден вътре.

Добавянето на default и rest параметри гарантира, че Function конструктора има същите възможности, както и формата за деклариране на функция при създаването на функция.

Оператора Spread (разпростиране)

Тясно свързан с rest параметрите е оператора spread. Докато rest параметрите ни позволяват да уточним множество независими аргументи, които трябва да бъдат комбинирани в един масив, оператора spread (разпростиране) ни позволява да определим масив, който трябва да се раздели и неговите елементи да се подадат, като отделни аргументи към функция. Помислете за Math.max() метода, който приема произволен брой аргументи и връща този с най-голяма стойност. Ето един прост случай на употреба на този метод:

let value1 = 25,
    value2 = 50;
console.log(Math.max(value1, value2));      // 50	
				

Когато имаш работа само с две стойности, както в този пример, Math.max() е много лесен за употреба. Двете стойности се подават в него и по-голямата се връща. Но ако трябва да проследявате стойности на масив и искате да се намери най-голямата стойност. Метода Math.max() не позволява подаване на масив, така че в ECMAScript 5 и по-рано, трябваше да се залепи търсачка за масива или да се използва метода apply(), както следва:

let values = [25, 50, 75, 100]
console.log(Math.max.apply(Math, values));  // 100	
				

Това решение работи, но използването на apply() по този начин е малко объркващо. То всъщност изглежда, че обърква истинското значение на кода с допълнителния синтаксис.

ECMAScript 6 с оператора spread прави този случай много прост. Вместо извикване на apply(), той подава масива на Math.max() директно и префикс със същия модел ... използван при rest параметрите. Машината JavaScript разделя масива на отделни аргументи и ги подава по начин подобен на този:

let values = [25, 50, 75, 100]
// еквивалентно на
// console.log(Math.max(25, 50, 75, 100));
console.log(Math.max(...values));           // 100			
				

Сега извикването на Math.max() изглежда малко по-конвенционално и избягва сложността за уточняване на свързващия this (първият аргумент на Math.max.apply() в предишния пример) за една проста математическа операция.

Можете да смесвате и съчетавате оператора spread с други аргументи, като избор. Да предположим, че искате най-малкия номер върнат от Math.max() да е 0 (за всеки случай поставяме отрицателни числа в масива). Можете да подадете този аргумент отделно и да използвате оператора spread върху другите аргументи, както следва:

let values = [-25, -50, -75, -100]
console.log(Math.max(...values, 0));        // 0
				

В този пример последният аргумент подаден към Math.max() е 0, който идва след другите аргументи, които са подадени за използване към оператора spread.

Оператора spread за подаване на аргумент, прави използването на масиви, като аргументи за функция много по-лесно. Най-вероятно ще откриете, че това е подходящ заместител на метода apply() в повечето случаи.

В допълнение на употребите, които видяхте за default и rest параметрите досега в ECMAScript 6, можете също така да прилагате двата типа параметри в JavaScript Function конструктора.

Name свойство в ECMAScript 6

Идентифицирането на функции може да бъде предизвикателство в JavaScript, като се има в предвид различните начини, по които функциите могат да бъдат дефинирани. Освен това разпространяването на анонимни функционални изрази, прави отстраняването на грешки малко по-трудно, което често води до следи в стека, които са трудни за четене и разчитане. Поради тази причина, ECMAScript 6 добави name property (име свойство) за всички функции.

Избор на подходящи имена

Всички функции в една ECMAScript 6 програма ще имат подходяща стойност за тяхното name свойство. За да видите това в действие, погледнете следния пример, който показва функция и функционален израз и отпечатване на name свойството за двете.

function doSomething() {
    // ...
}
var doAnotherThing = function() {
    // ...
};
console.log(doSomething.name);          // "doSomething"
console.log(doAnotherThing.name);       // "doAnotherThing"	
				

В този код doSomething() има name свойство равно на "doSomething", защото това е декларация на функция. Анонимната функция doAnotherThing() има name свойство "doAnotherThing", защото това е името на променливата, за която е предназначена.

Особени случаи на name свойството

Докато подходящи имена за декларации на функции и функционални изрази са лесни за намиране, ECMAScript 6 отива по-далеч, като гарантира, че всички функции са с подходящи имена. За да се убедите в това, да разгледаме следната програма:

var doSomething = function doSomethingElse() {
    // ...
};
var person = {
    get firstName() {
        return "Nicholas"
    },
    sayName: function() {
       console.log(this.name);
    }
}
console.log(doSomething.name);      // "doSomethingElse"
console.log(person.sayName.name);   // "sayName"

var descriptor = Object.getOwnPropertyDescriptor(person, "firstName");
console.log(descriptor.get.name); // "get firstName"	
				

В този пример doSomething.name е "doSomethingElse", защото самият функционален израз има име и това име е с приоритет пред променливата, на която е възложена функцията. name свойството person.sayName() е "sayName", като стойността се тълкува от обекта буквално. По същия начин, person.firstName е всъщност функция на getter, така че името и е "get firstName" за да покаже тази разлика. Setter функциите са с префикс "set". (И getter и setter функциите трябва да бъдат извлечени с помощта на Object.getOwnPropertyDescriptor().)

Има няколко други специални случаи за имена на функции, също. Функции създадени с помощта на bind() имената им ще започват с префикс "bound" и функции създадени с помощта на Function конструктор ще имат име "anonymous", както в този пример:

var doSomething = function() {
    // ...
};
console.log(doSomething.bind().name);   // "bound doSomething"
console.log((new Function()).name);     // "anonymous"	
				

The name на обвързната функция винаги ще бъде name на функцията, обвързано с префикс string "bound", така че обвързаната версия на doSomething() е "bound doSomething".

Имайте в предвид, че стойността на name за всяка функция, не е задължително да се отнася до променлива със същото име. Свойството name има за цел да бъде информативно, за да помогне с отстраняването на грешки, така че няма начин да се използва стойността на name, за да получите препратка към функцията.

Изясняване на двойната цел на функции

В ECMAScript 5 и по-рано, функциите които служат за двойна цел са със или без new. Когато се използват с new, стойността на this вътре във функцията е нов обект и този нов обект се връща, както е показано в следващия пример:

function Person(name) {
    this.name = name;
}
var person = new Person("Nicholas");
var notAPerson = Person("Nicholas");
console.log(person);        // "[Object object]"
console.log(notAPerson);    // "undefined"	
		    	

Когато създаваме notAPerson, извиквайки Person() без new резултата е undefined (и поставя name свойството в глобалния обект в режим nonstrict). Капитализацията на Person е единственият реален показател, че функцията е писана да бъде извикана използвайки new, както е обичайно в програми на JavaScript. Това объркване на двойната роля на функциите доведе до промени в ECMAScript 6.

JavaScript има два различни вътрешни метода за функции: [[Call]] и [[Construct]]. Когато дадена функция се извиква без new, се изпълнява метода [[Call]], който изпълнява тялото на функцията, както е посочено в кода. Когато дадена функция се извиква с new, се извиква метода [[Construct]]. Метода [[Construct]] е отговорен за създаването на нов обект, наречен new target, и след това изпълнява тялото на функцията с this настроено към новата цел. Функциите, които имат [[Construct]] метод се наричат конструктори.

info
Имайте в предвид, че не всички функции имат [[Construct]] и поради тази причина не всички функции могат да бъдат извикани с new. Функциите стрела, обсъдени в “Arrow Функции” на тази страница, нямат [[Construct]] метод.

Определяне на начина на извикване на функция в ECMAScript 5

Най-популярният начин за определяне дали функцията е извикана с new (и следователно дали е конструктор) в ECMAScript 5 е с използването на instanceof, например:

function Person(name) {
    if (this instanceof Person) {
        this.name = name;   // използва new
    } else {
       throw new Error("You must use new with Person.")
    }
}
var person = new Person("Nicholas");
var notAPerson = Person("Nicholas");  // хвърля грешка
		    	

Тука this стойноста се проверява, за да се види дали това е случай на конструктора и ако е така изпълнението продължава нормално. Ако this не е инстанция на Person се хвърля грешка. Това работи, защото метода [[Construct]] създава нова инстанция на Person и я възлага на this. За съжаление този подход не е напълно надежден, защото this може да бъде инстанция на Person без да се използва new, както в този пример:

function Person(name) {
    if (this instanceof Person) {
        this.name = name;   // използва new
    } else {
        throw new Error("You must use new with Person.")
    }
}
var person = new Person("Nicholas");
var notAPerson = Person.call(person, "Michael");    // работи!	
		    	

Извикването на Person.call() подава променливата person, като първи аргумент, което означава, че this е настроена на person вътре в Person функцията. Функцията няма начин да разграничи това да бъде извикано с new.

The new.target MetaProperty

За да реши този проблем, ECMAScript 6 въвежда new.target metaproperty. Metaproperty е свойство на не-обект, което предоставя допълнителна информация свързана с целта (като new). Когато на дадена функция е извикан [[Construct]] метода, new.target се изпълнява с целта на new оператора. Тази цел обикновено е инстанция на конструктора на ново-създадения обект, която ще стане this във вътрешността на функцията. Ако се изпълни [[Call]], тогава new.target е undefined.

Това ново metaproperty позволява, безопасно да се открие, дали дадена функция се изпълнява с new, като проверява дали new.target е дефиниран, както следва:

function Person(name) {
    if (typeof new.target !== "undefined") {
        this.name = name;   // използва new
    } else {
        throw new Error("You must use new with Person.")
    }
}
var person = new Person("Nicholas");
var notAPerson = Person.call(person, "Michael");    // грешка!	
		    	

С използването на new.target вместо this instanceof Person, Person конструктора вече правилно хвърля грешка, когато се използва без new.

Можете също така да проверите, дали new.target се извиква с определен конструктор. Например, вижте следния пример:

function Person(name) {
    if (typeof new.target === Person) {
        this.name = name;   // използва new
    } else {
        throw new Error("You must use new with Person.")
    }
}
function AnotherPerson(name) {
    Person.call(this, name);
}
var person = new Person("Nicholas");
var anotherPerson = new AnotherPerson("Nicholas");  // грешка!	
		    	

В този код, new.target трябва да е Person за да работи правилно. Когато new AnotherPerson("Nicholas") се извика, последващото извикване на Person.call(this, name) ще хвърли грешка, защтото new.target е undefined вътре в Person конструктора (който се извиква без new).

worning
Внимание: Използването на new.target извън функция е синтактична грешка.

Чрез добавяне на new.target, ECMAScript 6 помага да се изяснят някои неясноти около извикванията на функции. Като продължение на тази тема, ECMAScript 6 се обръща също и към друга по-рано двусмислена част от езика: обявяване на функции вътре в блока.

Block-Level Functions

В ECMAScript 3 и по-рано, декларации на функции срещани във вътрешността на блок (block-level function) е техническа синтактична грешка, като всички браузъри го поддържат все още. За съжаление, всеки браузър, който позволява прояви на синтаксиса по-малко по-различен начин, ги счита за най-добри практики, като да се избягват декларации на функции вътре в блока (най-добрата алтернатива е да се използва функционален израз).

В опит да овладее това несъвместимо поведение, ECMAScript 5 strict mode въведе грешка, когато декларация на функция се използва вътре в рамките на един блок, по този начин:

"use strict";
if (true) {
    // хвърля синтактична грешка в ES5, но не и в ES6
    function doSomething() {
        // ...
    }
}	
		    	

В ECMAScript 5, този код хвърля синтактична грешка. В ECMAScript 6, doSomething() функцията се счита за блокова декларация и може да бъде достъпна и извикана в рамките на същия блок, в който е била определена. Например:

"use strict";
if (true) {
    console.log(typeof doSomething);        // "function"
    function doSomething() {
        // ...
    }
    doSomething();
}
console.log(typeof doSomething);            // "undefined"	
		    	

Block level функциите се издигат до върха на блока в който са дефинирани, така typeof doSomething връща "function" въпреки, че се появява преди декларацията на функцията в кода. След като if блока е завършил изпълнението, doSomething() вече не съществува.

Вземане на решение, кога да се ползват Block-Level функциите

Block level функциите са сходни с функционалните изрази let в това, че дефиницията на функцията се отстранява, след като изпълнението излезе от блока, в който е дефинирана. Основната разлика е, че block level функциите се издигат до върха на съдържащия ги блок. Функционалните изрази, които използват let не се издигат, както този пример илюстрира:

"use strict";
if (true) {
    console.log(typeofdoSomething);        // хвърля грешка
    let doSomething = function () {
        // ...
    }
    doSomething();
}
console.log(typeof doSomething);
		    		

Тука, изпълнението на кода спира, когато се изпълни typeof doSomething, защото let твърдението не е изпълнено все още, оставяйки doSomething() в TDZ. Знаейки тази разлика може да изберете дали да ползвате block level функции или let изрази въз основа на това дали искате или не издигащо се поведение.

Block-Level функции в Nonstrict режим

ECMAScript 6 също така позволява блокови функции в nonstrict режим, но поведението им е малко по различно. Тези декларации вместо да се издигат до върха на блока, се издигат по целия път до съдържащата ги функция или глобална среда. Например:

// ECMAScript 6 поведение
if (true) {
    console.log(typeof doSomething);        // "function"
    function doSomething() {
        // ...
    }
    doSomething();
}
console.log(typeof doSomething);            // "function"
		    		
		    	

В този пример doSomething() е издигната в глобалния обхват, така че все още съществува извън if блока. ECMAScript 6 стандартизира това поведение, за да се отстрани несъвместимо поведение с браузъра, което преди това съществуваше, така че всички ECMAScript 6 работещи автономно трябва да се държат по същия начин.

Разрешаването на блокови функции подобрява способноста да се декларират функции в JavaScript, но ECMAScript 6 същевременно въвежда изцяло нов начин за деклариране на функции.

Arrow Functions (стрела функции)

Една от най-интересните нови части на ECMAScript 6 е arrow function. Стрела функциите, както подсказва името, са функции определени с нов вид синтаксис, който използва “стрела” (=>). Но стрела функциите се държат по различно в сравнение с традиционните функции в JavaScript, по редица важни неща:

  • Няма this, super, arguments и new.target обвързвания - Стойността на this, super, arguments и new.target вътре във функцията е най-близката съдържаща се nonarrow функция. (super е разгледан в Глава 4.)
  • Не може да се извика с new - Стрела функциите нямат [[Construct]] метод и следователно не могат да бъдат използвани, като конструктори. Стрела функциите хвърлят грешка ако се използват с new.
  • Няма прототип - Тъй като не може да използвате new върху стрела функцията, няма нужда от прототип. Свойството prototype на стрела функцията не съществува.
  • Не може да променя this - Стойността на this вътре във функцията не може да бъде променяна. Тя остава една и съща през целия жизнен цикъл на функцията.
  • Няма arguments обект - Тъй като стрела функциите са без обвързващи arguments, трябва да разчитат на имена и останалите параметри за достъп до аргументите на функцията.
  • Няма дублиране на имена на параметри - Стрела функциите не могат да имат дублиращи се имена на параметри в strict или nonstrict режим, за разлика от nonarrow функциите, които не могат да имат дублиращи имена на параметри само в strict mode.

Има няколко причини за тези разлики. На първо място this обвързването е общ източник на грешки в JavaScript. Много лесно могат да се загубят следите на this стойноста вътре във функция, което може да доведе до нежелано програмно поведение, стрела функциите елиминират това объркване. Второ, чрез ограничаване на стрела функциите до просто изпълнение на код с една this стойност, JavaScript машината по-лесно ще оптимизира тези операции, за разлика от обикновените функции, които могат да се използват, като конструктор или да бъдат модифицирани по някакъв друг начин.

Останалата част от различията също са насочени към отстраняване на грешки и неясноти във вътрешността на стрела функцията. По този начин, JavaScript машината е способна да оптимизира изпълнението на стрела функцията.

info
Забележка. Стрела функциите също имат name свойство, което следва същото правило, както и другите функции.

Синтаксис на стрела функциите

Синтаксиса на стрела функциите идва с много нюанси в зависимост от това, което се опитвате да постигнете. Всички промени започват с аргументите на функцията, следвани от стрелка, следвана от тялото на функцията. И аргументите и тялото могат да приемат различни форми в зависимост от употребата. Например, следната стрела функция приема един аргумент и просто го връща:

var reflect = value => value;
// ефективно еквивалентен на:
var reflect = function(value) {
    return value;
};	
		    	

Когато има само един аргумент за стрела функцията, той може да се използва директно без допълнителен синтаксис. Стрелата идва на следващо място и израза от дясно на стрелата се оценява и връща. Въпреки, че няма изрично return изявление, тази стрела функция ще върне първия аргумент, който и е подаден.

Ако се подадат повече от един аргумент, тогава трябва да се включат скоби около тези аргументи, като това:

var sum = (num1, num2) => num1 + num2;
// ефективно еквивалентен на:
var sum = function(num1, num2) {
    return num1 + num2;
};	
		    	

Функцията sum() просто добавя два аргумента заедно и връща резултата. Единствената разлика между стрела функцията и reflect() функцията е, че аргументите са затворени в скоби с отделяща ги запетая (като традициионите функции).

Ако няма аргументи на функцията, тогава трябва да се включат празен набор от скоби в декларацията, както следва:

var getName = () => "Nicholas";
// ефективно еквивалентен на:
var getName = function() {
    return "Nicholas";
};	
		    	

Когато искате да осигурите едно по-традиционно тяло на функция, може би състоящо се от повече от един израз, тогава трябва да увиете тялото на функцията във фигурни скоби и определите изрично return за връщане на стойност, както в тази версия на sum():

var sum = (num1, num2) => {
    return num1 + num2;
};
// ефективно еквивалентен на:
var sum = function(num1, num2) {
    return num1 + num2;
};	
		    	

Можете повече или по-малко да третирате вътрешността на фигурните скоби, също както бихте направили в традиционна функция с изключение на това, че аргументите не са в наличност.

Ако искате да създадете функция, която не прави нищо, тогава ще трябва да се включат фигурни скоби, като това:

var doNothing = () => {};
// ефективно еквивалентен на:
var doNothing = function() {};
		    	

Фигурните скоби се използват за означаване на тялото на функцията, което работи добре в случаите, които сме виждали досега. Но ако една стрела функция иска да върне обект извън тялото на функцията, трябва да я увиете в скоби. Например:

var getTempItem = id => ({ id: id, name: "Temp" });
// ефективно еквивалентен на:
var getTempItem = function(id) {
   return {
        id: id,
        name: "Temp"
    };
};
		    	

Опаковането на обекта в скоби сигнализира, че скобите са обект вместо тяло на функцията.

Създаване на Immediately-Invoked Function Expressions

Едно популярно използване на функции в JavaScript е създаване на immediately-invoked function expressions (IIFEs). IIFEs ви позволява да дефинирате анонимна функция и да я извикате веднага, без да записвате препратка. Този модел е по-удобен, когато искате да създадете обхват, който е защитен от останалата част на програмата. Например:

let person = function(name) {
    return {
        getName: function() {
            return name;
        }
    };
}("Nicholas");
console.log(person.getName());      // "Nicholas"
		    	

В този код IIFE се използва за създаване на обект с getName() метода. Метода използва name аргумента, като стойност за връщане, ефективно правейки name защитена (private) част на върнатия обект.

Можете да постигнете същото използвайки стрела функция стига да увиете стрела функцията в скоби:

let person = ((name) => {
    return {
        getName: function() {
            return name;
        }
    };
})("Nicholas");
console.log(person.getName());      // "Nicholas"
		    	

Имайте в предвид, че скобите са само около дефиницията на стрела функцията, а не около ("Nicholas"). Това е различно от нормална функция, където скобите могат да бъдат поставени отвън на подадените параметри, както и само около дефиницията на функцията.

Не на this обвързването

Една от най-общите грешки в JavaScript е обвързването на this във вътрешноста на функции. Тъй като стойността на this може да се промени във вътрешноста на една функция в зависимост от контекста, с който функцията се извиква, е възможно да се отрази по погрешка на един обект, когато е трябвало да повлияе на друг. Да разгледаме следния пример:

var PageHandler = {
    id: "123456",
    init: function() {
        document.addEventListener("click", function(event) {
            this.doSomething(event.type);     // error
        }, false);
    },
    doSomething: function(type) {
        console.log("Handling " + type  + " for " + this.id);
    }
};
                 

В този код, обекта PageHandler е проектиран да обработва взаимодействия на страницата. Метода init() се извиква да създаде взаимодействие и също определя манипулатор на събитие, който да извика this.doSomething(). Въпреки това, този код не работи точно, както е предвидено.

Извикването на this.doSomething() е счупено, защото this е препратка към обекта, който е целта на събитието (в този случай document) вместо да бъде обвързано с PageHandler. Ако сте опитали да пуснете този код, ще получите съобщение за грешка, когато се създаде ефект на манипулатора на събитието, защото this.doSomething() не съществува в целевия document обект.

Може да го оправите, като свържете стойността на this с PageHandler изрично използвайки bind() метод на функцията, както това:

var PageHandler = {
    id: "123456",
    init: function() {
        document.addEventListener("click", (function(event) {
            this.doSomething(event.type);     // no error
        }).bind(this), false);
    },
    doSomething: function(type) {
        console.log("Handling " + type  + " for " + this.id);
    }
};	
		     	

Сега кодът работи, както се очаква, но изглежда малко странно. С извикването на bind(this), вие всъщност създавате нова функция, чиято this се обвързва с текущото this, което е PageHandler. За да избегнете създаването на допълнителна функция, по добър начин за определяне на този код е да се използва функция стрела.

Стрела функциите нямат this обвързване, което означава, че стойността на this вътре във функцията стрела може да се определи само, като се потърси нагоре в обхвата на веригата. Ако стрела функцията се съдържа в рамките на nonarrow функция, this ще бъде същия, както в съдържащата функция. В противен случай, this е undefined. Ето един начин, по който може да напишете този код с помощта на функция стрела:

var PageHandler = {
    id: "123456",
    init: function() {
        document.addEventListener("click",
		event => this.doSomething(event.type), false);
    },
    doSomething: function(type) {
        console.log("Handling " + type  + " for " + this.id);
    }
};		
		     	

Манипулатора на събитие в този пример е функцията стрела, която извиква this.doSomething(). Стойността на this е същата, както в init(), така че тази версия на кода работи подобно на използването на bind(this). Въпреки, че метода doSomething() не връща стойност, тя все още е единственото изявление изпълнено в тялото на функцията, така че няма нужда от включване на фигурни скоби.

Стрела функциите са проектирани да бъдат за “еднократна употреба”, така че не могат да бъдат ползвани за определяне на нови типове. Това е ясно от липсващото prototype свойство, което редовните функции имат. Ако се опитате да използвате оператора new с функция стрела, ще получите съобщение за грешка, както в този пример:

var MyType = () => {},
    object = new MyType(); 
// грешка - не може да използвате стрела функция с new	
		     	

В този код, поканата за new MyType() не успява, защото MyType е функция стрела и затова няма [[Construct]] поведение. Знаейки, че стрела функциите не могат да бъдат използвани с new, позволява на JavaScript машината по нататъшно оптимизиране на поведението им.

Също така, тъй като стойността на this се определя от съдържащата функция, в която стрелата функция е дефинирана, не може да промените стойността на tish използвайки call(), apply() или bind().

Стрела функции и масиви

Краткия синтаксис на функциите стрела ги прави идеални за използване при обработка на масиви. Например, ако искате да сортирате масив, използвайки потребителско сравнение, може би бихте написали нещо такова:

var result = values.sort(function(a, b) {
    return a - b;
});
		     	

Това е много синтаксис за една проста процедура. Сравнете това с по-изразителната версия на стрела функция:

var result = values.sort((a, b) => a - b);		     		
		     	

Методите на масива, които приемат функции за обратно извикване, като sort(), map() и reduce() могат да се възползват от по-простия синтаксис на стрела функцията, която променя привидно сложни процеси в по-прост код.

Нямат обвързващи аргументи

Макар, че стрела функциите не разполагат със собствен arguments обект, е възможно да имат достъп до arguments обекта от съдържащата функция. Този arguments обект след това е на разположение, без значение къде се изпълнява функцията стрела по-късно. Например:

function createArrowFunctionReturningFirstArg() {
    return () => arguments[0];
}
var arrowFunction = createArrowFunctionReturningFirstArg(5);
console.log(arrowFunction());       // 5
		     	

Вътре в createArrowFunctionReturningFirstArg(), елемента arguments[0] е посочен от създадената функция стрела. Тази препратка съдържа първият аргумент подаден на createArrowFunctionReturningFirstArg() функцията. Когато се изпълни стрела функцията по-късно тя връща 5, който е първият аргумент подаден на createArrowFunctionReturningFirstArg(). Въпреки, че функцията стрела не е в обхвата на функцията, която я е създала, arguments остава достъпен поради разрешения обхват на веригата на arguments идентификатора.

Идентифициране на функции стрели

Въпреки различния синтаксис, функциите стрели са все още функции и са обозначени, като такива. Да разгледаме следния код:

var comparator = (a, b) => a - b;
console.log(typeof comparator);                 // "function"
console.log(comparator instanceof Function);    // true	
		     	

На изхода console.log() показва, че typeof и instanceof се държат по същия начин с функциите стрели, както го правят с други функции.

Също, както другите функции, може да използвате call(), apply() и bind() върху стрела функциите въпреки, че this обвързването на функцията няма да бъде засегнато. Ето някои примери:

var sum = (num1, num2) => num1 + num2;
console.log(sum.call(null, 1, 2));      // 3
console.log(sum.apply(null, [1, 2]));   // 3
var boundSum = sum.bind(null, 1, 2);
console.log(boundSum());                // 3
		     	

Функцията sum() се извиква използвайки call() и apply() за подаване на аргументи, както бихме направили с всяка функция. Метода bind() се използва за създаване на boundSum(), който разполага с два свои аргумента, свързани с 1 и 2 , така че те не трябва да се подават директно.

Стрела функциите са подходящи за използване на всякъде, където в момента се използват анонимни функционални изрази, като например с обратно извикване. Следващия раздел обхваща друг основен аспект на развитие в ECMAScript 6, но всичко е вътрешно и не разполага с нов синтаксис.

Оптимизацията Tail Call

Може би най-интересната промяна на функциите в ECMAScript 6 е оптимизация на двигателя, който променя системата на tail call. Tail call е, когато дадена функция се извиква, като последно изявление в друга функция, като тази:

function doSomething() {
    return doSomethingElse();   // tail call
}
		     	

Tail calls, от както е въведена в ECMAScript 5 се обработва, както всяка друга извикана функция: създава се нова стек рамка и се вкарва в стека за извикване, за да представлява извиканата функция. Това означава, че всяка предишна стек рамка се запазва в паметта, което е проблемно, когато стека за извикване стане твърде голям.

Какво е различното?

ECMAScript 6 има за цел да намали размера на стека за определени tail calls в strict modenonstrict mode tail calls са оставени непроменени). С тази промяна вместо да създаде нова стек рамка за tail call, текущата стек рамка се изчиства и се използва повторно толкова дълго, колкото са изпълнени следните условия:

  1. Повикването на tail call не изисква достъп до променливите в текущата стек рамка (което означава, че функцията не е закриваща).
  2. Функцията правеща tail call няма допълнителна работа за вършене след връщането на tail call.
  3. Резултата от tail call се връща, като стойност на функцията.

Като пример този код може лесно да бъде оптимизиран, тъй като се вписва във всичките три критерия:

"use strict";
function doSomething() {
    // optimized
    return doSomethingElse();
}
		     	

Тази функция прави tail call към doSomethingElse(), връща резултата незабавно и няма достъп до променливите в местния обхват. Една малка промяна, не връща резултат и води до неоптимизирана функция:

"use strict";
function doSomething() {
    // не оптимизирана- няма return
    doSomethingElse();
}	     		
		     	

По същия начин, ако имате функция, която изпълнява една операция след завръщането си от tail call, тя след това не може да бъде оптимизирана:

"use strict";
function doSomething() {
  // не оптимизирана - трябва да се добави след завръщането
    return 1 + doSomethingElse();
}

Този пример добавя 1 към резултата на doSomethingElse() преди да се върне стойността и това е достатъчно за да се изключи оптимизацията.

Друг често срещан начин за невнимателно изключване на оптимизацията е да се съхранява резултата от извикването на функцията в променлива и след това да се върне променливата на резултата, като този пример:

"use strict";
function doSomething() {
     // не оптимизирана- извикването не е в tail позиция
    var result = doSomethingElse();
    return result;
}	
		     	

Този пример не може да бъде оптимизиран, тъй като стойността на doSomethingElse() не се връща незабавно.

Може би най-трудната ситуация е да се избегне в използването на закриване. Понеже закриването има достъп до променливите в съдържащия обхват, tail call може да бъде изключен. Например:

"use strict";
function doSomething() {
    var num = 1,
        func = () => num;
    // не оптимизирана - функцията е закриваща
    return func();
}
		     	

Закриващата func() има достъп до локалната променлива num в този пример. Въпреки, че извикването към func() незабавно връща резултата, оптимизацията не може да се дължи на отнасяне към променливата num.

Как да впрегнем оптимизацията Tail Call

На практика, tail call оптимизацията се случва зад кулисите, така че не трябва да се мисли затова освен, ако не се опитвате да оптимизирате функция. Случаите за употреба на tail call оптимизация е в рекурсивни функции, тъй като това е мястото, където оптимизацията има най-голям ефект. Помислете за тази функция, която изчислява факториал:

function factorial(n) {
    if (n <= 1) {
        return 1;
    } else {
    // не оптимизирана - трябва да се умножават след завръщането
        return n * factorial(n - 1);
    }
}
		     	

Тази версия на функция не може да бъде оптимизирана, защото умножението трябва да се случи след рекурсивното извикване на factorial(). Ако n е много голям номер, размера на стека за извикване ще расте и може потенциално да доведе до препълване на стека.

С цел оптимизиране на функцията, трябва да се гарантира, че умножението не се случва след извикването на последната функция. За да направим това, можем да използваме default параметър и да преместим операцията за умножение извън return изявлението. Получената функция носи временен резултат в следващата итерация на създадената на функция, която работи по същия начин, но може да бъде оптимизирана от ECMAScript 6 машината. Ето новия код:

function factorial(n, p = 1) {
    if (n <= 1) {
        return 1 * p;
    } else {
        let result = n * p;
        // оптимизирана
        return factorial(n - 1, result);
    }
}
                  

В тази пренаписана версия на factorial(), вторият аргумент p е добавен, като параметър с default стойност 1. Параметъра p държи предишния резултат от умножението, така че следващия резултат може да бъде изчислен, без друга извикваща функция. Когато n е по-голямо от 1, се извършва първо умножението и след това се подава, като втори аргумент на factorial(). Това позволява на ECMAScript 6 машината да оптимизира рекурсивните извиквания.

Tail call оптимизацията е нещо, за което трябва да се мисли, когато пишете рекурсивни функции, тъй като тя може да осигури значително подобрение в производителноста, особено когато се прилага в изчисляването на по-скъпа функция.