Категории обекти

JavaScript използва комбинация от терминологии за описване на обекти намираща се в стандарта, за разлика от тази добавена от средите за изпълнение, като браузъра или Node.js и спецификацията на ECMAScript 6 има ясни определения за всяка категория обекти. Важно е да се разбере тази терминология, за да имате добро разбиране на езика, като цяло. Категориите обекти са:

  • Обикновени обекти - са всички обекти, които имат стандартно вътрешно поведение в JavaScript.
  • Екзотични обекти - са които имат вътрешно поведение, което се различава от това поведение по подразбиране по някакъв начин.
  • Стандартни обекти - са тези определени от ECMAScript 6, като Array, Date и т.н. Стандартните обекти могат да бъдат обикновени или екзотични.
  • Вградени обекти - са тези, които се намират в средата на изпълнение на JavaScript, когато скрипта започва да се изпълнява. Всички стандартни обекти са вградени обекти.

Ще използвам тези термини в цялата книга, за да обясня различните обекти, определени от ECMAScript 6.

Синтаксис разширения на обект

Обект е един от най-популярните модели в JavaScript. JSON е изграден върху своя синтаксис и това е почти всеки JavaScript файл в интернет. Обектът е толкова популярен, защото е с кратък синтаксис за създаване, който иначе би заел няколко реда код. За щастие на програмистите, ECMAScript 6 прави обекта по-мощен и по-кратък, чрез разширяване на синтаксиса по няколко начина.

Кратко инициализиране на свойства

В ECMAScript 5 и по-рано, обектите са просто колекции от двойки имена и стойност. Това означава, че може да има известно дублиране, когато се инициализира стойността на свойствата. Например:

function createPerson(name, age) {
    return {
        name: name,
        age: age
    };
}	
			

Функцията createPerson() създава обект, чийто имена на свойства са същите, като имената на параметрите на функцията. Резултатът изглежда е дублиране на name и age, въпреки че едното е името на свойството на обекта, а другото осигурява стойност на това свойство. Ключа name във върнатия обект присвоява стойността съдържаща се в променливата name, а ключа age във върнатия обект присвоява стойността съдържаща се в променливата age.

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

function createPerson(name, age) {
    return {
        name,
        age
    };
}	
			

Когато едно свойство в обект има име, JavaScript машината оглежда локалния обхват за променлива със същото име. Ако установи една, стойността на променливата присвоява същото име в обекта. В този пример, свойството на обекта name присвоява стойността на локалната променлива name.

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

Кратки методи

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

var person = {
    name: "Nicholas",
    sayName: function() {
        console.log(this.name);
    }
};	
			

В ECMAScript 6, синтаксиса е направен по кратък с премахване на двуточието и на ключовата дума function. Така можем да пренапишем предишния пример, като този:

var person = {
    name: "Nicholas",
    sayName() {
        console.log(this.name);
    }
};	
			

Този кратък синтаксис, също наречен concise method синтаксис, създава метод на person обекта, както е направен в предишния пример. Свойството sayName() е присвоена анонимна функция и има еднакви характеристики с ECMAScript 5 sayName() функцията. Разликата е, че кратките методи могат да използват super (което ще обсъдим по-късно в секцията “Лесен достъп до прототипа със super референция”), докато дългите методи не могат.

info
Свойството name на метода създадено с помощта на краткия синтаксис за записване е името, използвано преди скобите. В последния пример, name свойството на person.sayName() е "sayName".

Изчисляване на имена на свойства

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

var person = {},
    lastName = "last name";

person["first name"] = "Nicholas";
person[lastName] = "Zakas";

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

Тъй като lastName присвоява стойността на "last name" и двете свойства на имена в този пример използват пространство, което прави невъзможно да се обръщаме към тях използвайки точкова нотация. Въпреки това, скоба-нотацията позволява на всяка string стойност да се използва, като име на свойство, така присвояването на "first name" с "Nicholas" и “last name" с "Zakas" работи.

Освен това, можете да използвате strings директно, като имена на свойства в обектите, като тези:

var person = {
    "first name": "Nicholas"
};

console.log(person["first name"]);      // "Nicholas"		
			

Този модел работи за имена на свойства, които са известни от по-рано и могат да бъдат представени със string. Ако обаче, свойството "first name" се съдържа в променлива (както в предишния пример) или трябва да бъде изчислено, тогава няма да има начин да зададете това свойство за използване в обект в ECMAScript 5.

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

