Проблем с масиви

Масив обекта на JavaScript се държи по начин, който програмистите не можеха да имитират в собствените си обекти преди ECMAScript 6. Свойството length на масиви се засяга, когато присвоявате стойности към специфични елементи на масиви и може да променяте елементите на масиви чрез промяна на length свойството. Например:

let colors = ["red", "green", "blue"];

console.log(colors.length);         // 3

colors[3] = "black";

console.log(colors.length);         // 4
console.log(colors[3]);             // "black"

colors.length = 2;

console.log(colors.length);         // 2
console.log(colors[3]);             // undefined
console.log(colors[2]);             // undefined
console.log(colors[1]);             // "green"
  			

Тука, масива colors започва с три елемента. Присвояването на "black" към colors[3] автоматично увеличава стойността на length свойството на четири. Определянето на length свойството обратно на две, премахва последните два елемента в масива, оставяйки само първите два елемента. Няма нищо в ECMAScript 5, което да позволява на програмистите да постигнат същото поведение, но proxies променят това.

info
Това е нестандартно поведение, защото масивите се считат за екзотични обекти в ECMAScript 6.

Какво са Proxies и Reflection?

Можете да създадете proxy, което да използвате на място върху друг обект (наречен target ), като извикате new Proxy(). Proxy виртуализира мишената, така че proxy и target изглеждат, като един и същи обект на функционалност използвана от proxy.

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

Reflection API, представлявана от Reflect object, е колекция от методи, които дават поведение по подразбиране за същите операции от ниско ниво, които proxies могат да отменят. Има Reflect метод за всеки proxy trap. Тези методи имат същото име и му се подават същите аргументи, както на неговия съответен proxy trap. Таблицата по-долу съдържа всички proxy traps.

Proxy Trap Overrides the Behavior Of Default Behavior
get Reading a property value Reflect.get()
set Writing to a property Reflect.set()
has The in operator Reflect.has()
deletePropery The delete operator Reflect.deleteProperty()
getPrototypeOf Object.getPrototypeOf() Reflect.getPrototypeOf()
setPrototypeOf Object.setPrototypeOf() Reflect.setPrototypeOf()
isExtensible Object.isExtensible() Reflect.isExtensible()
preventExtensions Object.preventExtensions() Reflect.preventExtensions()
getOwnPropertyDescriptor Object.getOwnPropertyDescriptor Reflect.getOwnPropertyDescriptor()
defineProperty Object.defineProperty() Reflect.defineProperty()
ownKeys Object.keys, Object.getOwnPropertyNames(), Object.getOwnPropertySymbols() Reflect.ownKeys()
apply Calling a function Reflect.apply()
construct Calling a function with new Reflect.construct()

Всеки един traps замества някое от вградените поведения на JavaScript обектите, позволявайки ви да прихващате и променяте поведението. Ако все пак ви се наложи да използвате вградено поведение, тогава можете да използвате съответния reflection API метод. Връзката между proxies и reflection API става ясна, когато започнете да създавате proxies, така че най-добре да разгледаме няколко примера.

info
В първоначалната спецификация на ECMAScript 6 имаше допълнителен trap, наречен enumerate, който е проектиран да променя с for-in и Object.keys() начина на изброяване на свойствата на даден обект. Обаче, enumerate trap беше премахнат в ECMAScript 7 (наречен също ECMAScript 2016 г.), тъй като по време на изпълнението са открити трудности. enumerate trap вече не съществува в средата на JavaScript и следователно не е предмет на тази глава.

Създаване на Proxy

Proxies се създават с помощта на Proxy конструктора и подаване на два аргумента target (цел) и handler (манипулатор). Манипулатора е обект, който определя една или повече цели. Proxy използва поведение по подразбиране за всички операции, освен когато traps са дефинирани за тази операция. За да се създаде просто препращане на proxy, можете да използвате handler без никакви traps:

let target = {};

let proxy = new Proxy(target, {});

proxy.name = "proxy";
console.log(proxy.name);        // "proxy"
console.log(target.name);       // "proxy"

target.name = "target";
console.log(proxy.name);        // "target"
console.log(target.name);       // "target"
          
        

В този пример, proxy препраща всички операции директно на target. Когато "proxy" се възлага на proxy.name свойството, name се създава в target. Самия proxy не съхранява това свойство, това е просто препращане на операцията към target. Аналогично, стойностите на proxy.name и target.name са същите, защото те са две препратки към target.name. Това също означава, че задаването на нова стойност на target.name, кара proxy.name да отрази същата промяна. Разбира се, proxies без traps не са много интересни, така че какво се случва, когато се дефинира trap.

Валидиране на свойства с използване на set Trap

Да предположим, че искате да създадете обект, чийто стойности на свойства трябва да са номера. Това означава, че всяко ново свойство, добавено към обекта трябва да е валидирано и да бъде хвърлена грешка, ако стойността не е номер. За да постигнете това трябва да дефинирате set trap, който отменя поведението по подразбиране на настройката за създаване на стойност. Set trap получава четири аргумента:

  1. trapTarget - обекта, който ще получи свойството (proxy target).
  2. key - ключ за свойството (string или символ) за записване.
  3. value - стойност за записване на свойството.
  4. receiver - обекта, на който се извършва операцията (обикновено е proxy).

Съответстващия огледален trap метод на Reflect.set() е set, който е поведението по подразбиране за тази операция. Reflect.set() метода приема същите четири аргумента, като set proxy trap, което го прави лесен за използване във вътрешността на trap метода. Trap трябва да върне true, ако свойството е създадено или false ако не е (Reflect.set() метода връща правилната стойност на базата на успешна if операция).

За валидиране на стойностите на свойствата, можете да използвате set trap и да инспектирате value стойността, която се подава вътре. Ето един пример:

let target = {
    name: "target"
};

let proxy = new Proxy(target, {
        set(trapTarget, key, value, receiver) {

            // игнорира съществуващите свойства за да не ги засегне
            if (!trapTarget.hasOwnProperty(key)) {
                if (isNaN(value)) {
                    throw new TypeError("Property must be a number.");
                }
            }

            // добавяне на свойство
            return Reflect.set(trapTarget, key, value, receiver);
        }
    });

// добавяне на ново свойство
proxy.count = 1;
console.log(proxy.count);       // 1
console.log(target.count);      // 1

// може да зададете име, защото то съществува в целта вече
proxy.name = "proxy";
console.log(proxy.name);        // "proxy"
console.log(target.name);       // "proxy"

// хвърля грешка
proxy.anotherName = "proxy";
        

Този пример дефинира proxy trap, който валидира стойността на всяко ново свойство добавено към target. Когато proxy.count = 1 е изпълнено, се извиква set trap. Стойноста на trapTarget е равна на target, key е "count", value е 1 и receiver (не се използва в този пример) е proxy. Тъй като, не съществува свойство с име count в target, proxy валидира value с подаването и към isNaN. Ако резултата е NaN, тогава стойността на свойството не е номер и се хвърля грешка. Тъй като count е настроен на 1, proxy извиква Reflect.set() със същите четири аргумента, които са подадени на trap, за да добавите ново свойство.

Когато proxy.name се задава със string, операцията завършва успешно. Тъй като, target вече има name свойство, което е пропуснато от проверката за валидиране, чрез използване на trapTarget.hasOwnProperty(). Това гарантира, че по-рано съществуващите стойности, които не са цифрови свойства все още се поддържат.

Когато proxy.anotherName се задава със string, обаче, се хвърля грешка. Свойството anotherName не съществува в target, така че неговата стойност трябва да бъде валидирана. По време на проверката се хвърля грешка, защото "proxy" не е цифрова стойност.

Като се има в предвид, че set proxy trap ви позволява да се намесите, когато се пишат свойства, то get proxy trap ви позволява да се намесите, когато се четат свойства.

Валидиране на обектна форма с използване на get Trap

Един от най-интересните и понякога объркващи аспекти на JavaScript е, когато четенето на несъществуващи свойства не хвърля грешка. Вместо това се използва undefined за стойност на свойството. Например:

let target = {};

console.log(target.name);       // undefined
        

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

Една обектна форма е колекция от свойства и методи на разположение на обекта. JavaScript машината използва обектната форма за оптимизиране на кода и често създава класове за представяне на обектите. Ако може безопасно да се предположи, че един обект има същите свойства и методи, с които започва (поведението може се приложи с Object.preventExtensions(), Object.seal() или Object.freeze() методите), а след това хвърля грешка при опит за достъп до несъществуващи свойства, могат да бъдат полезни. Proxies правят валидирането на обектната форма лесно.

Тъй като, валидирането на свойство трябва да се направи, когато едно свойство е само за четене, трябва да използвате get trap. The get trap се извиква всеки път, когато едно свойство се чете, дори ако това свойство не съществува в обекта. Има три аргумента, които се подават към get trap:

  1. trapTarget - обект, от който се чете свойството (proxy target).
  2. key - ключ за свойството (string или символ) за четене.
  3. receiver - обекта, на който се извършва операцията (обикновено е proxy).

