Създаване на символи

Символите са уникални сред JavaScript примитивните типове, с това че не разполагат с литерална форма, като true за булев тип или 42 за числа. Можете да създадете символ с помощта на глобалната Symbol функция, както в този пример:

let firstName = Symbol();
let person = {};

person[firstName] = "Nicholas";
console.log(person[firstName]);     // "Nicholas"
	 		

В този пример символа firstName е създаден и се използва за присвояване от новото свойство на person обекта. Този символ трябва да се използва всеки път, когато искате да получите достъп до същото свойство. Именуването на променливата на символа подходящо е добра идея, така че лесно да разберем какво представлява символа.

worning
Тъй като символите са примитивни стойности, извикването на new Symbol() хвърля грешка. Възможно е да се създаде инстанция на Symbol чрез new Object(yourSymbol), но не е ясно, кога това би било полезно.

Функцията Symbol приема незадължителен аргумент с описанието на символа. Самото описание не може да се използва за достъп до свойството, но се използва с цел на отстраняване на грешки. Например:

let firstName = Symbol("first name");
let person = {};

person[firstName] = "Nicholas";

console.log("first name" in person);    // false
console.log(person[firstName]);         // "Nicholas"
console.log(firstName);                 // "Symbol(first name)"
	 		

Описанието на символа се съхранява във вътрешно свойство, наречено [[Description]]. Това свойство се чете, когато метода toString() за символи се извика пряко или косвено. В този пример, console.log извиква косвено символния метод toString върху firstName, така че описанието да бъде отпечатано на козолата. Няма друг възможен начин за достъп до [[Description]] директно с код. Затова е препоръчително винаги да се предостави описание за четене на символа, така грешките се отстраняват по лесно.

Идентифициране на символи

Тъй като, символите са примитивни стойности, можете да използвате оператора typeof, за да определите дали дадена променлива съдържа символ. ECMAScript 6 разширява typeof да връща "symbol", когато се използва за символ. Например:

let symbol = Symbol("test symbol");
console.log(typeof symbol);    // "symbol"
	 		

Докато има и други косвени начини за определяне дали дадена променлива е символ, typeof е най-точен и предпочитан начин затова.

Използване на символи

Можете да използвате символи навсякъде, където трябва да изчислявате име на свойство. Вече виждяхте използването на скоби нотация със символи в тази глава, но можете да използвате символите за изчисляване на имена на свойства за обекти, както с Object.defineProperty() така и с Object.defineProperties(), като това:

let firstName = Symbol("first name");
// използва изчисляване на свойство за обект литерал 
let person = {
    [firstName]: "Nicholas"
};

// прави свойството само за четене
Object.defineProperty(person, firstName, { writable: false  });

let lastName = Symbol("last name");

Object.defineProperties(person, {
    [lastName]: {
        value: "Zakas",
        writable: false 
    }
});

console.log(person[firstName]);     // "Nicholas"
console.log(person[lastName]);      // "Zakas"
	 		

Този пример първо използва изчисляване на свойство за обект, за създаване на firstName символ свойството. Свойството е създадено не номерирано, за разлика от изчислените свойства създадени с помощта на nonsymbol имена. Следващия ред, прави свойството само за четене. По-късно, свойството на символа само за четене lastName е създадено с помощта на Object.defineProperties() метода. Изчисляването на свойство за обект се използва отново, само че този път е част от втория аргумент на Object.defineProperties().

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

Споделяне на символи

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

Когато искате да създадете символ, който да бъде споделен, трябва да използвате Symbol.for() вместо Symbol(). Метода Symbol.for() приема един единствен параметър, който е string идентификатор за символа, който искате да създадете. Този параметър се използва, и като описание на символа. Например:

let uid = Symbol.for("uid");
let object = {};

object[uid] = "12345";

console.log(object[uid]);       // "12345"
console.log(uid);               // "Symbol(uid)"
	    	