var lastName = "last name";

var person = {
    "first name": "Nicholas",
    [lastName]: "Zakas"
};

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

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

var suffix = " name";

var person = {
    ["first" + suffix]: "Nicholas",
    ["last" + suffix]: "Zakas"
};

console.log(person["first name"]);      // "Nicholas"
console.log(person["last name"]);       // "Zakas"		
			

Тези свойства оценяват "first name" и "last name" и тези strings могат да се използват за указване на свойствата по-късно. Всичко, което сложите вътре в квадратните скоби, докато използвате скоби-нотацията в инстанцията на обекта, ще работи за изчисляване на имената на свойствата вътре в обекта.

Нови методи

Една от целите в дизайна на ECMAScript започващи от ECMAScript 5 е да се избегне създаването на нови глобални функции или методи на Object.prototype, вместо да се опита да намери обекти, за които новите методи трябва да бъдат на разположение. В резултат на това глобалния Object получава голям брой методи, когато няма други подходящи обекти. ECMAScript 6 въвежда няколко нови метода в глобалния Object, които са предназначени да направят някои задачи по-лесни.

Метода Object.is()

Когато искате да сравните две стойности в JavaScript, вие вероятно използвате оператора равно (==) или оператора за идентично равно (===). Много програмисти предпочитат последния за да избегнат различни корекции по време на сравнението. Но дори оператора за идентично равенство не е съвсем точен. Например, стойност +0 и -0 се считат равни с ===, въпреки че са представени по различен начин в JavaScript. Също NaN === NaN връща false, което налага използване на isNaN() за откриване на NaN правилно.

ECMAScript 6 въвежда Object.is() метода, за да компенсира особеностите на оператора за идентично равенство. Този метод приема два аргумента и връща true, ако стойностите са еквивалентни. Две стойности се считат за еквивалентни, когато са от същия тип и имат една и съща стойност. Ето няколко примера:

console.log(+0 == -0);              // true
console.log(+0 === -0);             // true
console.log(Object.is(+0, -0));     // false

console.log(NaN == NaN);            // false
console.log(NaN === NaN);           // false
console.log(Object.is(NaN, NaN));   // true

console.log(5 == 5);                // true
console.log(5 == "5");              // true
console.log(5 === 5);               // true
console.log(5 === "5");             // false
console.log(Object.is(5, 5));       // true
console.log(Object.is(5, "5"));     // false	
			

В много случаи Object.is() работи по същия начин, както оператора === . Единствените разлики са, че +0 и -0 не се считат за еквивалентни и NaN е еквивалентно на NaN. Не е необходимо да се спре използването на операторите за равенство изцяло. Избора дали да използвате Object.is() вместо == или === е въз основа на това, как тези специални случаи влияят върху вашия код.

Метода Object.assign()

Mixins са сред най-популярните модели в състава на обектите в JavaScript. В mixin, един обект получава свойства и методи от друг обект. Много JavaScript библиотеки имат mixin метод подобен на този:

function mixin(receiver, supplier) {
    Object.keys(supplier).forEach(function(key) {
        receiver[key] = supplier[key];
    });
    return receiver;
}	
        

Функцията mixin() минава с итерации над свойствата на supplier и ги копира в receiver (повърхностно копие, където се споделя позовавания обект, когато свойствата са обекти). Това позволява на receiver да се сдобие с нови свойства без наследяване, както в този код:

function EventTarget() { /*...*/ }
EventTarget.prototype = {
    constructor: EventTarget,
    emit: function() { /*...*/ },
    on: function() { /*...*/ }
};

var myObject = {};
mixin(myObject, EventTarget.prototype);

myObject.emit("somethingChanged");		
			

Тука, myObject получава поведение от EventTarget.prototype обекта. Това дава на myObject възможност да публикува събития и да се абонира за тях, използвайки emit() и on() методите, съответно.

Този модел става доста популярен, така че ECMAScript 6 добавя Object.assign() метод, който се държи по същия начин, приемайки receiver и произволен брой supplier, след което връща receiver. Промяната на името от mixin() на assign() отразява действителната работа, която се случва. Тъй като mixin() метода използва оператора за присвояване (=), той не може да копира accessor свойства на receiver, като accessor свойства. Името Object.assign() е избрано, за да отрази това разграничаване.