Тези аргументи са огледален образ на тези за set trap аргументите, с една забелжима разлика. Няма value аргумент, защото get trap не пишат стойности. Reflect.get() метода приема същите три аргумента, като get trap и връща стойност по подразбиране за свойството.

Можете да използвате get trap и Reflect.get() за хвърляне на грешка, ако свойството не съществува в целта:

let proxy = new Proxy({}, {
        get(trapTarget, key, receiver) {
            if (!(key in receiver)) {
                throw new TypeError("Property " + key + " doesn't exist.");
            }

            return Reflect.get(trapTarget, key, receiver);
        }
    });

// добавянето на свойство все още работи
proxy.name = "proxy";
console.log(proxy.name);            // "proxy"

// несъществуващи свойства хвърлят грешка
console.log(proxy.nme);             // хвърля грешка
          
        

В този пример, get trap прихваща операциите при четене на свойства. Оператора in се използва за да определи дали свойството вече съществува в receiver. Използваме receiver с in вместо trapTarget, в случай, че receiver е proxy, който има has trap, тип който ще покрием в следващия раздел. Използването на trapTarget ще заобиколи наличието на has trap и потенциално ще даде грешен резултат. Грешка се хвърля , ако свойството не съществува, в противен случай се използва поведението по подразбиране.

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

Скриване на наличието на свойства, използвайки has Trap

Оператора in определя дали съществува свойство в даден обект и връща true ако има съвпадение с името или символа на собствено или прототипно свойство. Например:

let target = {
    value: 42;
}

console.log("value" in target);     // true
console.log("toString" in target);  // true
          
        

И двата value и toString съществуват в обекта, така че и двата случая с in оператора връщат true. Свойството value е собствено, докато toString е прототипно свойство (наследено от Object). Proxies ви позволяват да прихванете тази операция и да върнете различна стойност за in с помощта на has trap.

The has trap се извиква всеки път, когато се използва оператора in. Когато се извиква има два аргумента подадени към has trap:

  1. trapTarget - обект, от който се чете свойството (proxy target).
  2. key - ключ за свойството (string или символ) за проверка.

Reflect.has() метода приема тези аргументи и връща отговор по подразбиране за in оператора. Използването на has trap и Reflect.has() ви позволяват да промените поведението на in за някои свовойста, докато съвпаднат с поведението по подразбиране за другите. Например, да предположим, че искате да скриете свойството value, можете да го направите по следния начин:

let target = {
    name: "target",
    value: 42
};

let proxy = new Proxy(target, {
    has(trapTarget, key) {

        if (key === "value") {
            return false;
        } else {
            return Reflect.has(trapTarget, key);
        }
    }
});


console.log("value" in proxy);      // false
console.log("name" in proxy);       // true
console.log("toString" in proxy);   // true
        

The has trap е за proxy проверките, за да се види дали key е "value", и ако е така връща false. В противен случай се използва поведение по подразбиране с Reflect.has() метода. Резултата е, че in оператора връща false за свойството value въпреки, че в действителност съществува в целта. Другите свойства name и toString, правилно връщат true, когато се използва in оператора.

Предотвратяване на заличаване на свойство с deleteProperty Trap

Оператора delete премахва свойство от един обект и при успех връща true, а при неуспех връща false. В strict mode, delete хвърля грешка, при опит за изтриване на nonconfigurable свойство, в nonstrict mode delete просто връща false. Ето един пример:

let target = {
    name: "target",
    value: 42
};

Object.defineProperty(target, "name", { configurable: false });

console.log("value" in target);     // true

let result1 = delete target.value;
console.log(result1);               // true

console.log("value" in target);     // false

// Забележка: Следващия ред хвърля грешка в strict mode
let result2 = delete target.name;
console.log(result2);               // false

console.log("name" in target);      // true
        

Свойството value се изтрива с помощта на оператора delete и като резултат оператора in връща false при третото извикване на console.log(). Nonconfigurable name свойството не може да бъде изтрито, така че оператора delete просто връща false (ако този код се изпълнява в strict mode, ще бъде хвърлена грешка). Можем да променим това поведение с помощта на deleteProperty trap в proxy.

The deleteProperty trap се извиква винаги, когато delete оператора се използва върху дадено обектно свойство. В trap се подават два аргумента:

  1. trapTarget - обект, от който трябва да се премахне свойството (proxy target).
  2. key - ключ за свойството (string или символ) за изтриване.

Reflect.deleteProperty() метода осигурява изпълнението по подразбиране на deleteProperty trap и приема същите два аргумента. Можете да комбинирате Reflect.deleteProperty() и deleteProperty trap, за да промените начина, по който delete оператора се държи. Например, можете да се гарантирате, че value свойството не може да бъде изтрито:

let target = {
    name: "target",
    value: 42
};

let proxy = new Proxy(target, {
    deleteProperty(trapTarget, key) {

        ìf (key === "value") {
            return false;
        } else {
            return Reflect.deleteProperty(trapTarget, key);
        }
    }
});

// опит за изтриване на proxy.value

console.log("value" in proxy);      // true

let result1 = delete proxy.value;
console.log(result1);               // false

console.log("value" in proxy);      // true

// опит за изтриване на proxy.name

console.log("name" in proxy);       // true

let result2 = delete proxy.name;
console.log(result2);               // true

console.log("name" in proxy);       // false
          
        

Този код е много подобен на has trap примера в това, че deleteProperty trap проверява за да види дали key е "value" и ако е така връща false. В противен случай се използва поведението по подразбиране, като се извиква Reflect.deleteProperty() метода. Свойството value не може да бъде изтрито чрез proxy, защото операцията е в trapped, но свойството name се премахва, както се очаква. Този подход е особено полезен, когато искате да защитите свойства от премахване, без да се предизвиква грешка в strict mode.

Prototype Proxy Traps

В Глава 4, учихме за ECMAScript 6 Object.setPrototypeOf() метода, който се добавя за да допълни ECMAScript 5 Object.getPrototypeOf() метода. Proxies позволяват прихващане на изпълнението на двата метода със setPrototypeOf и getPrototypeOf traps. И в двата случая метода на Object извиква trap върху съответното име на proxy, позволявайки ви да променяте поведението на методите. The setPrototypeOf trap получава тези аргументи:

  1. trapTarget - обекта, за който трябва да се създаде прототип (proxy target).
  2. proto - обекта, който трябва да се използва, като прототип.

Това са едни и същи аргументи подадени към Object.setPrototypeOf() и Reflect.setPrototypeOf(). The getPrototypeOf trap получава само trapTarget аргумент, който е аргумента подаден към Object.getPrototypeOf() и Reflect.setPrototypeOf() методите.

Как работи Prototype Proxy Traps

Има някои ограничения за тези traps. Първо, getPrototypeOf trap трябва да върне обект или null, както и всички други резултати, връщащи грешка по време на работа. Проверката за връщане на стойност гарантира, че Object.getPrototypeOf() винаги ще върне очакваната стойност. По същия начин, върнатата стойност от setPrototypeOf trap трябва да бъде false ако операцията не успее. Когато setPrototypeOf връща false, Object.setPrototypeOf() хвърля грешка. Ако setPrototypeOf връща някаква стойност различна от false, тогава Object.setPrototypeOf() приема, че операцията е успяла.

Следващия пример скрива прототипа на proxy, като винаги връща null и също така не позволява на прототипа да бъде променен:

let target = {};
let proxy = new Proxy(target, {
    getPrototypeOf(trapTarget) {
        return null;
    },
    setPrototypeOf(trapTarget, proto) {
        return false;
    }
});

let targetProto = Object.getPrototypeOf(target);
let proxyProto = Object.getPrototypeOf(proxy);

console.log(targetProto === Object.prototype);      // true
console.log(proxyProto === Object.prototype);       // false
console.log(proxyProto);                            // null

// succeeds (успех)
Object.setPrototypeOf(target, {});

// throws error
Object.setPrototypeOf(proxy, {});
        

Този код подчертава разликата между поведението на target и proxy. Докато Object.getPrototypeOf() връща стойност за target, той връща null за proxy, защото getPrototypeOf trap е извикан. По същия начин,Object.setPrototypeOf() успява, когато се използва за target, но хвърля грешка, когато се използва за proxy в setPrototypeOf trap.

Ако искате да използвате поведение по подразбиране за тези два traps, можете да използвате съответните методи на Reflect. Например, този код изпълнява поведение по подразбиране за getPrototypeOf и setPrototypeOf traps:

let target = {};
let proxy = new Proxy(target, {
    getPrototypeOf(trapTarget) {
        return Reflect.getPrototypeOf(trapTarget);
    },
    setPrototypeOf(trapTarget, proto) {
        return Reflect.setPrototypeOf(trapTarget, proto);
    }
});

let targetProto = Object.getPrototypeOf(target);
let proxyProto = Object.getPrototypeOf(proxy);

console.log(targetProto === Object.prototype);      // true
console.log(proxyProto === Object.prototype);       // true

// успява
Object.setPrototypeOf(target, {});

// също успява
Object.setPrototypeOf(proxy, {});
            
          

