Как расширить функцию с помощью классов ES6?


106

ES6 позволяет расширять специальные объекты. Таким образом, можно унаследовать от функции. Такой объект можно вызвать как функцию, но как реализовать логику такого вызова?

class Smth extends Function {
  constructor (x) {
    // What should be done here
    super();
  }
}

(new Smth(256))() // to get 256 at this call?

Любой метод класса получает ссылку на экземпляр класса через this. Но когда он вызывается как функция, thisссылается на window. Как получить ссылку на экземпляр класса, когда он вызывается как функция?

PS: Тот же вопрос по русски.


18
Ах, наконец-то кто-то спросил этот квест :-)
Берги

1
Просто сделать super(x)(т.е. передать Function)? Не уверен, что Functionможно действительно продлить.
Феликс Клинг,

Имейте в виду, что все еще существуют проблемы с расширением встроенных классов. Спецификация предполагает, что это должно быть возможно, но я столкнулся Error, среди прочего, с проблемами расширения .
ssube

1
Имейте в виду, что Functionэто просто конструктор функции. Реализация функции должна быть передана конструктору. Если вы не хотите , Smthчтобы принять выполнение, вы должны предоставить его в конструкторе, то есть super('function implementation here').
Феликс Клинг,

1
@Qwertiy: Я бы сказал, что это исключение , а не общий случай. Это также очень специфично для функциональных выражений , но вы используете Functionконструктор (среду выполнения), который сильно отличается от функционального выражения (синтаксиса).
Феликс Клинг,

Ответы:


49

superВызов будет вызывать Functionконструктор, который строку кода. Если вы хотите получить доступ к данным вашего экземпляра, вы можете просто жестко его закодировать:

class Smth extends Function {
  constructor(x) {
    super("return "+JSON.stringify(x)+";");
  }
}

но это не очень приятно. Мы хотим использовать закрытие.

Сделать возвращенную функцию закрытием, которое может получить доступ к вашим переменным экземпляра, возможно, но не просто. Хорошо то, что вам не нужно вызывать, superесли вы не хотите - вы по-прежнему можете returnпроизвольно использовать объекты из своих конструкторов класса ES6. В этом случае мы бы сделали

class Smth extends Function {
  constructor(x) {
    // refer to `smth` instead of `this`
    function smth() { return x; };
    Object.setPrototypeOf(smth, Smth.prototype);
    return smth;
  }
}

Но мы можем сделать еще лучше и абстрагироваться от этого Smth:

class ExtensibleFunction extends Function {
  constructor(f) {
    return Object.setPrototypeOf(f, new.target.prototype);
  }
}

class Smth extends ExtensibleFunction {
  constructor(x) {
    super(function() { return x; }); // closure
    // console.log(this); // function() { return x; }
    // console.log(this.prototype); // {constructor: …}
  }
}
class Anth extends ExtensibleFunction {
  constructor(x) {
    super(() => { return this.x; }); // arrow function, no prototype object created
    this.x = x;
  }
}
class Evth extends ExtensibleFunction {
  constructor(x) {
    super(function f() { return f.x; }); // named function
    this.x = x;
  }
}

По общему признанию, это создает дополнительный уровень косвенности в цепочке наследования, но это не обязательно плохо (вы можете расширить его вместо родного Function). Если вы хотите избежать этого, используйте

function ExtensibleFunction(f) {
  return Object.setPrototypeOf(f, new.target.prototype);
}
ExtensibleFunction.prototype = Function.prototype;

но обратите внимание, что Smthне будет динамически наследовать статические Functionсвойства.


Я хочу получить доступ к состоянию класса из функции.
Qwertiy

2
@Qwertiy: Тогда воспользуйтесь вторым предложением Берги.
Феликс Клинг,

@ AlexanderO'Mara: Вы не можете обойтись без мутации прототипа функции, если хотите, чтобы ваши Smthэкземпляры были такими instanceof Smth(как все ожидали). Вы можете опустить Object.setPrototypeOfвызов, если вам не нужен этот или какой-либо из ваших методов-прототипов, объявленных в вашем классе.
Bergi

@ AlexanderO'Mara: Также Object.setPrototypeOfне так много опасностей при оптимизации, если это делается сразу после создания объекта. Просто если вы измените [[прототип]] объекта взад и вперед в течение его жизни, это будет плохо.
Берги

