Loading...

Декораторлор жана чалууларды багыттоо, call/apply

Декораторлор жана чалууларды багыттоо, call/apply

 

JavaScript функциялар менен иштөөнүн өтө ийкемдүү жолдорун сунуштайт: аларды башка функцияларга өткөрүп, объект катары колдонсо болот, эми биз алардын ортосунда чалууларды кантип багыттоо жана аларды кантип кооздоону карап чыгабыз .

Ачык кэштөө

slow(x)Келгиле, бизде ресурсту көп талап кылган эсептөөлөрдү жүргүзгөн, бирок туруктуу натыйжаларды берген функция бар деп элестетип көрөлү . Башка сөз менен айтканда, бир эле нерсе үчүн, xал ар дайым ошол эле натыйжаны берет.

Функция тез-тез чакырылса, анда биз кайра эсептөөлөргө убакытты үнөмдөө үчүн ал кайтарып берген натыйжаларды кэштегибиз (эстөөнү) каалайбыз.

Кошумча функционалдуулук менен татаалдаштырбастан slow(x), биз аны орогуч функциясына - "wrapper" (англисчеден "wrap" - орогуч), кэш кошууга кошобуз. Бул ыкманын көптөгөн артыкчылыктары бар экенин кийинчерээк көрөбүз.

Бул жерде түшүндүрмөлөр менен код:

@A@function slow(x) {
  // здесь могут быть ресурсоёмкие вычисления
  alert(`Called with ${x}`);
  return x;
}

function cachingDecorator(func) {
  let cache = new Map();

  return function(x) {
    if (cache.has(x)) {    // если кеш содержит такой x,
      return cache.get(x); // читаем из него результат
    }

    let result = func(x); // иначе, вызываем функцию

    cache.set(x, result); // и кешируем (запоминаем) результат
    return result;
  };
}

slow = cachingDecorator(slow);

alert( slow(1) ); // slow(1) кешируем
alert( "Again: " + slow(1) ); // возвращаем из кеша

alert( slow(2) ); // slow(2) кешируем
alert( "Again: " + slow(2) ); // возвращаем из кеша@A@

Жогорудагы коддо cachingDecoratorбул жасалгалоочу , башка функцияны алып, анын жүрүм-турумун өзгөрткөн атайын функция.

Идея, биз cachingDecoratorкаалаган функция менен чалсак болот, натыйжада кэштөөчү таңгыч пайда болот. Бул сонун, анткени бизде бул функцияны колдонгон көптөгөн функциялар болушу мүмкүн жана биз кылышыбыз керек болгон нерсе - колдонуу cachingDecorator.

Кэш кодун негизги коддон бөлүү менен биз акыркысын да таза жана жөнөкөй сактайбыз.

Чалуунун натыйжасы cachingDecorator(func)"ороочу", б.а. Кэштөө логикасында function(x)чалууну "орот" :func(x)

Сырткы коддун көз карашынан алганда, оролгон функция slowдагы эле ошол эле нерсени кылат. Ороочу жөн гана анын жүрүм-турумуна кэш аспектисин кошот.

cachingDecoratorЖыйынтыктап айтканда, коддун өзүн өзгөртүүнүн ордуна өзүнчө колдонуунун бир нече артыкчылыктары бар slow:

  • Функцияны cachingDecoratorкайра колдонсо болот. Биз аны башка функцияга колдоно алабыз.
  • Кэштөө логикасы өзүнчө, ал өзүнүн татаалдыгын арттырбайт slow(эгерде бар болсо).
  • Зарыл болсо, биз бир нече жасалгалоочуларды бириктире алабыз (бул тууралуу кийинчерээк сүйлөшөбүз).

Контекстти өткөрүү үчүн "func.call" колдонуу.

Жогоруда айтылган кэш жасалгалоочу объект ыкмалары менен иштөө үчүн ылайыктуу эмес.