В този пример можете да използвате взаимозаменяеми target и proxy и да получите същия резултат, защото на getPrototypeOf и setPrototypeOf traps им се подава да използват изпълнението по подразбиране. Важното в този пример е, че използвате Reflect.getPrototypeOf() и Reflect.setPrototypeOf() методите, вместо методите с едно и също име за Object поради някои важни разлики.

Защо два комплекта методи?

Най объркващия аспект на Reflect.getPrototypeOf() и Reflect.setPrototypeOf() е, че изглеждат подозрително подобни на Object.getPrototypeOf() и Object.setPrototypeOf(). Докато двете групи методи извършват подобни операции, има някои видими разлики между двете.

Да започнем с, Object.getPrototypeOf() и Object.setPrototypeOf(), които са операции от по-високо ниво, което означава, че са създадени за използване от програмистите още от самото начало. Reflect.getPrototypeOf() и Reflect.setPrototypeOf() са операции от по-ниско ниво, като дават достъп на програмистите само за вътрешно ползване на [[GetPrototypeOf]] и [[SetPrototypeOf]] операциите. Метода Reflect.getPrototypeOf() е обвивка на вътрешната [[GetPrototypeOf]] операция (с някакъв вход за валидиране). Метода Reflect.setPrototypeOf() и [[SetPrototypeOf]] имат същата връзка. Съответните методи за Object също извикват [[GetPrototypeOf]] и [[SetPrototypeOf]], но изпълняват няколко стъпки, преди повикването и инспектират върнатата стойност, за да се определи как да се държат.

Reflect.getPrototypeOf() метода хвърля грешка, ако аргумента му не е обект, докато Object.getPrototypeOf() първо коригира стойността в обект, преди извършване на операцията. Ако ви се наложи да подадете номер във всеки метод, ще получите различен резултат:

let result1 = Object.getPrototypeOf(1);
console.log(result1 === Number.prototype);  // true

// throws an error
Reflect.getPrototypeOf(1);
          

Object.getPrototypeOf() метода ви позволява да извлечете прототип за цифрата 1, защото първо коригира стойността в Number обект и след това връща Number.prototype. Reflect.getPrototypeOf() метода не коригира стойността и тъй като 1 не е обект, той хвърля грешка.

Reflect.setPrototypeOf() метода също има няколко разлики с Object.setPrototypeOf() метода. Първо, Reflect.setPrototypeOf() връща булева стойност, показваща дали операцията е успешна (true за успех, false за провал). Ако Object.setPrototypeOf() не успее, той хвърля грешка.

Както видяхме по-рано, когато setPrototypeOf proxy trap върне false, той предизвиква Object.setPrototypeOf() да хвърли грешка. Object.setPrototypeOf() метода връща първия аргумент, като негова стойност и следователно не е подходящ за прилагане на поведение по подразбиране на setPrototypeOf proxy trap. Следният код демонстрира тези разлики:

let target1 = {};
let result1 = Object.setPrototypeOf(target1, {});
console.log(result1 === target1);                   // true

let target2 = {};
let result2 = Reflect.setPrototypeOf(target2, {});
console.log(result2 === target2);                   // false
console.log(result2);                               // true
          

В този пример, Object.setPrototypeOf() връща target1, като негова стойност, но Reflect.setPrototypeOf() връща true. Тази малка разлика е много важна. Вие ще видите още привидно дублиращи се методи на Object и Reflect, но никога не забравяйте да използвате Reflect метода вътре proxy traps.

info
И двете групи методи ще наричаме getPrototypeOf и setPrototypeOf proxy traps когато ги използваме за proxy.

Object Extensibility Traps

ECMAScript 5 добавя object extensibility модификация чрез Object.preventExtensions() и Object.isExtensible() методите и ECMAScript 6 позволява proxies да прихваща тези извиквания на методи към основните обекти чрез preventExtensions и isExtensible traps. И двата traps получават един аргумент trapTarget, който е обекта, върху който е извикан метода. The isExtensible trap трябва да върне булева стойност, показваща дали обекта е extensible, докато preventExtensions trap трабва да върне булева стойност, показваща дали операцията е успяла.

Има също и Reflect.preventExtensions() и Reflect.isExtensible() методи за прилагане на поведение по подразбиране. И двата връщат булеви стойности, така че могат да се използват директно в съответните им traps.

Два основни примера

За да видите object extensibility traps в действие, да разгледаме следния код, който реализира поведението по подразбиране за isExtensible и preventExtensions traps:

let target = {};
let proxy = new Proxy(target, {
    isExtensible(trapTarget) {
        return Reflect.isExtensible(trapTarget);
    },
    preventExtensions(trapTarget) {
        return Reflect.preventExtensions(trapTarget);
    }
});


console.log(Object.isExtensible(target));       // true
console.log(Object.isExtensible(proxy));        // true

Object.preventExtensions(proxy);

console.log(Object.isExtensible(target));       // false
console.log(Object.isExtensible(proxy));        // false
          

Този пример показва, че и двата Object.preventExtensions() и Object.isExtensible() правилно преминават от proxy към target. Можете разбира се, също да промените поведението. Например, ако не искате да позволите на Object.preventExtensions() да успее на вашето proxy, можете да върнете false от preventExtensions trap:

let target = {};
let proxy = new Proxy(target, {
    isExtensible(trapTarget) {
        return Reflect.isExtensible(trapTarget);
    },
    preventExtensions(trapTarget) {
        return false; 
    }
});


console.log(Object.isExtensible(target));       // true
console.log(Object.isExtensible(proxy));        // true

Object.preventExtensions(proxy);

console.log(Object.isExtensible(target));       // true
console.log(Object.isExtensible(proxy));        // true
          

Тука, извикването към Object.preventExtensions(proxy) се игнорира ефективно, защото preventExtensions trap връща false. Операцията не се препраща към основния target, така че Object.isExtensible() връща true.

Duplicate Extensibility Methods

Може би, за пореден път забелязвате, че има привидно дублиращи се методи за Object и Reflect. В този случай, те са по-близки, от колкото не са. Методите Object.isExtensible() и Reflect.isExtensible() са сходни с изключение на, когато подадената стойност не е обект. В този случай, Object.isExtensible() винаги връща false, докато Reflect.isExtensible() хвърля грешка. Ето един пример затова поведение:

let result1 = Object.isExtensible(2);
console.log(result1);                       // false

// throws error
let result2 = Reflect.isExtensible(2);
          

Това ограничение е подобно на разликата между Object.getPrototypeOf() и Reflect.getPrototypeOf(), тъй като функционалността на по-ниско ниво има по-строги проверки за грешки, от колкото на по-високо ниво.

Методите Object.preventExtensions() и Reflect.preventExtensions() също са много сходни. Метода Object.preventExtensions() винаги връща стойността, която и е подадена като аргумент, дори ако не е обект. Метода Reflect.preventExtensions() от друга страна, хвърля грешка, ако аргумента не е обект; ако аргумента е обект Reflect.preventExtensions() връща true, когато операцията е успешна или false ако не е. Например:

let result1 = Object.preventExtensions(2);
console.log(result1);                               // 2

let target = {};
let result2 = Reflect.preventExtensions(target);
console.log(result2);                               // true

// throws error
let result3 = Reflect.preventExtensions(2);
          

Тука, на Object.preventExtensions() е подадена стойност 2, като той връща тази стойност въпреки, че не е обект. Метода Reflect.preventExtensions() връща true, когато и е подаден обект и хвърля грешка, когато и е подадено 2.

Property Descriptor Traps

Една от най-важните характеристики на ECMAScript 5 е възможността да се определят свойства-атрибути използвайки Object.defineProperty() метода. В предишните версии на JavaScript нямаше начин да се определи свойство за достъп, да се направи свойство само за четене или да се направи свойство nonenumerable. Всички те са възможни с използване на Object.defineProperty() метода и можете да изтеглите тези атрибути с използване на Object.getOwnPropertyDescriptor() метода.

Proxies позволяват да се прихващат извикванията към Object.defineProperty() и Object.getOwnPropertyDescriptor() с помощта на defineProperty и getOwnPropertyDescriptor traps, съответно. The defineProperty trap получава следните аргументи:

  1. trapTarget - обекта, на който следва да бъде определено свойството (proxy target).
  2. key- string или символ за свойството.
  3. descriptor - descriptor обект за свойството

The defineProperty trap изисква от вас да върне true, ако операцията е успяла и false ако не е. The getOwnPropertyDescriptor traps получава само trapTarget и key и се очаква да върне descriptor. Съответно Reflect.defineProperty() и Reflect.getOwnPropertyDescriptor() методите приемат същите аргументи, каквито приема proxy trap. Ето един пример, който просто изпълнява поведение по подразбиране за всеки trap:

let proxy = new Proxy({}, {
    defineProperty(trapTarget, key, descriptor) {
        return Reflect.defineProperty(trapTarget, key, descriptor);
    },
    getOwnPropertyDescriptor(trapTarget, key) {
        return Reflect.getOwnPropertyDescriptor(trapTarget, key);
    }
});