Метода Symbol.for() първо търси в глобалния регистър за символи, за да види дали символ с ключ "uid" съществува. Ако е така, метода връща съществуващия символ. Ако няма съществуващ такъв символ, тогава създава нов символ и го регистрира в глобалния регистър за символи, използвайки указания ключ. След това връща новия символ. Това означава, че следващите извиквания на Symbol.for() ще използват един и същи ключ за връщане на същия символ:

let uid = Symbol.for("uid");
let object = {
    [uid]: "12345"
};

console.log(object[uid]);       // "12345"
console.log(uid);               // "Symbol(uid)"

let uid2 = Symbol.for("uid");

console.log(uid === uid2);      // true
console.log(object[uid2]);      // "12345"
console.log(uid2);              // "Symbol(uid)"
	    		

В този пример, uid и uid2 съдържат един и същи символ, така че могат да се използват, като взаимозаменяеми. Първото извикване на Symbol.for() създава символ, а второто извлича символа от регистъра за глобални символи.

Друг уникален аспект на споделените символи е, че можете да извлечете ключа, свързан със символа в регистъра за глобални символи с помощта на Symbol.keyFor(), например:

let uid = Symbol.for("uid");
console.log(Symbol.keyFor(uid));    // "uid"

let uid2 = Symbol.for("uid");
console.log(Symbol.keyFor(uid2));   // "uid"

let uid3 = Symbol("uid");
console.log(Symbol.keyFor(uid3));   // undefined
	    	

Забележете, че и двете uid и uid2 връщат ключ "uid". Символа uid3 не съществува в регистъра за глобални символи, така че няма ключ свързан с него и Symbol.keyFor() връща undefined.

worning
Регистърът за глобални символи е споделена среда, точно както глобалния обхват. Така че, не могат да се правят предположения, какво присъства или не в тази среда. Вие трябва да използвате пространството на имена за ключове на символи, за да се намали вероятноста от съвпадения при използване на компонентите от трета страна. Например, jQuery кода може да използва "jquery.", за да сложи префикс на всички ключове, като "jquery.element" и други подобни.

Символ коригиране на типа

Коригирането на типа е значителна част от JavaScript и има много гъвкаво отношение към способноста на езика да коригира един тип данни в друг. Символите, обаче не са толкова гъвкави, когато става въпрос за коригиране, защото не съществува логическа еквивалентност на символ в другите типове. По-конкретно, символите не могат да бъдат коригирани в strings или numbers, така че не могат случайно да се използват, като свойства, които в противен случай биха се очаквали да се държат, като символи.

Примерите в тази глава използват console.log() за да покажат на изхода, че символа работи, защото console.log() извиква String() върху символите, за да създаде полезен резултат. Можете да използвате String() директно за да получите същия резултат. Например:

let uid = Symbol.for("uid"),
    desc = String(uid);

console.log(desc);              // "Symbol(uid)"
	    	

Функцията String() извиква uid.toString() и връща описание на символа в string. Ако се опитате да свържете директно символа със string, ще бъде хвърлена грешка:

let uid = Symbol.for("uid"),
    desc = uid + "";            // error!
	    	

Конкатенацията на uid с празен string изисква, първо uid да се коригира в string. Грешка се хвърля, когато е открито коригиране, предотвратявайки използването му по този начин.

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

let uid = Symbol.for("uid"),
    sum = uid / 1;            // error!
	    	

Този пример се опитва да раздели символ с 1, което води до грешка. Грешка се хвърля независимо от използвания математически оператор (логическите оператори не хвърлят грешка, защото всички символи се считат за еквивалентни на true, точно както всяка друга не-празна стойност в JavaScript).

Извличане на символ от обект

Методите Object.keys() и Object.getOwnPropertyNames() могат да извличат всички имена на свойства в даден обект. Първия метод връща всички номерирани имена на свойства, а втория връща всички свойства, независимо от номерирането. Нито един от тези методи, не връща свойства на символ, за да се запази тяхната ECMAScript 5 функционалност. Вместо тях е добавен метода Object.getOwnPropertySymbols() в ECMAScript 6, който позволява извличане на свойства на символи от даден обект.