1
@amn Нет, если вы не используете thisи returnобъект.
Берги,

32

Это подход к созданию вызываемых объектов, которые правильно ссылаются на свои члены объекта и поддерживают правильное наследование, не вмешиваясь в прототипы.

Просто:

class ExFunc extends Function {
  constructor() {
    super('...args', 'return this.__self__.__call__(...args)')
    var self = this.bind(this)
    this.__self__ = self
    return self
  }

  // Example `__call__` method.
  __call__(a, b, c) {
    return [a, b, c];
  }
}

Расширьте этот класс и добавьте __call__метод, подробнее ниже ...

Объяснение в коде и комментариях:

// This is an approach to creating callable objects
// that correctly reference their own object and object members,
// without messing with prototypes.

// A Class that extends Function so we can create
// objects that also behave like functions, i.e. callable objects.
class ExFunc extends Function {
  constructor() {
    super('...args', 'return this.__self__.__call__(...args)');
    // Here we create a function dynamically using `super`, which calls
    // the `Function` constructor which we are inheriting from. Our aim is to create
    // a `Function` object that, when called, will pass the call along to an internal
    // method `__call__`, to appear as though the object is callable. Our problem is
    // that the code inside our function can't find the `__call__` method, because it
    // has no reference to itself, the `this` object we just created.
    // The `this` reference inside a function is called its context. We need to give
    // our new `Function` object a `this` context of itself, so that it can access
    // the `__call__` method and any other properties/methods attached to it.
    // We can do this with `bind`:
    var self = this.bind(this);
    // We've wrapped our function object `this` in a bound function object, that
    // provides a fixed context to the function, in this case itself.
    this.__self__ = self;
    // Now we have a new wrinkle, our function has a context of our `this` object but
    // we are going to return the bound function from our constructor instead of the
    // original `this`, so that it is callable. But the bound function is a wrapper
    // around our original `this`, so anything we add to it won't be seen by the
    // code running inside our function. An easy fix is to add a reference to the
    // new `this` stored in `self` to the old `this` as `__self__`. Now our functions
    // context can find the bound version of itself by following `this.__self__`.
    self.person = 'Hank'
    return self;
  }
  
  // An example property to demonstrate member access.
  get venture() {
    return this.person;
  }
  
  // Override this method in subclasses of ExFunc to take whatever arguments
  // you want and perform whatever logic you like. It will be called whenever
  // you use the obj as a function.
  __call__(a, b, c) {
    return [this.venture, a, b, c];
  }
}

// A subclass of ExFunc with an overridden __call__ method.
class DaFunc extends ExFunc {
  constructor() {
    super()
    this.a = 'a1'
    this.b = 'b2'
    this.person = 'Dean'
  }

  ab() {
    return this.a + this.b
  }
  
  __call__(ans) {
    return [this.ab(), this.venture, ans];
  }
}

// Create objects from ExFunc and its subclass.
var callable1 = new ExFunc();
var callable2 = new DaFunc();

// Inheritance is correctly maintained.
console.log('\nInheritance maintained:');
console.log(callable2 instanceof Function);  // true
console.log(callable2 instanceof ExFunc);  // true
console.log(callable2 instanceof DaFunc);  // true

// Test ExFunc and its subclass objects by calling them like functions.
console.log('\nCallable objects:');
console.log( callable1(1, 2, 3) );  // [ 'Hank', 1, 2, 3 ]
console.log( callable2(42) );  // [ 'a1b2', Dean', 42 ]

// Test property and method access
console.log(callable2.a, callable2.b, callable2.ab())

Посмотреть на repl.it

Дальнейшее объяснение bind:

function.bind()работает очень похоже function.call(), и у них есть аналогичная сигнатура метода:

fn.call(this, arg1, arg2, arg3, ...);больше на мдн

fn.bind(this, arg1, arg2, arg3, ...);больше на мдн

В обоих случаях первый аргумент переопределяет thisконтекст внутри функции. Дополнительные аргументы также могут быть привязаны к значению. Но where callнемедленно вызывает функцию со связанными значениями, bindвозвращает "экзотический" объект функции, который прозрачно обертывает оригинал, с thisпредустановленными аргументами и любыми другими.

Итак, когда вы определяете функцию, bindнекоторые из ее аргументов:

var foo = function(a, b) {
  console.log(this);
  return a * b;
}

foo = foo.bind(['hello'], 2);

Вы вызываете связанную функцию только с оставшимися аргументами, ее контекст предварительно установлен, в данном случае - ['hello'].

// We pass in arg `b` only because arg `a` is already set.
foo(2);  // returns 4, logs `['hello']`

Не могли бы вы добавить объяснение, почему bindработает (т.е. почему он возвращает экземпляр ExFunc)?
Bergi

@Bergi bindвозвращает прозрачный функциональный объект, который обертывает функциональный объект, для которого он был вызван, то есть наш вызываемый объект, только с повторным вызовом thisконтекста. Таким образом, он действительно возвращает прозрачно завернутый экземпляр ExFunc. Сообщение обновлено с дополнительной информацией о bind.
Адриан

1
@Bergi Доступны все геттеры / сеттеры и методы, свойства / атрибуты должны быть назначены constructorпосле bindв ExFunc. В подклассах ExFunc доступны все члены. Что касается instanceof; в es6 связанные функции называются экзотическими, поэтому их внутренняя работа не очевидна, но я думаю, что он передает вызов своей обернутой цели через Symbol.hasInstance. Это очень похоже на прокси, но это простой метод для достижения желаемого эффекта. Их подпись похожа не та.
Adrien

1
@ Адриен, но изнутри __call__я не могу получить доступ this.aили this.ab(). например, repl.it/repls/FelineFinishedDesktopenvironment
rob

1
@rob хорошо заметен, есть справочная ошибка, я обновил ответ и код, добавив исправление и новое объяснение.
Адриан

20

Вы можете обернуть экземпляр Smth в прокси с помощью apply(и, возможно, construct) ловушки:

class Smth extends Function {
  constructor (x) {
    super();
    return new Proxy(this, {
      apply: function(target, thisArg, argumentsList) {
        return x;
      }
    });
  }
}
new Smth(256)(); // 256

Классная идея. Как это. Должен ли я реализовать какую-то дополнительную логику вместо того, чтобы помещать внутри приложения?
Qwertiy

4
Прокси-сервер повлечет за собой некоторые накладные расходы, не так ли? Кроме того, thisвсе еще остается пустая функция (проверка new Smth().toString()).
Берги

2
@Bergi Понятия не имею о производительности. MDN имеет большое красное жирное предупреждение setPrototypeOfи ничего не говорит о прокси. Но я думаю, прокси могут быть такими же проблемными, как setPrototypeOf. А еще toString, его можно затенять кастомным методом в Smth.prototype. В любом случае собственный вариант зависит от реализации.
Oriol

@Qwertiy Вы можете добавить constructловушку, чтобы указать поведение new new Smth(256)(). И добавьте пользовательские методы, которые затеняют собственные методы, которые обращаются к коду функции, как toStringзаметил Берги.
Oriol

Я думаю, ваш applyметод реализован так, как предполагается, или это просто демонстрация, и мне нужно просмотреть дополнительную информацию Proxyи Reflectиспользовать его надлежащим образом?
Qwertiy

3

Я воспользовался советом из ответа Берги и заключил его в модуль NPM .

var CallableInstance = require('callable-instance');

class ExampleClass extends CallableInstance {
  constructor() {
    // CallableInstance accepts the name of the property to use as the callable
    // method.
    super('instanceMethod');
  }

  instanceMethod() {
    console.log("instanceMethod called!");
  }
}

var test = new ExampleClass();
// Invoke the method normally
test.instanceMethod();
// Call the instance itself, redirects to instanceMethod
test();
// The instance is actually a closure bound to itself and can be used like a
// normal function.
test.apply(null, [ 1, 2, 3 ]);

3

Обновить:

К сожалению, это не совсем работает, потому что теперь он возвращает объект функции вместо класса, поэтому кажется, что это действительно невозможно сделать без изменения прототипа. Ламе.


По сути, проблема в том, что нет возможности установить thisзначение для Functionконструктора. Единственный способ действительно сделать это - использовать .bindметод впоследствии, однако это не очень удобно для классов.

Мы могли бы сделать это в вспомогательном базовом классе, однако thisон становится доступным только после первоначального superвызова, поэтому это немного сложно.