Object.defineProperty(proxy, "name", {
    value: "proxy"
});

console.log(proxy.name);            // "proxy"

let descriptor = Object.getOwnPropertyDescriptor(proxy, "name");

console.log(descriptor.value);      // "proxy"
          

Този код дефинира свойството "name" на proxy използвайки Object.defineProperty(). Свойството descriptor затова свойство се извлича с помощта на Object.getOwnPropertyDescriptor() метода.

Блокирване на Object.defineProperty()

The defineProperty trap изисква от вас да върне булева стойност за да покаже, че операцията е била успешна. Когато се връща true, Object.defineProperty() успява, както обикновено; когато се връща false, Object.defineProperty() хвърля грешка. Можете да използвате тази функция, за да ограничите вида на свойството, което може да бъде определено с помощта на Object.defineProperty() метода. Например, ако искате да предотвратите да бъдат определени символни свойства, бихте могли да проверите дали ключа е string и да върне false ако не е. Например:

let proxy = new Proxy({}, {
    defineProperty(trapTarget, key, descriptor) {

        if (typeof key !== "string") {
            return false;
        }

        return Reflect.defineProperty(trapTarget, key, descriptor);
    }
});


Object.defineProperty(proxy, "name", {
    value: "proxy"
});

console.log(proxy.name);                    // "proxy"

let nameSymbol = Symbol("name");

// throws error
Object.defineProperty(proxy, nameSymbol , {
    value: "proxy"
});
          

В този код, defineProperty proxy trap връща false, когато key е символ, в противен случай продължава с поведението по подразбиране. Когато Object.defineProperty() се извика с ключ "name", той успява защото ключът е string. Когато Object.defineProperty() се извика с nameSymbol, хвърля грешка, защото definePropert trap връща false.

info
Може също така, Object.defineProperty() тихо да се провали, като върне true и да не извика Reflect.defineProperty(). Това ще подтисне грешката, докато всъщност не определя свойство.

Descriptor Object Restrictions

За да осигури последователно поведение при използване на Object.defineProperty() и Object.getOwnPropertyDescriptor() методите, descriptor обектите подадени към defineProperty trap се нормализират. Обектите върнати от getOwnPropertyDescriptor() trap винаги сe валидират по същата причина.

Без значение е какъв обект се подава, като трети аргумент на Object.defineProperty() метода, само свойствата enumerable, configurable, value, writable, get и set ще бъдат в обекта на descriptor подаден към defineProperty trap. Например:

let proxy = new Proxy({}, {
    defineProperty(trapTarget, key, descriptor) {
        console.log(descriptor.value);              // "proxy"
        console.log(descriptor.name);               // undefined

        return Reflect.defineProperty(trapTarget, key, descriptor);
    }
})


Object.defineProperty(proxy, "name", {
    value: "proxy",
    name: "custom"
});
          

Тука, Object.defineProperty() се извиква с нестандартното свойство name, като трети аргумент. Когато defineProperty trap се извика, descriptor обекта не разполага със свойство name, но има свойство value. Това е така, защото descriptor не е препратка към действителния трети аргумент на Object.defineProperty(), а по-скоро е нов обект, който съдържа само допустимите свойства. Reflect.defineProperty() метода, също така пренебрегва всякакви нестандартни свойства на descriptor.

The getOwnPropertyDescriptor trap има малко по-различно ограничение, което изисква върнатата стойност да бъде null, undefined или object. Ако се връща обект, само enumerable, configurable, value, writable, get и set са позволени, като собствени свойства на обекта. Грешка се хвърля, ако се върне обект със собствено свойство, което не е разрешено, например:

let proxy = new Proxy({}, {
    getOwnPropertyDescriptor(trapTarget, key) {
        return {
            name: "proxy";
        };
    }
});

// throws error
let descriptor = Object.getOwnPropertyDescriptor(proxy, "name");
            
          

Свойството name не е допустимо, като свойство на descriptors, така че, когато Object.getOwnPropertyDescriptor() се извика, getOwnPropertyDescriptor връща стойност, която предизвиква грешка. Това ограничение гарантира, че стойността върната от Object.getOwnPropertyDescriptor() винаги има надеждна структура, независимо от използването на proxies.

Дублиране на Descriptor методи

За пореден път, ECMAScript 6 има някои объркващо подобни методи, като Object.defineProperty() и Object.getOwnPropertyDescriptor(), които изглежда, че правят същото нещо, като Reflect.defineProperty() и Reflect.getOwnPropertyDescriptor() методите, съответно. Както и при другите двойки методи, обсъдени по-рано в тази глава, има някои фини но важни разлики.

Методите defineProperty ()

Object.defineProperty() и Reflect.defineProperty() методите са абсолютно същите с изключение на стойностите им за връщане. Object.defineProperty() метода връща първия аргумент, докато Reflect.defineProperty() връща true ако операцията е успешна и false ако не е. Например:

let target = {};

let result1 = Object.defineProperty(target, "name", { value: "target "});

console.log(target === result1);        // true

let result2 = Reflect.defineProperty(target, "name", { value: "reflect" });

console.log(result2);                   // true
          

Когато Object.defineProperty() се извика върху target, върнатата стойност е target. Когато Reflect.defineProperty() се извика върху target, върнатата стойност е true, което показва, че операцията е успяла. Тъй като, defineProperty proxy trap изисква да бъде върната булевата стойност, е по-добре да се използва Reflect.defineProperty() за да изпълни поведението по подразбиране, когато е необходимо.

Методите getOwnPropertyDescriptor ()

Object.getOwnPropertyDescriptor() метода коригира своя първи аргумент в обект, когато се подава примитивна стойност и след това продължава операцията. От друга страна Reflect.getOwnPropertyDescriptor() хвърля грешка ако първият аргумент е примитивна стойност. Ето един пример:

let descriptor1 = Object.getOwnPropertyDescriptor(2, "name");
console.log(descriptor1);       // undefined

// throws an error
let descriptor2 = Reflect.getOwnPropertyDescriptor(2, "name");
          

Object.getOwnPropertyDescriptor() метода връща undefined, защото коригира 2 в обект и този обект не разполага с name свойство. Това е стандартно поведение на метода, когато свойство с дадено име не е намерено в даден обект. Обаче, когато Reflect.getOwnPropertyDescriptor() се извика, незабавно се хвърля грешка, защото този метод не приема примитивни стойности за първи аргумент.

The ownKeys Trap

The ownKeys proxy trap прихваща вътрешния метод [[OwnPropertyKeys]] и позволява да се замени това поведение, като връща масив от стойности. Този масив се използва в четири метода: Object.keys(), Object.getOwnPropertyNames(), Object.getOwnPropertySymbols() и Object.assign() (Object.assign() метода използва масива за да определи, кои свойства са за копиране).

Поведението по подразбиране за Object.keys() се прилага от Reflect.ownKeys() метода и връща масив с всички собствени ключове, включително strings и symbols. Object.getOwnProperyNames() и Object.keys(), филтрират символи от масива и връщат резултата, докато Object.getOwnPropertySymbols() филтрира strings от масива и връща резултата. Object.assign() метода използва масива, както със strings, така и със symbols.

The ownKeys trap получава един аргумент - target и винаги трябва да върне масив или масиво-подобен обект (в противен случай хвърля грешка). С използването на ownKeys trap можете, например, да филтрирате определени ключови свойства, които не искате да се използват от Object.keys(), Object.getOwnPropertyNames(), Object.getOwnPropertySymbols() или Object.assign(). Да предположим, че вие не искате да се включат всички имена на свойства, които започват с долна черта на характера, което е обща нотация за означаване в JavaScript, показваща, че полето е частно. Можете да използвате ownKeys trap да филтрира тези ключове:

let proxy = new Proxy({}, {
    ownKeys(trapTarget) {
        return Reflect.ownKeys(trapTarget).filter(key => {
            return typeof key !== "string" || key[0] !== "_";
        });
    }
});

letnameSymbol = Symbol("name");

proxy.name = "proxy";
proxy._name = "private";
proxy[nameSymbol] = "symbol";

let names = Object.getOwnPropertyNames(proxy),
    symbols = Object.getOwnPropertySymbols(proxy);

console.log(names.length);      // 1
console.log(names[0]);          // "proxy"

console.log(symbols.length);    // 1
console.log(symbols[0]);        // "Symbol(name)"
          

Този пример използва ownKeys trap, който първо извиква Reflect.ownKeys() за да получи списък по подразбиране на ключовете на целта. После, filter() метода се използва за филтриране на ключовете, които са strings и започват с долна черта. След това, се добавят три свойства на proxy обекта: name, _name и nameSymbol. Когато Object.getOwnPropertyNames() и Object.keys() се извикат върху proxy, само name свойството се връща. По същия начин, само nameSymbol се връща, когато Object.getOwnPropertySymbols() се извика върху proxy. Свойството _name не присъства в нито един резултат, тъй като е било филтрирано.

Докато ownKeys proxy trap позволява да променяме върнатите ключовете от малък набор от операции, това не засяга по-често използваните операции, като for-of цикли или Object.keys() метода. Тези, които не могат да се променят с помощта на proxy.