Мисалы, төмөнкү коддо, worker.slow()ал декораторду колдонгондон кийин иштебей калат:

 
 
@A@// сделаем worker.slow кеширующим
let worker = {
  someMethod() {
    return 1;
  },

  slow(x) {
    // здесь может быть страшно тяжёлая задача для процессора
    alert("Called with " + x);
    return x * this.someMethod(); // (*)
  }
};

// тот же код, что и выше
function cachingDecorator(func) {
  let cache = new Map();
  return function(x) {
    if (cache.has(x)) {
      return cache.get(x);
    }
    let result = func(x); // (**)
    cache.set(x, result);
    return result;
  };
}

alert( worker.slow(1) ); // оригинальный метод работает

worker.slow = cachingDecorator(worker.slow); // теперь сделаем его кеширующим

alert( worker.slow(2) ); // Ой! Ошибка: не удаётся прочитать свойство 'someMethod' из 'undefined'@A@

Ката линияда пайда болот (*). Функция кирүүгө аракет кылат this.someMethodжана ишке ашпай калат. Эмнеге көрдүңүз?

Себеби, сызыкта (**)жасалгалоочу баштапкы функцияны деп атайт func(x), бул учурда ал алат this = undefined.

Эгерде биз чуркай турган болсок, окшош жагдайды байкамакпыз:

let func = worker.slow;
func(2);

Ошол. жасалгалоочу чакырууну баштапкы методго өткөрөт, бирок контекстсиз. Демек, ката.

Келгиле, муну оңдойлу.

Func.call (контекст, …args) атайын орнотулган функция ыкмасы бар , ал функцияны ачык коюу менен чакырууга мүмкүндүк берет this.

Синтаксис:

func.call(context, arg1, arg2, ...)

funcАл биринчи аргументти контекст катары thisжана кийинки аргументтерди аргумент катары колдонуу менен функцияны иштетет .

Жөнөкөй сөз менен айтканда, бул эки чалуу дээрлик бирдей нерсени аткарат:

func(1, 2, 3);
func.call(obj, 1, 2, 3)

Экөө тең funcаргументтер менен 1чакырышат 2жана 3. Бир гана айырмасы - бул func.callда thisбелгилейт obj.

Мисалы, төмөндөгү коддо биз sayHiар кандай объекттердин контекстинде чакырабыз: sayHi.call(user)runs sayHi, passing this=user, жана төмөнкү сап топтомдору this=admin:

 
 
@A@function sayHi() {
  alert(this.name);
}

let user = { name: "John" };
let admin = { name: "Admin" };

// используем 'call' для передачи различных объектов в качестве 'this'
sayHi.call( user ); // John
sayHi.call( admin ); // Admin

Бул жерде биз берилген контекст жана сөз айкашы менен callчакыруу үчүн колдонобуз:say

 
 
function say(phrase) {
  alert(this.name + ': ' + phrase);
}

let user = { name: "John" };

// 'user' становится 'this', и "Hello" становится первым аргументом
say.call( user, "Hello" ); // John: Hello

callБиздин учурда, контекстти баштапкы функцияга өткөрүү үчүн орогучта колдоно алабыз :

 
 
let worker = {
  someMethod() {
    return 1;
  },

  slow(x) {
    alert("Called with " + x);
    return x * this.someMethod(); // (*)
  }
};

function cachingDecorator(func) {
  let cache = new Map();
  return function(x) {
    if (cache.has(x)) {
      return cache.get(x);
    }
    let result = func.call(this, x); // теперь 'this' передаётся правильно
    cache.set(x, result);
    return result;
  };
}

worker.slow = cachingDecorator(worker.slow); // теперь сделаем её кеширующей

alert( worker.slow(2) ); // работает
alert( worker.slow(2) ); // работает, не вызывая первоначальную функцию (кешируется)@A@

Азыр баары жайында.