Рабочий пример:

'use strict';

class ClassFunction extends function() {
    const func = Function.apply(null, arguments);
    let bound;
    return function() {
        if (!bound) {
            bound = arguments[0];
            return;
        }
        return func.apply(bound, arguments);
    }
} {
    constructor(...args) {
        (super(...args))(this);
    }
}

class Smth extends ClassFunction {
    constructor(x) {
        super('return this.x');
        this.x = x;
    }
}

console.log((new Smth(90))());

(Для примера требуется современный браузер или node --harmony.)

По сути, базовая функция ClassFunctionextends будет заключать Functionвызов конструктора в пользовательскую функцию, которая похожа на .bind, но допускает привязку позже, при первом вызове. Затем в самом ClassFunctionконструкторе он вызывает возвращаемую функцию, из superкоторой теперь является связанная функция, переходя thisк завершению настройки пользовательской функции привязки.

(super(...))(this);

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


1
Вы слишком усложняете. boundбудет ссылаться на функцию, которую вы returnиз этого анонимного класса. Просто назовите его и обратитесь к нему напрямую. Я также рекомендовал бы избегать передачи строк кода, с ними просто беспорядок работать (на каждом этапе процесса разработки).
Bergi

На extendsсамом деле это не работает так, как ожидалось, поскольку Function.isPrototypeOf(Smth)и также new Smth instanceof Functionявляются ложными.
Берги

@Bergi Какой JS-движок вы используете? console.log((new Smth) instanceof Function);это trueдля меня в узле v5.11.0 и последней Firefox.
Александр О'Мара

Ой, неправильный пример. Это new Smth instanceof Smthне работает с вашим решением. Кроме того Smth, в ваших экземплярах не будут доступны методы - поскольку вы просто возвращаете стандарт Function, а не файл Smth.
Берги

1
@Bergi Черт, похоже, ты прав. Однако расширение любых собственных типов, похоже, имеет ту же проблему. extend Functionтакже делает new Smth instanceof Smthложным.
Александр О'Мара

1

Сначала я пришел к решению arguments.callee, но это было ужасно.
Я ожидал, что он сломается в глобальном строгом режиме, но похоже, что он работает даже там.

class Smth extends Function {
  constructor (x) {
    super('return arguments.callee.x');
    this.x = x;
  }
}

(new Smth(90))()

Это был плохой способ из-за использования arguments.callee, передачи кода в виде строки и принудительного выполнения в нестрогом режиме. Но потом applyпоявилась идея перекрыть .

var global = (1,eval)("this");

class Smth extends Function {
  constructor(x) {
    super('return arguments.callee.apply(this, arguments)');
    this.x = x;
  }
  apply(me, [y]) {
    me = me !== global && me || this;
    return me.x + y;
  }
}

И тест, показывающий, что я могу запускать эту функцию по-разному:

var f = new Smth(100);

[
f instanceof Smth,
f(1),
f.call(f, 2),
f.apply(f, [3]),
f.call(null, 4),
f.apply(null, [5]),
Function.prototype.apply.call(f, f, [6]),
Function.prototype.apply.call(f, null, [7]),
f.bind(f)(8),
f.bind(null)(9),
(new Smth(200)).call(new Smth(300), 1),
(new Smth(200)).apply(new Smth(300), [2]),
isNaN(f.apply(window, [1])) === isNaN(f.call(window, 1)),
isNaN(f.apply(window, [1])) === isNaN(Function.prototype.apply.call(f, window, [1])),
] == "true,101,102,103,104,105,106,107,108,109,301,302,true,true"

Версия с

super('return arguments.callee.apply(arguments.callee, arguments)');

на самом деле содержит bindфункциональность:

(new Smth(200)).call(new Smth(300), 1) === 201

Версия с

super('return arguments.callee.apply(this===(1,eval)("this") ? null : this, arguments)');
...
me = me || this;

делает callи applyпо windowнесовместимым:

isNaN(f.apply(window, [1])) === isNaN(f.call(window, 1)),
isNaN(f.apply(window, [1])) === isNaN(Function.prototype.apply.call(f, window, [1])),

поэтому чек следует переместить в apply:

super('return arguments.callee.apply(this, arguments)');
...
me = me !== global && me || this;

1
Что вы на самом деле пытаетесь сделать?
Спасибо