info
ownKeys trap също се отразява на for-in цикъла, като извиква trap за да определи кои ключове да използва вътре в цикъла.

Функция Proxies с apply и construct traps

От всички proxy traps, само apply и construct изискват proxy target да бъде функция. Както научихме в Глава 3 функциите имат два вътрешни метода, [[Call]] и [[Construct]], които се изпълняват, когато една функция се извика без или със оператора new. The apply и construct traps съответстват на тези вътрешни методи и ни позволяват да ги заменим. Kогато дадена функция се извиква без new, apply trap получава, а Reflect.apply() очаква следните аргументи:

  1. trapTarget - функцията се изпълнява (proxy target).
  2. thisArg - стойността на this вътре във функцията по време на извикване.
  3. argumentsList - масив от аргументи подадени към функцията.

The construct trap, който се извиква, когато функцията се изпълнява с помощта на new, получава следните аргументи:

  1. trapTarget - функцията се изпълнява (proxy target).
  2. argumentsList - масив от аргументи подадени към функцията.

Reflect.construct() метода приема тези два аргумента и има трети не задължителен аргумент, newTarget. Когато се дава, аргумента newTarget уточнява стойността на new.target вътре във функцията.

Взети заедно, apply и construct traps напълно контролират поведението на всяка proxy target функция. За да имитирате поведение по подразбиране на функцията, можете да направите следното:

let target = function() { return 42 },
    proxy = new Proxy(target, {
        apply: function(trapTarget, thisArg, argumentList) {
            return Reflect.apply(trapTarget, thisArg, argumentList);
        },
        construct: function(trapTarget, argumentList) {
            return Reflect.construct(trapTarget, argumentList);
        }
    });

// a proxy with a function as its target looks like a function
console.log(typeof proxy);                  // "function"

console.log(proxy());                       // 42

var instance = new proxy();
console.log(instance instanceof proxy);     // true
console.log(instance instanceof target);    // true
          

Този пример има функция, която връща номера 42. The proxy за тази функция използва apply и construct traps да делегират това поведение на Reflect.apply() и Reflect.construct() методите, съответно. Крайният резултат е, че proxy функцията работи, точно като target функция, включително и идентифицираща се, като функция при използване на typeof. The proxy се извиква без new за да върне 42 и после се извиква с new за да създаде обект, наречен instance. Обекта instance се счита за инстанция на proxy и target едновременно, защото instanceof използва прототипната верига за да определи тази информация. Тъй като прототипната верига не се влияе от това proxy, то proxy и target изглежда имат същия прототип на JavaScript машината.

Валидиране на параметрите на функция

The apply и construct traps отварят много възможности за промяна на начина, по който функцията се изпълнява. Например, да предположим, че искате да потвърдите, че всички аргументи са от определен тип. Можете да проверите аргументите в apply trap:

// добавя заедно всички аргументи
function sum(...values) {
    return values.reduce((previous, current) => previous + current, 0);
}

let sumProxy = new Proxy(sum, {
        apply: function(trapTarget, thisArg, argumentList) {

            argumentList.forEach((arg) => {
                 if ( typeof arg !== "number") {
                     throw new TypeError("All arguments must be numbers.");
                }
            });

             return Reflect.apply(trapTarget, thisArg, argumentList);
        },
        construct: function(trapTarget, argumentList) {
            throw new TypeError("This function can't be called with new.");
        }
    });

console.log(sumProxy(1, 2, 3, 4));          // 10

// хвърля грешка
console.log(sumProxy(1, "2", 3, 4));

// също хвърля грешка
let result = new sumProxy();
          

Този пример използва apply trap за да гарантира, че всички аргументи са числа. Функцията sum() добавя всички аргументи, които са и подадени. Ако и се подаде не-номер стойност, функцията все пак ще изпълни операция, която може да доведе до неочаквани резултати. Чрез увиването на sum() в proxy извиквайки sumProxy(), този код прихваща извикванията на функции и гарантира, че всеки аргумент е номер, преди да позволи повикването да продължи. За да е сигурно, кода също използва construct trap за да гарантира, че функциите не могат да бъдат извикани с new.

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

function Numbers(...values) {
    this.values = values;
}

let NumbersProxy = new Proxy(Numbers, {

        apply: function(trapTarget, thisArg, argumentList) {
            throw new TypeError("This function must be called with new.");
        },

        construct: function(trapTarget, argumentList) {
            argumentList.forEach((arg) => {
                if (typeof arg !== "number") {
                    throw new TypeError("All arguments must be numbers.");
                }
            });

            return Reflect.construct(trapTarget, argumentList);
        }
    });

let instance = new NumbersProxy(1, 2, 3, 4);
console.log(instance.values);               // [1,2,3,4]

// throws error
NumbersProxy(1, 2, 3, 4);
            
          

Тука, apply trap хвърля грешка, докато construct trap използва Reflect.construct() метода за валидиране на входа и връща нова инстанция. Разбира се можете да постигнете същото нещо без proxies, ако използвате new.target.

Извикване на конструктора без new

В Глава 3 въведохме new.target metaproperty. Да преговорим, new.target е препратка към функция, която се извиква с new, което означава, че можете да разберете дали дадена функция се извиква с използване на new или не, чрез проверка на стойността на new.target, подобно на това:

function Numbers(...values) {

    if (typeof new.target === "undefined") {
        throw new TypeError("This function must be called with new.");
    }

    this.values = values;
}

let instance = new Numbers(1, 2, 3, 4);
console.log(instance.values);               // [1,2,3,4]

// throws error
Numbers(1, 2, 3, 4);
            
          

Този пример хвърля грешка, когато Numbers се извиква без new, което е подобно на примера в секцията "Валидиране на параметрите на функция", но без използване на proxy. Писане на код, като този е много по-лесно, от колкото с използване на proxy и е за предпочитане, ако единствената ви цел е да предотвратите извикване на функция без new. Обаче, понякога нямате контрол над функцията, чието поведение трябва да се измени. В този случай използването на proxy има смисъл.

Да предположим, че функцията Numbers дефинирана в кода не може да се променя. Вие знаете, че кода разчита на new.target и искате да избегнете тази проверка, като все още можете да извиквате функцията. Поведението при използване на new е вече дефинирано, така че просто можете да използвате apply trap:

function Numbers(...values) {

    if (typeof new.target === "undefined") {
        throw new TypeError("This function must be called with new.");
    }

    this.values = values;
}


let NumbersProxy = new Proxy(Numbers, {
        apply: function(trapTarget, thisArg, argumentsList) {
            return Reflect.construct(trapTarget, argumentsList);
        }
    });


let instance = NumbersProxy(1, 2, 3, 4);
console.log(instance.values);               // [1,2,3,4]
            
          

Функцията NumbersProxy ви позволява да извикате Numbers без да използвате new и тя да се държи така, все едно, че е използван new. За да направите това, apply trap извиква Reflect.construct() с аргументите подадени в apply. Правейки това означава, че new.target вътрешно в Numbers е равно на Numbers и следователно няма хвърли грешка. Въпреки, че това е един прост пример за модифициране на new.target, можете да го направите по-директен.

Overriding Abstract Base Class Constructors

Можете да отидете една стъпка напред и да посочите трети аргумент на Reflect.construct(), като специфична стойност за присвояване от new.target. Това е полезно, когато една функция проверява new.target срещу известна стойност, също като при създаването на абстрактен базов клас конструктор (обсъден в Глава 9). В абстрактния базов клас конструктор, new.target се очаква да бъде нещо различно от себе си в конструктора на класа, като в този пример:

class AbstractNumbers {

    constructor(...values) {
        if (new.target === AbstractNumbers) {
            throw new TypeError("This function must be inherited from.");
        }

        this.values = values;
    }
}

class Numbers extends AbstractNumbers {}

let instance = new Numbers(1, 2, 3, 4);
console.log(instance.values);           // [1,2,3,4]

// throws error
new AbstractNumbers(1, 2, 3, 4);
            
          

Когато new AbstractNumbers() се извика, new.target е равен на AbstractNumbers и се хвърля грешка. Извикването на new Numbers() все още работи, защото new.targetis е равен на Numbers. Можете да заобиколите това ограничение, чрез ръчно задаване на new.target с proxy:

class AbstractNumbers {

    constructor(...values) {
        if (new.target === AbstractNumbers) {
            throw new TypeError("This function must be inherited from.");
        }

        this.values = values;
    }
}

let AbstractNumbersProxy = new Proxy(AbstractNumbers, {
        construct: function(trapTarget, argumentList) {
            return Reflect.construct(trapTarget, argumentList, function() {});
        }
    });


let instance = new AbstractNumbersProxy(1, 2, 3, 4);
console.log(instance.values);               // [1,2,3,4]
          

AbstractNumbersProxy използва construct trap за да прекъсне извикването към new AbstractNumbersProxy() метода. След това, Reflect.construct() метода се извиква с аргументи от trap и добавя празна функция, като аргумент. Тази празна функция се използва, като стойност на new.target във вътрешността на конструктора. Понеже new.target не е равен на AbstractNumbers, няма хвърлена грешка и конструктора се изпълнява напълно.