Баары түшүнүктүү болушу үчүн, келгиле, анын кантип жугушун тереңирээк карап көрөлү this:

  1. Декорациядан кийин worker.slowал орогуч болуп калат function (x) { ... }.
  2. Ошентип, аткарылганда, worker.slow(2)орогуч 2аргумент катары кабыл алат жана this=worker(анткени бул чекиттин алдындагы объект).
  3. Ороочтун ичинде, эгерде натыйжа буга чейин кэштеле элек болсо, func.call(this, x)учурдагы this=worker) жана учурдагы аргументти ( =2) баштапкы функцияга өткөрөт.

"func.apply" менен бир нече аргументтерди өткөрүү

cachingDecoratorЭми аны ого бетер ар тараптуу кылалы . Буга чейин ал бир гана аргументтүү функциялар менен иштеген.

Бир нече аргументи бар ыкманы кантип кэштөө керек worker.slow?

@A@let worker = {
  slow(min, max) {
    return min + max; // здесь может быть тяжёлая задача
  }
};

// будет кешировать вызовы с одинаковыми аргументами
worker.slow = cachingDecorator(worker.slow);@A@

Бул жерде биз чече турган эки маселе бар.

Биринчиден, коллекциядагы аргументти minжана ачкычты кантип колдонсо болот ? Мурда бир аргумент үчүн биз жөн гана натыйжаны сактап , аны кийинчерээк алуу үчүн чакыра алмакпыз. Бирок азыр биз аргументтердин айкалышынын жыйынтыгын эстешибиз керек . Курулган ачкыч катары бир гана маанини кабыл алат.maxcachexcache.set(x, result)cache.get(x) (min,max)Map

Көптөгөн мүмкүн болгон чечимдер бар:

  1. Жыйноо үчүн жаңы (же үчүнчү тарапты колдонуңуз) орнотулганга караганда жалпы Mapжана бир нече ачкычтарды колдогон маалымат структурасын ишке ашырыңыз.
  2. Уюшкан коллекцияларды колдонуңуз: cache.set(min)will Map, ал жупту сактайт (max, result). Андан кийин resultтелефон чалып алсак болот cache.get(min).get(max).
  3. Эки баалуулукту бирге бириктириңиз. Биздин өзгөчө учурда, биз жөн гана сапты "min,max"ачкыч катары колдоно алабыз Map. Ийкемдүүлүк үчүн, биз хэшинг функциясын көптүн ичинен бир маанини кантип жасоону билген декораторго өткөрүүгө уруксат бере алабыз.

Көптөгөн практикалык колдонмолор үчүн үчүнчү вариант жетиштүү, ошондуктан биз аны карманабыз.

Биринчи эле эмес, оролгон функцияга бардык аргументтерди өткөрүү үчүн биз func.call(this, x)менен алмаштыруу керек .func.call(this, ...arguments)

Бул жерде күчтүүрөөк cachingDecorator:

 
 
@A@let worker = {
  slow(min, max) {
    alert(`Called with ${min},${max}`);
    return min + max;
  }
};

function cachingDecorator(func, hash) {
  let cache = new Map();
  return function() {
    let key = hash(arguments); // (*)
    if (cache.has(key)) {
      return cache.get(key);
    }

    let result = func.call(this, ...arguments); // (**)

    cache.set(key, result);
    return result;
  };
}

function hash(args) {
  return args[0] + ',' + args[1];
}

worker.slow = cachingDecorator(worker.slow, hash);

alert( worker.slow(3, 5) ); // работает
alert( "Again " + worker.slow(3, 5) ); // аналогично (из кеша)@A@

Азыр ал аргументтердин ар кандай саны менен иштейт.

Эки өзгөртүү бар:

  • Бир ачкычты түзүү үчүн онлайн (*)чалуу . Бул жерде биз аргументтерди ачкычка айландырган жөнөкөй "биримдик" функциясын колдонуп жатабыз . Татаал учурлар башка хэшинг функцияларын талап кылышы мүмкүн.hasharguments(3, 5)"3,5"
  • Андан кийин, бир сапта биз аны контекстти да, орогуч тарабынан алынган бардык аргументтерди (алардын санына карабастан) баштапкы функцияга өткөрүү үчүн (**)колдонобуз .func.call(this, ...arguments)