Върнатата стойност от Object.getOwnPropertySymbols() е масив от собствени символни свойства, например:

let uid = Symbol.for("uid");
let object = {
    [uid]: "12345"
};

let symbols = Object.getOwnPropertySymbols(object);

console.log(symbols.length);        // 1
console.log(symbols[0]);            // "Symbol(uid)"
console.log(object[symbols[0]]);    // "12345"
	    	

В този код, object има едно единствено символ свойство, наречено uid. Върнатия масив от Object.getOwnPropertySymbols() съдържа само този символ.

Всички обекти започват с нулеви символни свойства, но обектите могат да наследяват символни качества от своите прототипи. ECMAScript 6 предварително настройва няколко такива свойства, с помощта на well-known символите.

Излагане на вътрешни операции с Well-Known символи

Важна тема за ECMAScript 5 е да изложи и определи някои "магически" части на JavaScript - частите, които програмистите не могат да подражават в момента. ECMAScript 6 следва тази традиция чрез излагане на повече от преди на вътрешната логика на езика, главно чрез използване на прототипа на символни свойства, за определяне на основното поведение на някои обекти.

ECMAScript 6 включва предварително зададени символи, наречени well-known символи, които представляват общо поведение в JavaScript и които преди това са били обмисляни само за вътрешни операции. Всеки well-known символ е представен от свойство на Symbol обекта, например като Symbol.create

Well-known символи са:

  • Symbol.hasInstance - метод използван от instanceof да определи наследството на даден обект.
  • Symbol.isConcatSpreadable - булева стойност, показваща дали употребата на Array.prototype.concat() ще изглади елементите на колекцията, ако колекцията се подаде, като параметър на Array.prototype.concat().
  • Symbol.iterator - метод, който връща iterator (итераторите са обхванати в Глава 8 - Iterators и Generators).
  • Symbol.match - метод използван от String.prototype.match() за сравняване на strings.
  • Symbol.replace - метод използван от String.prototype.replace() за заместване на substrings.
  • Symbol.search - метод използван от String.prototype.search() за намиране на substrings.
  • Symbol.species - конструктор за правене на производни обекти (производни обекти са обхванати в Глава 9 - Класове).
  • Symbol.split - метод използван от String.prototype.split() за разделяне на strings.
  • Symbol.toPrimitive - метод който връща представянето на примитивна стойност в обекта.
  • Symbol.toStringTag - string използван от Object.prototype.toString() за създаване на описание на обект.
  • Symbol.unscopables - обект, чиито свойства са имената на свойствата на обекта, които не трябва да бъдат включени в with изявление.

Някои често използвани well-known символи са обсъдени по-долу в раздела, а други са разгледани в останалата част от книгата, за да ги държи в правилния контекст.

info
Заменящия метод дефиниран с well-known символ, променя обикновен обект в екзотичен, защото това променя някои вътрешни поведения по подразбиране. Това няма практическо отражение върху вашия код, като резултат, просто променя начина, по който спецификация описва обекта.

Symbol.hasInstance

Всяка функция има Symbol.hasInstance метод, които определя дали даден обект е инстанция на тази функция. Метода е дефиниран от Function.prototype, така че всички функции наследяват това поведение по подразбиране за instanceof свойството. В Symbol.hasInstance, самото свойство се определя, като nonwritable и nonconfigurable, като nonenumerable гарантира, че не се презаписва по погрешка.

Метода Symbol.hasInstance приема един единствен аргумент: стойността за проверка. Той връща true, ако подадената стойност е инстанция на функцията. За да разберете, как Symbol.hasInstance работи, нека да разгледаме следния код:

obj instanceof Array;
	    	

Този код е еквивалентен на:

Array[Symbol.hasInstance](obj);
	    	

По същество, ECMAScript 6 предефинира instanceof оператора, като кратък запис на синтаксиса за извикване на този метод. И сега, когато имате участващ извикан метод, всъщност можете да промените начина на работа на instanceof.