Изискуем клас конструктор

В Глава 9 научихме, че клас конструктора винаги трябва да се извиква с new. Това се случва, защото вътрешния [[Call]] метод за клас конструктори е настроен да хвърли грешка. Но proxies могат да прихващат извикванията към [[Call]], което означава, можете да създадете ефективно изискуеми клас конструктори с помощта на proxy. Например, ако искате клас конструктора да работи без да използва new, можете да използвате apply trap да създаде нова инстанция. Ето един примерен код:

class Person {
    constructor(name) {
        this.name = name;
    }
}

let PersonProxy = new Proxy(Person, {
        apply: function(trapTarget, thisArg, argumentList) {
            return new trapTarget(...argumentList);
        }
    });


let me = PersonProxy("Nicholas");
console.log(me.name);                   // "Nicholas"
console.log(me instanceof Person);      // true
console.log(me instanceof PersonProxy); // true
            
          

Обекта PersonProxy е proxy на Person клас конструктора. Клас конструкторите са само функции, така че се държат като функции, когато се използват в proxies. The apply trap отменя поведението по подразбиране и вместо това връща нова инстанция на trapTarget, която е равна на Person (Аз използвах trapTarget за да покажа, че не е нужно ръчно да определяте класа). The argumentList се подава към trapTarget с помощта на оператора spread, за да подаде всеки аргумент по отделно. Извикването на PersonProxy() без използване на new връща инстанция на Person. Ако се опитаме да извикаме Person() без new, конструктора все още ще хвърли грешка. Създаване на изискуеми клас конструктори е нещо, което е възможно само с помощта на proxies.

Отмяна на Proxies

Обикновено, proxy не може да бъде разкачен от target, след като proxy вече е бил създаден. Всички примери до този момент в тази глава използват неотменими proxies. Въпреки това, може да има ситуации, когато да искаме да revoke (отменим) proxy, така че той вече да не може да се използва. Ще откриете, че най-полезно е да се отмени proxy, когато искате да се осигури обект чрез API за целите на сигурността и да поддържа възможността да се отреже достъпа до някои функции във някаква точка от времето.

Отмяна на proxies се създават с помощта на метода Proxy.revocable(), който има същите аргументи, както Proxy constructor - target object и proxy handler. Върнатата стойност е обект със следните свойства:

  1. proxy - обекта proxy, който да бъде отменен
  2. revoke - функция извикваща revoke на proxy

Когато функцията revoke() се извика, няма по-нататъшни операции, които могат да се извършат с proxy. Всеки опит за взаимодействие с proxy object ще предизвика proxy trap да хвърли грешка. Например:

let target = {
    name: "target"
};

let { proxy, revoke } = Proxy.revocable(target, {});

console.log(proxy.name);        // "target"

revoke();

// throws error
console.log(proxy.name);
          

Този пример създава отмяна на proxy. Той използва destructuring за да присвои proxy и revoke променливите към свойства със същото име на обекта върнат от Proxy.revocable() метода. След това, proxy обекта, може да се използва само, като неотменим proxy обект, така proxy.name връща "target", защото минава през target.name. След като, се извика функцията revoke(), обаче, proxy вече не функционира. Опита за достъп до proxy.name хвърля грешка, както и за всяка друга операция, която разчита на proxy traps.

Решаване на Array проблема

В началото на тази глава, аз обясних как програмистите не могат да имитират поведението на масив в JavaScript преди ECMAScript 6. Proxies и reflection API ви позволяват да създадете обект, който се държи по същия начин, както вградения Array, когато се добавят и отстраняват свойства. За да опресните паметта си, ето един пример, показващ поведението, което proxies помагат да имитирате:

let colors = ["red", "green", "blue"];

console.log(colors.length);        // 3

colors[3] = "black";

console.log(colors.length);        // 4
console.log(colors[3]);            // "black"

colors.length = 2;

console.log(colors.length);         // 2
console.log(colors[3]);             // undefined
console.log(colors[2]);             // undefined
console.log(colors[1]);             // "green"
          

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

  1. Свойството length се увеличава на 4, когато на colors[3] се задава стойност.
  2. Последните две позиции в масива се изтриват, когато length свойството се определи на 2.

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

Откриване на индексите на масиви

Имайте предвид, че определяне на цяло число, като свойство за ключове е специален случай за масиви, като те се третират по различен начин от ключовете не цяло число. Спецификацията на ECMAScript 6 дава инструкции затова, как да се определи дали свойството за ключове е индекс на масив:

String свойството P е индекс на масив, ако и само ако ToString(ToUint32(P)) е равно на P и ToUint32(P) не е равно на 232-1.

Тази операция може да се приложи в JavaScript, както следва:

function toUint32(value) {
  return Math.floor(Math.abs(Number(value))) % Math.pow(2, 32);
}

  function isArrayIndex(key) {
  let numericKey = toUint32(key);
  return String(numericKey) == key && numericKey < (Math..pow(2, 32) - 1);
}  
          

Функцията toUint32() превръща дадена стойност в положително 32-битово цяло число, като се използва алгоритъма описан в спецификацията. Функцията isArrayIndex() първо преобразува ключа в uint32 и след това изпълнява сравненията за да определи, дали ключът е индекс на масива или не. С предоставянето на тези полезни функции, можете да започнете да прилагате един обект, който ще имитира вграден масив.

Увеличаване на дължината при добавяне на нови елементи

Може би сте забелязали, че двете поведения на масива, които описах разчитат на прехвърлянето на свойство. Това означава, че наистина само трябва да използвате set proxy trap да постигнете и двете поведения. За да започнете, ето един пример, който реализира първото от двете поведения чрез увеличаване на length свойството, когато се използва индекс на масив по-голям от length - 1:

function toUint32(value) {
  return Math.floor(Math.abs(Number(value))) % Math.pow(2, 32);
}

  function isArrayIndex(key) {
  let numericKey = toUint32(key);
  return String(numericKey) == key && numericKey < (Math..pow(2, 32) - 1);
}

function createMyArray(length=0) {
  return new Proxy({ length }, {
    set(trapTarget, key, value) {

      let currentLength = Reflect.get(trapTarget, "length");

      // специален случай
      if (isArrayIndex(key)) {
          let numericKey = Number(key);

          if (numericKey >= currentLength) {
              Reflect.set(trapTarget, "length", numericKey + 1);
          }
      }

      // винаги прави това независимо от типа на ключа
      return Reflect.set(trapTarget, key, value);
    }
  });
}

let colors = createMyArray(3);
console.log(colors.length);         // 3

colors[0] = "red";
colors[1] = "green";
colors[2] = "blue";

console.log(colors.length);         // 3

colors[3] = "black";

console.log(colors.length);         // 4
console.log(colors[3]);             // "black" 
          

Този пример използва set proxy trap за да се намеси при определянето на индекса на масив. Ако ключът е индекс на масив, то тогава се преобразува в число, защото ключовете винаги се подават, като string. След това, ако тази числова стойност е по-голяма или равна на текущото length свойството, тогава length свойството се актуализира, да бъде с едно повече от цифровия ключ (създаване на обект с позиция 3 означава, че length трябва да бъде 4). След това, поведението по подразбиране за създаване на свойство се използва направо в Reflect.set(), тъй като искате свойството да получи стойността, както е посочена.

Първоначално потребителския масив се създава с извикването на createMyArray() с дължина 3 и стойностите на тези три елемента се добавят веднага след това. Свойството length правилно остава 3, докато стойността "black" се присвои към позиция 3. В този момент length става на 4.

След работата по първото поведение, е време да преминем към второто.

Изтриване на елементи за намаляване на дължината

Първото имитирано поведение на масива се използва, само когато индекса на масива е по-голям или равен на length свойството. Второто поведение прави обратното и премахва елементи от масива, когато length свойството се настрои на по-малка стойност, от колкото преди това се съдържа. Това включва не само промяна на length свойството, но също и изтриване на всички елементи, които иначе биха могли да съществуват. Например ако един масив с дължина 4, се настрои на дължина 2, елементите в позиции 2 и 3 се заличават. Можете да постигнете това вътре с set proxy trap заедно с първото поведение. Това отново е предишния пример с актуализация на createMyArray метода:

function toUint32(value) {
  return Math.floor(Math.abs(Number(value))) % Math.pow(2, 32);
}

  function isArrayIndex(key) {
  let numericKey = toUint32(key);
  return String(numericKey) == key && numericKey < (Math..pow(2, 32) - 1);
}

function createMyArray(length=0) {
  return new Proxy({ length }, {
    set(trapTarget, key, value) {

      let currentLength = Reflect.get(trapTarget, "length");

      // специален случай
      if (isArrayIndex(key)) {
          let numericKey = Number(key);

          if (numericKey >= currentLength) {
              Reflect.set(trapTarget, "length", numericKey + 1);
          }  
      } else if (key === "length") {
          
          if (value < currentLength) {
              for (let index = currentLength - 1; index >= value; index--) {
                        Reflect.deleteProperty(trapTarget, index);
              }
          }
        }
      
      // винаги прави това независимо от типа на ключа
      return Reflect.set(trapTarget, key, value);
    }
  });
}