info
Подобни методи в различни библиотеки могат да имат други имена за една и съща основна функционалност, популярни заместници включват методите extend() и mix(). За кратко имаше метод Object.mixin() в ECMAScript 6 в допълнение на Object.assign() метода. Основната разлика е, че Object.mixin() също копира accessor свойства, но е премахнат поради опасения за използването на super (обсъдено в секцията “Лесен достъп до прототипа със super референция” на тази глава).

Можете да използвате Object.assign() навсякъде, където mixin() функцията е била използвана. Ето един пример:

function EventTarget() { /*...*/ }
EventTarget.prototype = {
    constructor: EventTarget,
    emit: function() { /*...*/ },
    on: function() { /*...*/ }
}

var myObject = {}
Object.assign(myObject, EventTarget.prototype);

myObject.emit("somethingChanged");
			

Метода Object.assign() приема произволен брой suppliers, и receiver получава свойствата в реда, по който са определени в suppliers. Това означава, че втория supplier може да замени стойност от първия supplier в receiver, което е показано в този откъс:

var receiver = {};

Object.assign(receiver,
    {
        type: "js",
        name: "file.js"
    },
    {
        type: "css"
    }
);
console.log(receiver.type);     // "css"
console.log(receiver.name);     // "file.js"
			

Стойността на receiver.type е "css", защото втория supplier презаписва стойността на първия.

Метода Object.assign() не е голямо допълнение в ECMAScript 6, но той формализира обща функция в много JavaScript библиотеки.

Работа с Accessor свойства

Имайте в предвид, че Object.assign() не създава accessor свойства на receiver, когато supplier има accessor свойства. Тъй като, Object.assign() използва оператор за присвояване, accessor свойството на supplier ще стане свойство за данни на receiver. Например:

var receiver = {},
    supplier = {
        get name() {
            return "file.js"
        }
    };

Object.assign(receiver, supplier);

var descriptor = Object.getOwnPropertyDescriptor(receiver, "name");

console.log(descriptor.value);      // "file.js"
console.log(descriptor.get);        // undefined		
			

В този код, supplier има accessor свойство наречено name. След използване на Object.assign() метода, receiver.name съществува, като свойство за данни със стойност "file.js", защото supplier.name връща "file.js", когато Object.assign() се извика.

Дублиране на свойства на обекта

ECMAScript 5 strict mode въведе проверка за дублиране на обектни свойства, която ще хвърли грешка, ако установи такова дублиране. Например този код е проблемен:

"use strict";

var person = {
    name: "Nicholas",
    name: "Greg"        // syntax error in ES5 strict mode
};	
			

При работа в ECMAScript 5 strict mode, второто name свойство предизвиква синтактична грешка. Но в ECMAScript 6, проверката за дублиране на свойства е отстранена. И двете strict и nonstrict mode вече не проверяват за дублирани свойства. Вместо това, последното свойство с даденото име става действителна стойност на свойството, както е показано тук:

"use strict";

var person = {
    name: "Nicholas",
    name: "Greg"        // no error in ES6 strict mode
};

console.log(person.name);       // "Greg"
			

В този пример стойността на person.name е "Greg", защото това е последната определена стойност на свойството.

Ред на изброяване на собствени свойства

ECMAScript 5 не определя реда на изброяване на обектни свойства. Обаче, ECMAScript 6 стриктно определя реда, в който собствените свойства трябва да бъдат върнати, когато се изброяват. Това се отразява на свойства , които се връщат след използването на Object.getOwnPropertyNames() и Reflect.ownKeys (разгледани в Глава 12). Това се отразява на реда, по който свойствата се обработват от Object.assign().

Основният ред за изброяване на собствени свойства е:

  1. Всички цифрови ключове във възходящ ред.
  2. Всички string ключове в реда, по който са били добавени към обекта.
  3. Всички simbol ключове (обхванати в Глава 6) в реда, по който са били добавени към обекта.

Ето един пример:

var obj = {
    a: 1,
    0: 1,
    c: 1,
    2: 1,
    b: 1,
    1: 1
};

obj.d = 1;

console.log(Object.getOwnPropertyNames(obj).join("")); 
                                            // "012acbd"	
			