Анын ордуна, func.call(this, ...arguments)биз жаза алабыз func.apply(this, arguments).

Камтылган func.apply методунун синтаксиси :

func.apply(context, args)

Ал аргументтердин тизмеси катары псевдо-массивди funcкоюу жана алуу менен аткарат .this=contextargs

callжана ортосундагы синтаксистин бир гана айырмасы, applyал псевдо-массивди алып callжатканда аргументтердин тизмесин күтөт .apply

Бул эки чалуу дээрлик эквиваленттүү:

@A@func.call(context, ...args); // передаёт массив как список с оператором расширения
func.apply(context, args);   // тот же эффект@A@

Бир гана кичинекей айырма бар:

  • Жайылтуу оператору кайталанган объектти тизмеге ...өткөрүүгө мүмкүндүк берет .argscall
  • Ал псевдо-массивдиapply гана кабыл алат . args

Ошентип, бул чакырыктар бири-бирин толуктап турат. Кайталануучу объекттер үчүн иштейт call, жана биз псевдо-массив күткөн жерде - apply.

Эгер бизде экөөнү тең аткарган объект болсо, мисалы, чыныгы массив, анда техникалык жактан биз эки ыкманы колдонсок болот, бирок, балким, applyтезирээк болмок, анткени көпчүлүк JavaScript кыймылдаткычтары аны ички оптималдаштырышат.

Бардык аргументтерди башка функциянын контексти менен бирге өткөрүү "чакырууну багыттоо" деп аталат.

Мындай багыттоо эң жөнөкөй түрү:

@A@let wrapper = function() {
  return func.apply(this, arguments);
};@A@

Тышкы коддон чакырганда wrapper, ал баштапкы функцияны чакыруудан айырмаланбайт.

Карыз алуу ыкмасы

Эми хэш-функцияга дагы бир кичинекей жакшыртуу жасайлы:

@A@function hash(args) {
  return args[0] + ',' + args[1];
}@A@

Учурда ал эки гана аргумент үчүн иштейт. Каалаган санды чаптап алса жакшы болмок args.

Табигый чечим arr.join ыкмасын колдонуу болот :

@A@function hash(args) {
  return args.join();
}@A@

...Тилекке каршы, бул иштебейт, анткени биз чалып жатабыз hash(arguments)жана объект argumentsкайталануучу жана чыныгы массив эмес, псевдо-массив.

joinОшентип , биз төмөндө көрүп тургандай, ага чакыруу ишке ашпай калат:

 
 
@A@function hash() {
  alert( arguments.join() ); // Ошибка: arguments.join не является функцией
}

hash(1, 2);@A@

Бирок, массивди бириктирүүнү колдонуунун оңой жолу бар:

 
 
@A@function hash() {
  alert( [].join.call(arguments) ); // 1,2
}

hash(1, 2);@A@

Бул ыкма карыз алуу деп аталат .

joinБиз кадимки массивден методду алабыз (карызга алабыз) [].join. Жана биз [].join.callаны контекстте аткаруу үчүн колдонобуз arguments.

Эмне үчүн ал иштейт?

Себеби, орнотулган ыкманын ички алгоритми arr.join(glue)абдан жөнөкөй. Иш жүзүндө "болгондой" спецификациядан алынган:

  1. Биринчи аргумент болсун glueже аргументтер жок болсо үтүр болот","
  2. resultБул бош сап болсун "".
  3. кошуу . this[0]_result
  4. кошуу glueжана this[1].
  5. кошуу glueжана this[2].
  6. ... this.lengthэлементтер бири-бирине чапталганга чейин аткарыңыз.
  7. Return result.