Да предположим, че искате да дефинирате функция, която претендира, че няма обект, като инстанция. Можете да го направите hardcoding връщайки от Symbol.hasInstance false, като:

function MyObject() {
    // ...
}

Object.defineProperty(MyObject, Symbol.hasInstance, {
    value: function(v) {
        return false;
    }
});

let obj = new MyObject();

console.log(obj instanceof MyObject);       // false
	    	

Трябва да използвате Object.defineProperty() за презаписване на nonwritable свойство, така че, този пример да използва този метод, за да презапише Symbol.hasInstance с новата функция. Новата функцията винаги ще връща false, въпреки, че obj всъщност е инстанция на MyObject, защото оператора instanceof връща false след извикването на Object.defineProperty().

Разбира се, може също да инспектира стойността и да реши дали стойността трябва да се разглежда, като нейна инстанция въз основа на всяко произволно състояние. Например, може би числа със стойност между 1 и 100, трябва да се считат за инстанция на специален вид номер. За да се постигне това поведение, може да се напише код, подобен на този::

function SpecialNumber() {
    // ...
}

Object.defineProperty(SpecialNumber, Symbol.hasInstance, {
    value: function(v) {
        return (v instanceof Number) && (v >= 1 && v <= 100);
    }
});

let two = new Number(2),
    zero = new Number(0);

console.log(two instanceof SpecialNumber);    // true
console.log(zero instanceof SpecialNumber);   // false
	    	

Този код дефинира Symbol.hasInstance метод, който връща true, ако стойността е инстанция на Number и е в диапазона между 1 и 100. По този начин, позволява на SpecialNumber да претендира, че two е нейна инстанция въпреки, че няма пряка връзка между функцията SpecialNumber и two променливата. Имайте в предвид, че левият операнд на instanceof трябва да бъде обект, за да задейства извикването към Symbol.hasInstance, докато nonobjects причинява instanceof просто да връща false през цялото време.

worning
Можете също така да замените по подразбиране Symbol.hasInstance свойството, на всички вградени функции, като Date и Error. Това не се препоръчва, тъй като ефектите върху вашия код може да бъдат неочаквани и объркващи. Добра идея е, да заменяте Symbol.hasInstance на вашите собствени функции само когато е необходимо.

Symbol.isConcatSpreadable

JavaScript масивите имат concat() метод, който има за цел да залепи два масива заедно, например:

let colors1 = [ "red", "green" ],
    colors2 = colors1.concat([ "blue", "black" ]);

console.log(colors2.length);    // 4
console.log(colors2);        // ["red","green","blue","black"]
	    	

Този код конкатенира нов масив от края на colors1 и създава colors2, в единичен масив с всички елементи от двата масива. Обаче, concat() може да приема nonarray аргументи и в този случай тези аргументи просто се добавят към края на масива. Например:

let colors1 = [ "red", "green" ],
    colors2 = colors1.concat([ "blue", "black" ], "brown");

console.log(colors2.length);    // 5
console.log(colors2);           // ["red","green","blue","black","brown"]
	    	

Тука, допълнителния аргумент "brown" се подава на concat() и се превръща в петия елемент на colors2. Защо масив от аргументи се третира по различен начин от string аргументи? Спецификацията казва, че масивите автоматично се разделят на техните отделни елементи, а всички други типове не. Преди ECMAScript 6, нямаше начин да се коригира това поведение.

Свойството Symbol.isConcatSpreadable е булева стойност, показваща дали даден обект има lenght свойство и цифрови ключове и дали тези номерирани стойности на свойства трябва да бъдат добавяни поотделно към резултата с concat(). За разлика от други well-known символи, това символ свойство не се дава на всички стандартни обекти по подразбиране. Вместо това, то е достъпно, като начин да се увеличи работата на concat() върху някои типове обекти, като поведение на късо съединение по подразбиране. Можте да определите всеки тип да се държи, както масиви в concat(), също като:

let collection = {
    0: "Hello",
    1: "world",
    length: 2,
    [Symbol.isConcatSpreadable]: true
};

let messages = [ "Hi" ].concat(collection);

console.log(messages.length);    // 3
console.log(messages);           // ["hi","Hello","world"]
	    	

Обекта collection в този пример, е настроен да изглежда, като масив: тoй има length свойство и два цифрови ключа. Свойството Symbol.isConcatSpreadable е определено на true за да покаже, че стойностите на свойствата трябва да се добавят, като отделни елементи към масива. Когато collection се подаде на concat() метода, резултата е масив, който съдържа "Hello" и "world", като отделни елементи след "hi" елемента.

info
Може също да настроите Symbol.isConcatSpreadable на false за масив от подкласове, за да се предотврати елементите да бъдат слепени посредством concat(). Под-класовете се обсъждат в Глава 9.

Symbol.match, Symbol.replace, Symbol.search и Symbol.split

Винаги е имало тясна връзка между strings и регулярни изрази в JavaScript. Типа string по специално, има няколко метода, които приемат регулярни изрази, като аргументи.

  • match(regex) - определя дали даден string съвпада с регулярен израз.
  • replace(regex, replacement) - замества съвпадащия регулярен израз със заменящия
  • search(regex) - открива съвпадение на регулярния израз вътре в string
  • split(regex) - разделя string в масив по съвпаденията на регулярния израз

Преди ECMAScript 6, начина по който тези методи са взаимодействали с регулярния израз е бил скрит от програмистите, не оставяйки никакъв начин да се имитира това, което регулярния израз прави с помощта на определени от програмиста обекти. ECMAScript 6 дефинира четири символа, които отговарят на тези четири метода, ефективно отговарящи на поведението на вградения RegExp обект.

Символите Symbol.match, Symbol.replace, Symbol.search и Symbol.split представляват методи на аргумента на регулярния израз, който трябва да се извика, като първи аргумент на методите match(), replace(), search() и split(), съответно. Четирите символни свойства са дефинирани в RegExp.prototype , като те трябва да се използват по подразбиране при изпълнение на методите.

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

  • Symbol.match - функция, която приема за аргумент string и връща масив от съвпадения или null, ако не е намерила съвпадение.
  • Symbol.replace - функция, която приема за аргумент string и string за замяна и връща string.
  • Symbol.search - функция, която приема за аргумент string и връща номера с индекса на съвпадението или -1, ако не е намерила съвпадение.
  • Symbol.split- функция, която приема за аргумент string и връща масив съдържащ парчета от string-а, разделени от съвпаденията

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

// ефективно еквивалентно на /^.{10}$/
let hasLengthOf10 = {
    [Symbol.match]: function(value) {
        return value.length === 10 ? [value.substring(0, 10)] : null;
    },
    [Symbol.replace]: function(value, replacement) {
        return value.length === 10 ? replacement + value.substring(10) : value;
    },
    [Symbol.search]: function(value) {
        return value.length === 10 ? 0 : -1;
    },
    [Symbol.split]: function(value) {
        return value.length === 10 ? ["", ""] : [value];
    }
};

let message1 = "Hello world",   // 11 characters
    message2 = "Hello John";    // 10 characters


let match1 = message1.match(hasLengthOf10),
    match2 = message2.match(hasLengthOf10);

console.log(match1);            // null
console.log(match2);            // ["Hello John"]

let replace1 = message1.replace(hasLengthOf10),
    replace2 = message2.replace(hasLengthOf10);

console.log(replace1);          // "Hello world"
console.log(replace2);          // "Hello John"

let search1 = message1.search(hasLengthOf10),
    search2 = message2.search(hasLengthOf10);

console.log(search1);           // -1
console.log(search2);           // 0

let split1 = message1.split(hasLengthOf10),
    split2 = message2.split(hasLengthOf10);

console.log(split1);            // ["Hello world"]
console.log(split2);            // ["", ""]
	   	