Методът Object.getOwnPropertyNames() връща свойствата от obj по ред: 0, 1, 2, a, c, b, d. Обърнете внимание, че цифровите ключове са групирани и сортирани, въпреки че се появяват разбъркано в обекта. String ключовете идват след цифровите ключове и се появяват в реда, по който са добавени към obj. Ключовете в обекта са на първо място, следвани от всякакви динамични ключове, които са били добавени по-късно (в този случай, d).

worning
Цикъла for-in все още има неуточнено изброяване, тъй като не всички JavaScript машини го прилагат по един и същи начин. Методите Object.keys() и JSON.stringify() са определени да използват един и същи (неопределен) ред на изброяване, както е за for-in.

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

По-мощни прототипи

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

Промяна на прототипа на обектите

Нормално, прототипа на един обект се определя, когато обекта е създаден, чрез конструктор или чрез метода Object.create(). Идеята, че прототипа на даден обект остава непроменен след инстанция е едно от най-големите постижения на JavaScript програмирането в ECMAScript 5. ECMAScript 5 добавя метода Object.getPrototypeOf() за извличане на прототипа на даден обект, но все още не разполага със стандартен начин за промяна на прототипа на даден обект след инстанция.

ECMAScript 6 промени това положение, чрез добавяне на Object.setPrototypeOf() метода, който ви позволява да промените прототипа на даден обект. Метода Object.setPrototypeOf() приема два аргумента: обекта, чийто прототип трябва да се промени и обекта, който трябва да стане първи аргумент на прототипа. Например:

let person = {
    getGreeting() {
        return "Hello";
    }
};

let dog = {
    getGreeting() {
        return "Woof";
    }
};

// прототипа е person
let friend = Object.create(person);
console.log(friend.getGreeting());                   // "Hello"
console.log(Object.getPrototypeOf(friend) === person); // true

// определя прототипа на dog
Object.setPrototypeOf(friend, dog);
console.log(friend.getGreeting());                   // "Woof"
console.log(Object.getPrototypeOf(friend) === dog);    // true		
			

Този код определя два основни обекта: person и dog. И двата обекта са с getGreeting() метод, който връща string. Обекта friend първо наследява person обекта, което означава, че getGreeting() извежда "Hello". Когато прототипа се превръща в dog обект, person.getGreeting() извежда "Woof", защото оригиналната връзка към person е счупена.

Действителната стойност на прототипа на даден обект се съхранява във вътрешно свойство, наречено [[Prototype]]. Метода Object.getPrototypeOf() връща стойността съхранявана в [[Prototype]], а Object.setPrototypeOf() променя стойността съхранявана в [[Prototype]]. Въпреки това, те не са единствените начини да се работи със стойността на [[Prototype]].

Лесен достъп до прототипа със super референция

Както вече споменахме, прототипите са много важни за JavaScript и много труд изискваше да ги направят лесни за употреба в ECMAScript 6. Друго подобрение е въвеждането на super референция, която прави достъпа до функционалността на прототипа на даден обект по-лесен. Например, за да замените метод в инстанцията на обект, така че той да призовава метода на прототипа със същото име, трябва да направите следното:

let person = {
    getGreeting() {
        return "Hello";
    }
};

let dog = {
    getGreeting() {
        return "Woof";
    }
};

let friend = {
    getGreeting() {
        return Object.getPrototypeOf(this).getGreeting.call(this) + ", hi!";
    }
};

// определя прототипа на person
Object.setPrototypeOf(friend, person);
console.log(friend.getGreeting());                      // "Hello, hi!"
console.log(Object.getPrototypeOf(friend) === person);  // true

// определя прототипа на dog
Object.setPrototypeOf(friend, dog);
console.log(friend.getGreeting());                      // "Woof, hi!"
console.log(Object.getPrototypeOf(friend) === dog);     // true
			

В този пример, getGreeting() на friend извиква метода на прототип със същото име. Метода Object.getPrototypeOf() осигурява правилното извикване на прототипа и след това прилага допълнителен string към изхода. Допълнителното .call(this) гарантира, че тази this стойност вътре в метода на прототипа е зададена правилно.

Запомнянето на използването на Object.getPrototypeOf() и .call(this) за извикване на метода на прототипа е малко ангажиращо, затова ECMAScript 6 въвежда super. Най-просто казано, super е указател към текущия прототип на текущия обект, ефективно, както Object.getPrototypeOf(this) стойността. Знаейки това, можете да опростите getGreeting() метода, както следва:

let friend = {
    getGreeting() {
        // в предишния пример, this е също както:
        // Object.getPrototypeOf(this).getGreeting.call(this)
        return super.getGreeting() + ", hi!";
    }
};
			

Извикването на super.getGreeting() е същото, както Object.getPrototypeOf(this).getGreeting.call(this) в този контекст. Също така, можете да извикате всеки метод на прототипа на даден обект с помощта на super референция, токова дълго, колкото той е вътре в краткия метод. Опитите за използване на super извън краткия метод, ще доведе до синтактична грешка, както в този пример:

let friend = {
    getGreeting: function() {
	// syntax error
        return super.getGreeting() + ", hi!";
    }
};
			

Този пример използва името на свойството с функция и извикването на friend.getGreeting() хвърля грешка, защото super е невалиден в този контекст.

Super референцията е наистина мощна, когато имаме няколко нива на наследяване, тъй като в такива случаи Object.getPrototypeOf() вече не работи при всички обстоятелства. Например:

let person = {
    getGreeting() {
        return "Hello";
    }
};

// prototype is person
let friend = {
    getGreeting() {
        return Object.getPrototypeOf(this).getGreeting.call(this) + ", hi!";
    }
};
Object.setPrototypeOf(friend, person);

// prototype is friend
let relative = Object.create(friend);

console.log(person.getGreeting());            //"Hello"
console.log(friend.getGreeting());            //"Hello, hi!"
console.log(relative.getGreeting());          // error		
			

Извикването на Object.getPrototypeOf() води до грешка, когато се извика relative.getGreeting(). Това се случва, защото this е relative, а прототипа на relative е обекта friend. Когато friend.getGreeting().call() се извика с relative, като this, процесът започва отначало и продължава да се призовава рекурсивно, докато се появи грешка или се препълни стека.

Този проблем е труден за решаване в ECMAScript 5, но с ECMAScript 6 и super, е лесно:

let person = {
    getGreeting() {
        return "Hello";
    }
};

// prototype is person
let friend = {
    getGreeting() {
        return super.getGreeting() + ", hi!";
    }
};
Object.setPrototypeOf(friend, person);

// prototype is friend
let relative = Object.create(friend);

console.log(person.getGreeting());         // "Hello"
console.log(friend.getGreeting());         // "Hello, hi!"
console.log(relative.getGreeting());       // "Hello, hi!"		
			

Понеже super референциите не са динамични те винаги се отнасят до правилния обект. В този случай, super.getGreeting() винаги се отнася до person.getGreeting(), независимо от това, колко други обекти наследяват този метод.

A Formal Method Definition

Преди ECMAScript 6, концепцията “method” не е официално определена. Методите са само свойства на обекта, който съдържа функции вместо данни. ECMAScript 6 официално определя метода, като функция, която има вътрешно [[HomeObject]] свойство съдържащ обекта, към който принадлежи метода. Помислете върху следното:

let person = {

    // method
    getGreeting() {
        return "Hello";
    }
};

// not a method
function shareGreeting() {
    return"Hi!";
}	
			

Този пример дефинира person с един единствен метод, наречен getGreeting().В [[HomeObject]] за getGreeting() е person, чрез възлагане на функцията директно към дадения обект. Функцията shareGreeting() от друга страна, няма [[HomeObject]] спецификация, защото тя не е присвоена към даден обект, когато е създадена. В повечето случаи, тази разлика не е важна, но е особено важна, когато се използват super референции.

Всяко позоваване на super използва [[HomeObject]] за да определи какво да прави. Първата стъпка е да извика Object.getPrototypeOf() на [[HomeObject]] за да изтегли референция към прототипа. След това, прототипа търси функция със същото име. Накрая, this обвързването се определя и се извиква метода. Ето един пример:

let person = {
    getGreeting() {
        return "Hello";
    }
};

// prototype is person
let friend = {
    getGreeting() {
        return super.getGreeting() + ", hi!";
    }
};
Object.setPrototypeOf(friend, person);

console.log(friend.getGreeting());  // "Hello, hi!"
			

Извикването на friend.getGreeting() връща string, който съчетава стойността от person.getGreeting() с " .hi!". [[HomeObject]] на friend.getGreeting() е friend, а прототипа на friend e person, така че super.getGreeting() е еквивалентно на person.getGreeting.call(this).