let colors = createMyArray(3);
console.log(colors.length);         // 3

colors[0] = "red";
colors[1] = "green";
colors[2] = "blue";
colors[3] = "black";

console.log(colors.length);         // 4

colors.length = 2;

console.log(colors.length);         // 2
console.log(colors[3]);             // undefined
console.log(colors[2]);             // undefined
console.log(colors[1]);             // "green"
console.log(colors[0]);             // "red"
          

The set proxy trap в този код, проверява дали key е "length", за да коригира останалата част на обекта правилно. Когато това се случи, текущата дължина първо се извлича с помощта на Reflect.get() и се сравнява с новата стойност. Ако новата стойност е по-малка от текущата дължина, тогава for цикъл изтрива всички свойства на целта и те вече не трябва да бъдат на разположение. for цикъла се движи назад по дължината на текущия масив (currentLength) и изтрива всяко свойство, докато достигне до новата дължина на масива (value).

Този пример добавя четири цвята към colors и след това определя свойството length на 2. Това ефективно премахва елементите в позиции 2 и 3, така че те сега връщат undefined, при опит за достъп до тях. Свойството length е правилно настроено на 2 и елементите в позиции 0 и 1 все още са достъпни.

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

Изпълнение на MyArray Class

Най-простият начин за създаване на клас, който използва proxy е да дефинирате клас, както обикновено и след това да върнете proxy от конструктора. По този начин, върнатия обект ще бъде proxy вместо инстанцията на този клас. (Инстанцията е стойността на this вътре в конструктора.) Инстанцията става target на proxy и proxy се връща, като инстанция. Инстанцията ще бъде напълно самостоятелна и няма да можете да я достигате директно, ще имате достъп до нея косвено чрез proxy.

Ето един прост пример за връщане на proxy от клас конструктор:

class Thing {
    constructor() {
        return new Proxy(this, {});
    }
}

let myThing = new Thing();
console.log(myThing instanceof Thing);      // true
          

В този пример, класа Thing връща proxy от своя конструктор. Target на proxy е this и proxy се връща от конструктора. Това означава, че myThing всъщност е proxy, въпреки че е създаден с извикване на конструктора от Thing. Тъй като proxies подават своето поведение към техните targets, myThing все още се счита за инстанция на Thing, като proxy е напълно прозрачен за всеки, който използва Thing класа.

Имайки това в предвид, създаването на потребителски клас масив, използвайки proxy е относително ясно. Кодът е предимно същия, като кода в раздела "Изтриване на елементи за намаляване на дължина". Използва се същия proxy код, но този път това е вътре в клас конструктора. Ето пълния пример:

function toUint32(value) {
  return Math.floor(Math.abs(Number(value))) % Math.pow(2, 32);
}

  function isArrayIndex(key) {
  let numericKey = toUint32(key);
  return String(numericKey) == key && numericKey < (Math..pow(2, 32) - 1);
}

class MyArray {
  constructor(length=0) {
      this.length = length;
      
      return new Proxy(this, { 
          set(trapTarget, key, value) {

            let currentLength = Reflect.get(trapTarget, "length");

            // специален случай
            if (isArrayIndex(key)) {
                let numericKey = Number(key);

                if (numericKey >= currentLength) {
                    Reflect.set(trapTarget, "length", numericKey + 1);
                }  
            } else if (key === "length") {
          
                if (value < currentLength) {
                    for (let index = currentLength - 1; index >= value; index--) {
                        Reflect.deleteProperty(trapTarget, index);
                    }
                }
            }
      
            // винаги прави това независимо от типа на ключа
            return Reflect.set(trapTarget, key, value);
        }
    });
  }
}

let colors = new MyArray(3);
console.log(colors instanceof MyArray);     // true

console.log(colors.length);         // 3

colors[0] = "red";
colors[1] = "green";
colors[2] = "blue";
colors[3] = "black";

console.log(colors.length);         // 4

colors.length = 2;

console.log(colors.length);         // 2
console.log(colors[3]);             // undefined
console.log(colors[2]);             // undefined
console.log(colors[1]);             // "green"
console.log(colors[0]);             // "red"
          

Този код създава MyArray клас, който връща proxy от конструктора си. Свойството length се добавя в конструктора (като се инициализира със стойността, която се подава във или със стойност по подразбиране 0) и след това се създава proxy и се въръща. Това дава на променливата colors облик на точно копие на MyArray и изпълнява двете основни поведения за масиви.

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

Използване на Proxy, като Prototype

Proxies могат да бъдат използвани, като прототипи, но това е малко по-ангажиращо от предишните примери в тази глава. Когато proxy е прототип, proxy traps се извикват само, когато операцията е по подразбиране и обикновено продължава към прототипа, което ограничава възможностите на proxy, като прототип. Помислете за този пример:

let target = {};
let newTarget = Object.create(new Proxy(target, {

    // никога не се извиква
    defineProperty(trapTarget, name, descriptor) {

        // ще доведе до грешка, ако се извика
        return false;
    }
}));

Object.defineProperty(newTarget, "name", {
    value: "newTarget"
});

console.log(newTarget.name);                    // "newTarget"
console.log(newTarget.hasOwnProperty("name"));  // true
          

Обекта NewTarget е създаден с proxy, като прототип. Правейки target на proxy target, ефективно прави target прототип на newTarget, защото proxy е прозрачен. Сега proxy traps ще бъдат извикани само ако операция на newTarget подаде операцията да се случи в target.

Метода Object.defineProperty() се извиква в newTarget за да създадете собствено свойство, наречено name. Дефиниране на свойство върху обект не е операция, която обикновено продължава да е прототип на обекта, така че никога не се извиква defineProperty trap на proxy и свойството name се добавя към newTarget, като собствено свойство.

Докато proxies са силно ограничени, когато се използват, като прототипи, има някои traps, които все още са полезни.

Използване на get trap в Prototype

Когато вътрешния [[Get]] метод се извика да чете свойство, операцията първо се оглежда за собствени свойства. Ако не намери собствено свойство с даденото име, след това операцията продължава към прототипа и търси свойството там. Процесът продължава докато няма никакви по-нататъшни прототипи за проверка.

Благодарение на този процес, ако настроите get proxy trap, trap ще се извика в прототипа, когато собствено свойство с даденото име не съществува. Можете да използвате get trap да предотврати неочаквано поведение, като достъп до свойствата, които не можете да гарантирате, че съществуват. Просто създавате обект, който хвърля грешка, когато се опитвате да получите достъп до свойство, което не съществува:

let target = {};
let thing = Object.create(new Proxy(target, {
    get(trapTarget, key, receiver) {
        throw new ReferenceError(`${key} doesn't exist`);
    }
}));

thing.name = "thing";

console.log(thing.name);        // "thing"

// хвърля се грещка
let unknown = thing.unknown;
          

В този код thing обекта е създаден с proxy, като негов прототип. The get trap хвърля грешка, когато бъде извикан за да покаже, че даден ключ не съществува в thing обекта. Когато thing.name се чете, операцията никога не извиква get trap в прототипа, защото свойството съществува в thing. The get trap се извиква само, когато thing.unknown, който не съществува, е достъпен.

Когато се изпълнява последния ред, unknown не е свойство на thing, така че операцията продължава в прототипа и get trap след това хвърля грешка. Този тип поведение може да бъде много полезен в JavaScript, където неизвестни свойства мълчаливо връщат undefined, вместо да хвърлят грешка (както се случва в други езици).

Важно е да се разбере, че в този пример trapTarget и receiver са различни обекти. Когато proxy се използва, като прототип, trapTarget е самия обект прототип, докато receiver е инстанция на обекта. В този случай, това означава, че trapTarget е равен на target и receiver е равен на thing. Това ви позволява достъп, както до оригиналния обект на proxy, така и до обекта, в който операцията е трябвало да се осъществи.

Използване на set Trap в Prototype

Вътрешния [[Set]] метод също проверява за собствени свойства и след това продължава в прототипа, ако е необходимо. Когато присвоявате стойност на свойство на обект, стойността се присвоява на собствено свойство със същото име, ако съществува. Ако собствено свойство с даденото име не съществува, тогава операцията продължава в прототипа. Сложната част е, че въпреки че операцията на присвояване продължава в прототипа, присвояване на стойност на свойство ще създаде свойство на инстанцията (а не прототипа) по подразбиране, независимо дали съществува свойство с такова име в прототипа.

За да получите по-добра представа, кога set trap ще бъде извикан в прототипа и кога няма. Разгледайте следния пример, който показва поведението по подразбиране:

let target = {};
let thing = Object.create(new Proxy(target, {
    set(trapTarget, key, value, receiver) {
         return Reflect.set(trapTarget, key, value, receiver);
    }
}));

console.log(thing.hasOwnProperty("name"));      // false