Тука, hasLengthOf10 обекта е предназначен да работи подобно на регулярен израз, който съвпада, когато дължината (length) на string е точно 10. Всеки от четирите метода се осъществява чрез използване на съответните символи и след това се извикват съответните методи на двата strings. Първия string, message1, има 11 знака и така няма да съвпадне; втория string, message2, има 10 знака и така ще съвпадне. Въпреки, че не е регулярен израз hasLengthOf10 се подава на всеки string метод и се използва правилно поради допълнителните методи.

Въпреки, че това е един прост пример, възможността да се извършват по-сложни съвпадения, от колкото е възможно в момента с регулярни изрази отваря много възможности.

Symbol.toPrimitive

JavaScript често се опитва да превърне обекти в примитивни стойности по подразбиране, когато се прилагат определени операции. Например, когато се сравнява string с даден обект с помощта на двойното равно (==), обекта се превръща в примитивна стойност преди това сравняване. Точно каква примитивна стойност трябва да се използва е вътрешна операция, която ECMAScript 6 излага чрез метода Symbol.toPrimitive.

Метода Symbol.toPrimitive се определя за прототип на всеки стандартен тип и предписва точното поведение, когато обектът се превръща в примитивен тип. Когато е необходимо примитивно преобразуване, се извиква Symbol.toPrimitive с един аргумент, посочен като hint в спецификацията. Аргумента hint е един от следните три string стойности:

  • Ако hint e "number" - Symbol.toPrimitive трябва да върнe номер
  • Ако hint e "string" - трябва да върне string
  • Ако hint е "default" - операцията няма предпочитания за вида

За повечето стандартни обекти, поведението на number режим има следния приоритет:

  1. Извиква valueOf(), и ако резултата е примитивна стойност я връща.
  2. В противен случай, извиква toString() и ако резултата е примитивна стойност я връща.
  3. В противен случай хвърля грешка.

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

  1. Извиква toString() и ако резултата е примитивна стойност я връща.
  2. В противен случай, извиква valueOf() и ако резултата е примитивна стойност я връща.
  3. В противен случай хвърля грешка.

В много случаи стандартните обекти третират режима по подразбиране, като равностоен на number режима (с изключение на Date, която се отнася към режима по подразбиране, като равностоен на string режима). Чрез дефиниране на Symbol.toPrimitive, можем да коригираме това поведение по подразбиране.

info
Режим по подразбиране се използва само за ==, + и при подаване на единичен аргумент към Date конструктора. Повечето операции използват string или number режим.

За да замените поведението по подразбиране, използвайте Symbol.toPrimitive и задайте функция, като негова стойност, Например:

function Temperature(degrees) {
    this.degrees = degrees;
}

Temperature.prototype[Symbol.toPrimitive] = function(hint) {

    switch (hint) {
        case "string":
            return this.degrees + "\u00b0"; // degrees symbol

        case "number":
            return this.degrees;

        case "default":
            return this.degrees + " degrees";
    }
};

let freezing = new Temperature(32);

console.log(freezing + "!");            // "32 degrees!"
console.log(freezing / 2);              // 16
console.log(String(freezing));          // "32째"
	   	

Този пример дефинира Temperature конструктор и заменя метода по подразбиране с Symbol.toPrimitive на прототипа. Различна стойност се връща в зависимост от това дали hint аргумента е string, number или default стойност. В string режима функцията Temperature() връща температурата с Unicode характера за градус. В number режима връща само цифрова стойност, а в режима по подразбиране добавя думата “degrees” след цифрите.

Всяко от твърдения в console.log предизвиква различна hint аргумент стойност. Операторът + предизвиква default режим, чрез определяне на hint към "default", операторът / предизвиква number режим, чрез определяне hint към "number" и String () функцията предизвиква string режим, чрез определяне hint към 'string'. Въпреки че е възможно да върне различни стойности за всичките три режима, много по-често използвано е да настроите default режима да е същия, като за string или number режим.

Symbol.toStringTag

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