Ошентип, техникалык жактан ал алат thisжана бириктирет this[0]this[1]... ж.б. бирге. Бул кандайдыр бир псевдо-массивди кабыл алуу үчүн атайын жазылган this(кокусунан эмес, көптөгөн ыкмалар бул практиканы аткарат). Ошондуктан ал менен да иштейт this=arguments.

Бардыгы

Декоратор - бул функциянын кыймыл-аракетин өзгөрткөн оролгон. Негизги ишти дагы эле функция аткарат.

Көбүнчө бир кичинекей нерседен башка функцияны же ыкманы кооздолгонго алмаштыруу коопсуз. Эгерде оригиналдуу функция ушуга окшогон func.calledCountже ушул сыяктуу касиеттерди камсыз кылса, анда кооздолгон функция аларды камсыз кылбайт. Анткени ал орогуч. Андыктан аларды колдонууда этият болушуңуз керек. Кээ бир жасалгалоочулар өз касиеттерин камсыз кылат.

Декораторлорду функцияга кошула турган "кошумча функциялар" же "аспекттер" катары кароого болот. Биз бир же бир нече жасалгалоочуну кошо алабыз. Мунун баары баштапкы функциянын кодун өзгөртпөстөн!

Ишке ашыруу үчүн cachingDecoratorбиз ыкмаларды изилдедик:

Негизинен чалууларды багыттоо төмөнкү менен жүзөгө ашырылат apply:

let wrapper = function(original, arguments) {
  return original.apply(this, arguments);
};

Биз ошондой эле карыз алуу ыкмасын карадык , анда биз башка объекттин контекстинде объект боюнча методду чакырабыз. Массив ыкмаларын алуу жана аларды колдонуу үчүн кеңири таралган arguments...argsЖе болбосо, чыныгы массив болгон rest объектин колдоно аласыз .

Иш жүзүндө, декораторлор ар кандай милдеттерди аткаруу үчүн колдонулат. Бул бөлүмдөгү маселелерди чечүү менен аларды канчалык жакшы өздөштүргөнүңүздү текшериңиз.

Tasks

маанилүүлүгү: 5

spy(func)Бардык функциялык чакырыктарды өзүнүн менчигинде сактаган таңгычты кайтарып бере турган жасалгалоочуну түзүңүз calls.

Ар бир чалуу аргументтердин массивинде сакталышы керек.

Мисалы:

@A@function work(a, b) {
  alert( a + b ); // произвольная функция или метод
}

work = spy(work);

work(1, 2); // 3
work(4, 5); // 9

for (let args of work.calls) {
  alert( 'call:' + args.join() ); // "call:1,2", "call:4,5"
}@A@

PS: Бул жасалгалоочу кээде бирдикти сыноо үчүн пайдалуу. Анын кеңейтилген түрү - - Sinon.JSsinon.spy китепканасында камтылган .

Тапшырма үчүн тесттер менен кумкоргонду ачыңыз.

чечим
маанилүүлүгү: 5

Ар бир чалууну миллисекундга delay(f, ms)кечиктирген жасалгалоочу түзүңүз . Мисалы:fms

@A@function f(x) {
  alert(x);
}

// создаём обёртки
let f1000 = delay(f, 1000);
let f1500 = delay(f, 1500);

f1000("test"); // показывает "test" после 1000 мс
f1500("test"); // показывает "test" после 1500 мс@A@

Башка сөз менен айтканда, ал "мес үчүн кечигүү " параметрин delay(f, ms)кайтарат .fms

Жогорудагы коддо fбул бир аргументи бар функция, бирок сиздин чечимиңиз бардык аргументтерден жана контексттен өтүшү керек this.

Тапшырма үчүн тесттер менен кумкоргонду ачыңыз.

чечим
маанилүүлүгү: 5

Декоратордун натыйжасы debounce(f, ms)чалуудан миллисекундда fэң көп дегенде бир жолу өтүүчү орогуч болушу керек ms. Башкача айтканда, биз чалганда debounce, ал бардык башка чалуулар учурунда көңүл бурулбай турганын камсыздайт ms.