2
Я думаю, что классы всегда находятся в строгом режиме: stackoverflow.com/questions/29283935/…
Александр О'Мара

@ AlexanderO'Mara, кстати, thisявляется окном, а не неопределенным, поэтому созданная функция не находится в строгом режиме (по крайней мере, в хроме).
Qwertiy

Пожалуйста, перестаньте использовать этот ответ. Я уже писал, что это плохой способ. Но это действительно ответ - он работает как в FF, так и в Chrome (не нужно проверять Edge).
Qwertiy

Я предполагаю, что это работает, потому что Functionэто не в строгом режиме. Ужасно, но интересно +1. Однако вы, вероятно, больше не сможете ходить по цепи.
Александр О'Мара

1

Это решение, которое я разработал, которое удовлетворяет все мои потребности в расширении функций и служит мне достаточно хорошо. Преимущества этой техники:

  • При расширении ExtensibleFunctionкод идиоматичен для расширения любого класса ES6 (нет, возиться с воображаемыми конструкторами или прокси).
  • Цепочка прототипов сохраняется во всех подклассах и instanceof/ .constructorвозвращает ожидаемые значения.
  • .bind() .apply()и .call()все работают как положено. Это делается путем переопределения этих методов для изменения контекста «внутренней» функции в отличие от ExtensibleFunctionэкземпляра (или его подкласса).
  • .bind()возвращает новый экземпляр конструктора функций (будь то он ExtensibleFunctionили подкласс). Он используется Object.assign()для обеспечения того, чтобы свойства, хранящиеся в связанной функции, соответствовали свойствам исходной функции.
  • Замыкания соблюдаются, а стрелочные функции продолжают поддерживать правильный контекст.
  • «Внутренняя» функция хранится через a Symbol, который может быть запутан модулями или IIFE (или любым другим распространенным методом приватизации ссылок).

И без лишних слов код:

// The Symbol that becomes the key to the "inner" function 
const EFN_KEY = Symbol('ExtensibleFunctionKey');

// Here it is, the `ExtensibleFunction`!!!
class ExtensibleFunction extends Function {
  // Just pass in your function. 
  constructor (fn) {
    // This essentially calls Function() making this function look like:
    // `function (EFN_KEY, ...args) { return this[EFN_KEY](...args); }`
    // `EFN_KEY` is passed in because this function will escape the closure
    super('EFN_KEY, ...args','return this[EFN_KEY](...args)');
    // Create a new function from `this` that binds to `this` as the context
    // and `EFN_KEY` as the first argument.
    let ret = Function.prototype.bind.apply(this, [this, EFN_KEY]);
    // For both the original and bound funcitons, we need to set the `[EFN_KEY]`
    // property to the "inner" function. This is done with a getter to avoid
    // potential overwrites/enumeration
    Object.defineProperty(this, EFN_KEY, {get: ()=>fn});
    Object.defineProperty(ret, EFN_KEY, {get: ()=>fn});
    // Return the bound function
    return ret;
  }

  // We'll make `bind()` work just like it does normally
  bind (...args) {
    // We don't want to bind `this` because `this` doesn't have the execution context
    // It's the "inner" function that has the execution context.
    let fn = this[EFN_KEY].bind(...args);
    // Now we want to return a new instance of `this.constructor` with the newly bound
    // "inner" function. We also use `Object.assign` so the instance properties of `this`
    // are copied to the bound function.
    return Object.assign(new this.constructor(fn), this);
  }

  // Pretty much the same as `bind()`
  apply (...args) {
    // Self explanatory
    return this[EFN_KEY].apply(...args);
  }

  // Definitely the same as `apply()`
  call (...args) {
    return this[EFN_KEY].call(...args);
  }
}

/**
 * Below is just a bunch of code that tests many scenarios.
 * If you run this snippet and check your console (provided all ES6 features
 * and console.table are available in your browser [Chrome, Firefox?, Edge?])
 * you should get a fancy printout of the test results.
 */

// Just a couple constants so I don't have to type my strings out twice (or thrice).
const CONSTRUCTED_PROPERTY_VALUE = `Hi, I'm a property set during construction`;
const ADDITIONAL_PROPERTY_VALUE = `Hi, I'm a property added after construction`;