В каноничен пример, това е подаване на масив от вградената рамка в съдържащата страница или обратно. В ECMAScript 6 терминологията, вградена рамка и съдържаща страница, всяка представлява различна област, която е среда за изпълнение на JavaScript. Всяка област има свой собствен глобален обхват с негово собствено копие на глобални обекти. В зависимост от това в коя област се създава масива, се определя вида на масива. Когато това е в различна среда на изпълнение instanceof Array връща false, защото масива е създаден с конструктор от различни среди.

Временно решение за идентифициране на проблема

Сблъсквайки се с този проблем програмистите много скоро намериха добър начин за идентифициране на масиви. Те открили, че когато се извика стандартния метод toString() върху обект, се връща винаги предвидим string. По този начин , много JavaScript библиотеки, започнаха да включват функция, която работи подобно на това:

function isArray(value) {
    return Object.prototype.toString.call(value) === "[object Array]";
}

console.log(isArray([]));   // true
	   	

Това може да изглежда малко, като кръгово движение, но в действителност е установено, че работи добре за идентифициране на масиви във всички браузъри. Метода toString() за масиви не е много полезен за идентифициране на обект, тъй като връща string представяне на елементите на обекта, които съдържа. Но методa toString () на Object.prototype има тази идея: той включва вътрешно-дефинирано име, наречено [[class]] във върнатия резултат. Програмистите могат да използват този метод върху даден обект, за да извлекат, това което околната среда на JavaScript смята за тип на данните на обекта.

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

Преди ECMAScript 5, много употребяван е Douglas Crockford’s json2.js, който създава глобален JSON обект. Когато браузърите започнаха да прилагат глобалния JSON обект, се наложи да се каже дали глобалния JSON е предоставен от самата среда на JavaScript или чрез някоя друга библиотека. Използвайки същата техника, която показах с функцията isArray(), много програмисти създадоха функции, като тази:

function supportsNativeJSON() {
    return typeof JSON !== "undefined" &&
      Object.prototype.toString.call(JSON) === "[object JSON]";
}
	   	

Това, е същата характеристика на Object.prototype, която позволява на програмистите да идентифицират масиви в границите на вградената рамка и също предоставя начин да се разбере дали JSON е местен обект или не. Не-местен JSON обект ще върне [object Object], докато местния обект ще върне [object JSON]. Този подход се превърна в стандарт за идентифициране на местни обекти.

Отговора на ECMAScript 6

ECMAScript 6 предефинира това поведение чрез Symbol.toStringTag символа. Този символ представлява свойство на всеки обект, което определя, каква стойност трябва да се произведе, когато Object.prototype.toString.call() се извика върху него. За масив, стойността която функцията връща се обяснява със съхранение на "Array" в Symbol.toStringTag свойството.

По същия начин, можете да определите Symbol.toStringTag стойността на вашите собствени обекти.

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

Person.prototype[Symbol.toStringTag] = "Person";

let me = new Person("Nicholas");

console.log(me.toString());                         // "[object Person]"
console.log(Object.prototype.toString.call(me));    // "[object Person]"
	   	

В този пример, свойството Symbol.toStringTag се дефинира в Person.prototype, за да осигури поведение по подразбиране за създаване на string представяне. Тъй като, Person.prototype наследява Object.prototype.toString() метода, стойността върната от Symbol.toStringTag се използва също, като извикване на me.toString() метода. Въпреки това, все още можете да определите свой toString() метод, който осигурява различно поведение, без да засяга използването на Object.prototype.toString.call() метода. Ето как може да изглежда това:

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

Person.prototype[Symbol.toStringTag] = "Person";

Person.prototype.toString = function() {
    return this.name;
};

let me = new Person("Nicholas");

console.log(me.toString());                         // "Nicholas"
console.log(Object.prototype.toString.call(me));    // "[object Person]"
	   	

Този контекст дефинира Person.prototype.toString() да връща стойността на name свойството. Тъй като Person инстанцията вече не се наследява от Object.prototype.toString() метода, извиквайки me.toString() проявява различно поведение.