// задейства `set` proxy trap
thing.name = "thing";

console.log(thing.name);                        // "thing"
console.log(thing.hasOwnProperty("name"));      // true

// не задейства `set` proxy trap
thing.name = "boo";

console.log(thing.name);                        // "boo"
          

В този пример target започва без собствени свойства. The thing обекта има proxy, като негов прототип, който дефинира set trap за хване създаването на всякакви нови свойства. Когато thing.name присвоява "thing", като негова стойност, set proxy trap се извиква , защото thing не разполага със собствено свойство, наречено name. Вътре в set trap, trapTarget е равен на target, а receiver е равен на thing. Операцията в крайна сметка трябва да създаде ново свойство на thing и за щастие Reflect.set() изпълнява това поведение по подразбиране за вас, ако се подаде receiver, като четвърти аргумент.

След като свойството name е създадено на thing, настройването на thing.name към друга стойност вече няма да извика set proxy trap. В този момент name е собствено свойство, така че [[Set]] операцията никога не продължава към прототипа.

Използване на has Trap в Prototype

Спомнете си, че has trap прихваща използването на in оператора в обекти. Операторът in първо търси за свойство на обект с това име. Ако собствено свойство с такова име не съществува, операцията продължава в прототипа. Ако няма собствено свойство в прототипа, тогава търсенето продължава по веригата от прототипи, докато свойството се установи или няма повече прототипи за търсене.

The has trap се извиква последователно, само когато търсенето достигне proxy обекта в прототипната верига. Вие използвате proxy, като прототип, само когато няма собствено свойство с даденото име. За пример:

let target = {};
let thing = Object.create(new Proxy(target, {
    has(trapTarget, key) {
         return Reflect.has(trapTarget, key);
    }
}));

// задейства `has` proxy trap
console.log("name" in thing);                   // false

thing.name = "thing";

// не задейства `has` proxy trap
console.log("name" in thing);                   // true
          

Този код създава has proxy trap в прототипа на thing. The has trap не подава receiver обект, като get и set traps, защото търсенето в прототипа се случва автоматично, когато се използва оператора in. Вместо това, has trap трябва да работи само върху trapTarget, който е равен на target. Когато за първи път in оператора се използва в този пример, has trap се извиква, защото името на свойството не съществува, като собствено свойство в thing. Когато на thing.name е зададена стойност и след това in оператора се използва отново, has trap не се извиква, защото операцията спира след намирането на името на свойството в thing.

Прототипните примери до този момент са центрирани около обекти, създадени с помощта на Object.create() метода. Но ако искате да създадете клас, който има proxy, като прототип, процесът е малко по-ангажиращ.

Proxies as Prototypes on Classes

Класове не могат директно да бъдат модифицирани за да използвате proxy, като прототип, защото техните prototype свойства са (non-writable) не са достъпни за писане. Можете обаче, да използвате малко заблуда за да създадете клас, който има proxy, като негов прототип, с помощта на наследяването. За да започнете, трябва да създадете дефиниция в ECMAScript 5-стил, с помощта на функция конструктор. След това може да замените прототип да бъде proxy. Ето един пример:

function NoSuchProperty() {
    // празно
}

NoSuchProperty.prototype = new Proxy({}, {
    get(trapTarget, key, receiver) {
        throw new ReferenceError(`${key} doesn't exist`);
    }
});

let thing = new NoSuchProperty();

// хвърля грешка заради `get` proxy trap 
let result = thing.name;
          

Функцията NoSuchProperty представлява основата, от която ще се наследи класа. Няма ограничения за прототипното свойство на функции, така че можете да го замените с proxy. The get trap се използва за да хвърли грешка, когато свойството не съществува. Обекта thing е създаден, като инстанция на NoSuchProperty и хвърля грешка, когато несъществуващо name свойство е достъпно.

Следващата стъпка е да се създаде клас, който наследява от NoSuchProperty. Можете просто да използвате разширения синтаксис, обсъден в глава 9 за да въведете proxy в прототипната верига на класа, като това:

function NoSuchProperty() {
    // празно
}

NoSuchProperty.prototype = new Proxy({}, {
    get(trapTarget, key, receiver) {
        throw new ReferenceError(`${key} doesn't exist`);
    }
});

class Square extends NoSuchProperty {
    constructor(length, width) {
        super();
        this.length = length;
        this.width = width;
    }
}

let shape = new Square(2, 6);

let area1 = shape.length * shape.width;
console.log(area1);                         // 12

// хвърля се грешка, защото "wdth" не съществува
let area2 = shape.length * shape.wdth;
          

Класа Square наследява от NoSuchProperty и така proxy е в прототипната верига на Square класа . Обекта shape след това е създаден, като нова инстанция на Square и има две собствени свойства: length и width. Четенето на стойностите на тези свойства успява, защото get proxy trap никога не се извиква. Само когато свойство, което не съществува в shape е достъпно (shape.wdth е с очевидна правописна грешка), get proxy trap се задейства и хвърля грешка.

Това доказва, че proxy е в прототипната верига на shape, но може да не е очевидно, че proxy не директен прототип на shape. В действителност proxy е няколко стъпки нагоре в прототипната верига на shape. Можете да видите това по-ясно, като леко променим предишния пример:

function NoSuchProperty() {
    // празно
}

// съхранява референция към proxy, който ще бъде прототипът
let proxy = new Proxy({}, {
    get(trapTarget, key, receiver) {
        throw new ReferenceError(`${key} doesn't exist`);
    }
});

NoSuchProperty.prototype = proxy;

class Square extends NoSuchProperty {
    constructor(length, width) {
        super();
        this.length = length;
        this.width = width;
    }
}

let shape = new Square(2, 6);

let shapeProto = Object.getPrototypeOf(shape);

console.log(shapeProto === proxy);                  // false

let secondLevelProto = Object.getPrototypeOf(shapeProto);

console.log(secondLevelProto === proxy);            // true
          

Тази версия на кода съхранява proxy в променлива наречена proxy, така че лесно да се идентифицира по-късно. Прототипа на shape е Shape.prototype, което не е proxy. Но прототип на Shape.prototype е proxy, който е бил наследен от NoSuchProperty.

Наследството добавя още една стъпка в прототипната верига и това има значение, защото операциите, които могат да доведат до извикване на get proxy trap трябва да минат през една допълнителна стъпка преди да стигнат до него. Ако има свойство в Shape.prototype това ще попречи get proxy trap да се извика, както в този пример:

function NoSuchProperty() {
    // празно
}

NoSuchProperty.prototype = new Proxy({}, {
    get(trapTarget, key, receiver) {
        throw new ReferenceError(`${key} doesn't exist`);
    }
});

class Square extends NoSuchProperty {
    constructor(length, width) {
        super();
        this.length = length;
        this.width = width;
    }

    getArea() {
        return this.length * this.width;
    }
}

let shape = new Square(2, 6);

let area1 = shape.length * shape.width;
console.log(area1);                         // 12

let area2 = shape.getArea();
console.log(area2);                         // 12

// хвърля се грешка, защото "wdth" не съществува
let area3 = shape.length * shape.wdth;
          

Тука, класа Square има getArea() метод. The getArea() автоматично се добавя, като метод към Square.prototype, така че когато shape.getArea() се извика, търсенето на метода getArea() започва в инстанцията на shape и след това продължава в неговия прототип. Тъй като, getArea() се намира в прототипа, търсенето спира и proxy никога не се извиква. Това всъщност е поведението, което искате в тази ситуация, тъй като не искате неправилно да се хвърли грешка, когато се извика getArea().

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

Обобщение

Преди ECMAScript 6, някои обекти (като масиви) показват не стандартно поведение, което програмистите не могат да възпроизведат. Proxies променят това. Те ви позволяват да дефинирате свое собствено не стандартно поведение за редица операции от ниско ниво в JavaScript, така че да можете да възпроизведете всички поведения на вградените в JavaScript обекти, чрез proxy trap. Тези traps се извикват зад кулисите, когато се извършват различни операции, например като използването на in оператора.

Reflection API също е въведен в ECMAScript 6, за да позволи на програмистите да прилагат поведение по подразбиране за всеки proxy trap. Всеки proxy trap има съответния метод със същото име в Reflect обекта, друго ECMAScript 6 допълнение. С използването на комбинация от proxy trap и reflection API методи, е възможно да филтрирате някои операции, които да се държат различно само при определени условия.

Revocable proxies са специални proxies, които могат да бъдат ефективно изключени, чрез функцията revoke(). Функцията revoke() прекратява всички функции на proxy, така че всеки опит за взаимодействие с proxy свойствата хвърля грешка след извикването на revoke(). Revocable proxies са важни за приложения за сигурност, където програмисти от трета страна могат да наложат достъп до определени обекти за определен период от време.

Използването на proxy директно е най-мощният случай на използване, но можете да използвате proxy и като прототип на друг обект. В този случай вие сте силно ограничени в броя на proxy traps, които можете да използвате ефективно. Само get, set и has proxy traps могат винаги да се извикват на proxy, когато се използва като прототип, правейки случаите на употреба много по-малки.