// Lets extend our `ExtensibleFunction` into an `ExtendedFunction`
class ExtendedFunction extends ExtensibleFunction {
  constructor (fn, ...args) {
    // Just use `super()` like any other class
    // You don't need to pass ...args here, but if you used them
    // in the super class, you might want to.
    super(fn, ...args);
    // Just use `this` like any other class. No more messing with fake return values!
    let [constructedPropertyValue, ...rest] = args;
    this.constructedProperty = constructedPropertyValue;
  }
}

// An instance of the extended function that can test both context and arguments
// It would work with arrow functions as well, but that would make testing `this` impossible.
// We pass in CONSTRUCTED_PROPERTY_VALUE just to prove that arguments can be passed
// into the constructor and used as normal
let fn = new ExtendedFunction(function (x) {
  // Add `this.y` to `x`
  // If either value isn't a number, coax it to one, else it's `0`
  return (this.y>>0) + (x>>0)
}, CONSTRUCTED_PROPERTY_VALUE);

// Add an additional property outside of the constructor
// to see if it works as expected
fn.additionalProperty = ADDITIONAL_PROPERTY_VALUE;

// Queue up my tests in a handy array of functions
// All of these should return true if it works
let tests = [
  ()=> fn instanceof Function, // true
  ()=> fn instanceof ExtensibleFunction, // true
  ()=> fn instanceof ExtendedFunction, // true
  ()=> fn.bind() instanceof Function, // true
  ()=> fn.bind() instanceof ExtensibleFunction, // true
  ()=> fn.bind() instanceof ExtendedFunction, // true
  ()=> fn.constructedProperty == CONSTRUCTED_PROPERTY_VALUE, // true
  ()=> fn.additionalProperty == ADDITIONAL_PROPERTY_VALUE, // true
  ()=> fn.constructor == ExtendedFunction, // true
  ()=> fn.constructedProperty == fn.bind().constructedProperty, // true
  ()=> fn.additionalProperty == fn.bind().additionalProperty, // true
  ()=> fn() == 0, // true
  ()=> fn(10) == 10, // true
  ()=> fn.apply({y:10}, [10]) == 20, // true
  ()=> fn.call({y:10}, 20) == 30, // true
  ()=> fn.bind({y:30})(10) == 40, // true
];

// Turn the tests / results into a printable object
let table = tests.map((test)=>(
  {test: test+'', result: test()}
));

// Print the test and result in a fancy table in the console.
// F12 much?
console.table(table);

редактировать

Так как я был в настроении, я решил, что опубликую для этого пакет на npm.


1

Существует простое решение, использующее функциональные возможности JavaScript: передать «логику» в качестве аргумента функции конструктору вашего класса, назначить методы этого класса этой функции, а затем вернуть эту функцию из конструктора в качестве результата. :

class Funk
{
    constructor (f)
    { let proto       = Funk.prototype;
      let methodNames = Object.getOwnPropertyNames (proto);
      methodNames.map (k => f[k] = this[k]);
      return f;
    }

    methodX () {return 3}
}

let myFunk  = new Funk (x => x + 1);
let two     = myFunk(1);         // == 2
let three   = myFunk.methodX();  // == 3

Вышеупомянутое было протестировано на Node.js 8.

Недостатком приведенного выше примера является то, что он не поддерживает методы, унаследованные от суперклассовой цепочки. Чтобы поддержать это, просто замените «Object. GetOwnPropertyNames (...)» чем-то, что также возвращает имена унаследованных методов. Как это сделать, я полагаю, объясняется в другом вопросе-ответе на Stack Overflow :-). Кстати. Было бы неплохо, если бы ES7 добавил метод для создания имен унаследованных методов ;-).

Если вам нужно поддерживать унаследованные методы, одна из возможностей - добавить статический метод к указанному выше классу, который возвращает все унаследованные и локальные имена методов. Затем вызовите это из конструктора. Если вы затем расширите этот класс Funk, вы также получите унаследованный статический метод.


Думаю, этот пример дает простой ответ на исходный вопрос «... как я могу реализовать логику для такого вызова». Просто передайте его конструктору как аргумент со значением функции. В приведенном выше коде класс Funk не расширяет функцию явно, хотя может, но в этом нет необходимости. Как видите, его «экземпляры» можно вызывать выступами, как и любую обычную функцию.
Panu Logic
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.