info
Всички обекти наследяват Symbol.toStringTag от Object.prototype, освен ако не е посочено нещо друго. Стойността на свойството по подразбиране е "Object".

Няма ограничения с кои стойности може да бъде използван Symbol.toStringTag върху определените от програмиста обекти. Например, нищо не пречи да използваме "Array", като стойност на Symbol.toStringTag, също като:

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

Person.prototype[Symbol.toStringTag] = "Array";

Person.prototype.toString = function() {
    return this.name;
};

let me = new Person("Nicholas");

console.log(me.toString());                         // "Nicholas"
console.log(Object.prototype.toString.call(me));    // "[object Array]"
	   	

В този код, резултата от извикването на Object.prototype.toString() е "[object Array]", което е същото, като което ще получим от действителен масив. Това подчертава факта, че Object.prototype.toString() вече не е напълно надежден начин за идентифициране на типа на даден обект.

Възможно е да се промени string маркера на местния обект, чрез присвояване на Symbol.toStringTag на неговия прототип. Например:

Array.prototype[Symbol.toStringTag] = "Magic";

let values = [];

console.log(Object.prototype.toString.call(values));    // "[object Magic]"
	   	

Въпреки, че Symbol.toStringTag се презаписва за масива в този пример, извикването на Object.prototype.toString() води до "[object Magic]". Въпреки, че не се препоръчва да се променят вградени обекти по този начин, няма нищо в езика, което да го забранява.

Symbol.unscopables

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

В резултат на това, with изявлението не е разширено в strict mode, което се отразява на класове и модули, които са под strict mode по подразбиране, без право на изключения.

Въпреки, че няма бъдеще за with изявлението, ECMAScript 6 все още го поддържа в nonstrict mode за обратна съвместимост и като такова, трябва да намери начин да позволи на кода да продължи да работи правилно с използването на with.

За да се разбере сложността на тази задача, да разгледаме следния код:

let values = [1, 2, 3],
    colors = ["red", "green", "blue"],
    color = "black";

with(colors) {
    push(color);
    push(...values);
}

console.log(colors);    // ["red", "green", "blue", "black", 1, 2, 3]
	   	

В този пример, двете извиквания на push() вътре в with изявлението, са еквивалентни на colors.push(), защото with изявлението добавя puhs в локалното обвързване. Препратката към color се отнася до променлива, създадена извън whit, което прави values референтна.

ECMAScript 6 добавя метода values() за масиви (Метода values() е разгледан по-подробно в Глава 8 - Iterators и Generators). Това би означавало, че в ECMAScript 6 среда, препратката към values в рамките на with изявлението, трябва да се отнася не към локалната променлива values, а към стойностите на метода values на масива, което ще прекъсне кода. Ето, защо символа Symbol.unscopables съществува.

Символа Symbol.unscopables се използва за Array.prototype за да посочи, кои свойства не трябва да създават обвързвания във вътрешността на with изявление. Когато е налице, Symbol.unscopables е обект, чийто ключове са идентификатори за пропускане на whit обвързвания и чийто стойности са true за прилагане на блока. Тук е по подразбиране за масиви:

// вградено в ECMAScript 6 по подразбиране
Array.prototype[Symbol.unscopables] = Object.assign(Object.create(null), {
    copyWithin: true,
    entries: true,
    fill: true,
    find, true,
    findIndex: true,
    keys: true,
    values: true
});
	   	

Обекта Symbol.unscopables има null прототип създаден от Object.create(null) и съдържа всички нови методи за масиви в ECMAScript 6 (тези методи са разгледани подробно в Глава 8 - Iterators и Generators и в Глава 10 - Arrays). Обвързванията за тези методи не са създадени вътре в with изявлението, което позволява на стария код да продължи да работи без никакъв проблем.

По принцип, не трябва да се дефинира Symbol.unscopables за вашите обекти, освен ако не използвате with изявление или се правят промени в съществуващ обект във вашия база код.