Мисалы:

@A@let f = debounce(alert, 1000);

f(1); // выполняется немедленно
f(2); // проигнорирован

setTimeout( () => f(3), 100); // проигнорирован (прошло только 100 мс)
setTimeout( () => f(4), 1100); // выполняется
setTimeout( () => f(5), 1500); // проигнорирован (прошло только 400 мс от последнего вызова)@A@

Маалыматтарды алган/жаңыртуучу функциялар үчүн практикада пайдалуу debounceжана кыска убакыттын ичинде кайра чалуу жаңы эч нерсе алып келбей турганын билебиз. Андыктан ага ресурстарды коротпогону жакшы.

Тапшырма үчүн тесттер менен кумкоргонду ачыңыз.

чечим
маанилүүлүгү: 5

Чалууну миллисекундда эң көп дегенде бир жолу throttle(f, ms)өткөрүп, орогучту кайтарган жай жасалгалоочуну түзүңүз . «Тормоздук» мезгилге туш келген чакырыктарга көңүл бурулбайт.fms

Айырмачылыгы debounce- эгерде көңүл бурулбаган чалуу "кечигүү" учурунда акыркы болуп калса, анда ал аягында аткарылат.

Келгиле, бул талапты жакшыраак түшүнүү жана анын кайдан келгенин билүү үчүн реалдуу тиркемени карап көрөлү.

Мисалы, биз чычкандын кыймылына көз салгыбыз келет.

Браузерде көрсөткүч жылган сайын иштей турган жана анын жайгашкан жерин ала турган функцияны жарыялай алабыз. Чычканды активдүү колдонуу учурунда бул функция абдан тез иштейт, ал секундасына 100 жолу (ар бир 10 мс) болушу мүмкүн.

Көчкөндө баракчадагы маалыматты жаңырткыбыз келет.

...Бирок жаңыртуу функциясы update()өтө көп ресурстарды талап кылат, аны ар бир микро кыймыл менен аткаруу үчүн. Ооба, 1000 мс сайын бир жолудан көп жаңыртуу мааниси жок.

throttle(update, 1000)Ошондуктан, биз чакырууну жасалгалоочуга оройбуз: аны көрсөткүчтүн ордуна оригиналдуу ордуна жылдырган сайын иштей турган функция катары колдонобуз update(). Декоратор тез-тез чакырылат, бирок чалууну update()эң көп дегенде 1000 мс бир жолу жөнөтөт.

Визуалдык түрдө ал төмөнкүдөй болот:

  1. Көрсөткүчтүн биринчи кыймылы үчүн кооздолгон версия дароо чакырууну update. Бул маанилүү, анткени колдонуучу дароо анын кыймылына биздин реакцияны көрөт.
  2. Андан кийин, көрсөткүч кыймылды улантканда, 1000 мс эч нерсе болбойт. Декорацияланган версия чалууларга көңүл бурбайт.
  3. 1000 мсден кийин updateакыркы координаттар менен дагы бир чакыруу пайда болот.
  4. Анан, акырында, көрсөткүч бир жерде токтойт. Декорацияланган версия 1000 мс өткүчө күтүп, андан соң updateакыркы координаттарды чакырат. Натыйжада көрсөткүчтүн акыркы координаттары да иштетилет.

Код мисалы:

@A@function f(a) {
  console.log(a)
}

// f1000 передаёт вызовы f максимум раз в 1000 мс
let f1000 = throttle(f, 1000);

f1000(1); // показывает 1
f1000(2); // (ограничение, 1000 мс ещё нет)
f1000(3); // (ограничение, 1000 мс ещё нет)

// когда 1000 мс истекли ...
// ...выводим 3, промежуточное значение 2 было проигнорировано@A@

PS Аргументтер жана thisберилген контекст f1000түпнускага өткөрүлүшү керек f.