📜 ⬆️ ⬇️

Listen to function calls in Javascript

Many people know about the mechanism of Event-Dispatcher-Listener'ov, implemented in many programming languages. I will create a similar mechanism not for Event'ov, but for any method of the JavaScript object - Object.
I do not pretend to originality, no. The main purpose of the article is to consider interesting prototyping mechanisms in JavaScript, the creation of decorators and, in fact, try to at least a little to reveal the power and flexibility of this wonderful language that is so often offended and underestimated.

UPD1: a summary:
1. Creating a JavaScript Decorator
2. Creation of the Function call listener mechanism using decorators

UPD2: 06/09/2009
At the end of the article, I added the Update: Decorator Mark II section. In it - corrections and improvements (I didn’t redraw the whole article because of this)
')
So, the first (and hopefully not the last) recipe in my JavaScript cookbook.

Dish


Function Call Listener , stuffed ( multifunctional ), baked in its own juice ( without using any libraries, in pure JavaScript ).

Ingredients


1. Understanding JavaScript Prototyping
2. Understanding JavaScript Anonymous Functions
3. Approximate knowledge of the Function class

Do not worry, I will describe the ingredients in more detail.

Recipe


We dive a couple of meters deep


So let's get started. First, we will borrow a small assistant from John Resig 's (from his book):
Function.prototype.method = function (methodName, f) {
return this .prototype[methodName] = f;
}


* This source code was highlighted with Source Code Highlighter .


What does this method do? Literally: it allows you to easily add a new method to the prototype of the current function.
As you know, a function in JavaScript is also a class and constructor of a class (do not hit, I give up - there are no classes in JavaScript , in the usual sense).

The following is also true: the constructor of any class is a function (or, in a scientific way, an instance of the Function class ).
Based on the principles of prototyping - after adding a new method method(...) to the prototype of the Function class , all instances of the Function class have a new method(...) (but remember: there are no methods in JavaScript, Neo).

Any class - Object , Array , Number , YourClassName - is an instance of the Function class , i.e. just a function. So we got: Object.method(...) , Array.method(...) , YourClassNam.method(...)

If we assume that the function is a familiar class , then we essentially added a static method to all-all classes (JavaScript actually inserts :-)).

We build a decorator


Okay, enough about the genius. We now turn to my code:
Function.method( "decorate" , function (f) {
var oldMe = this ;
var newMe = f;
newMe.old = oldMe;
return newMe;
})


* This source code was highlighted with Source Code Highlighter .

Voila! As I said, the Function class itself is a function, or an instance of the Function class (aaa :-))
So, as soon as we added:
Function.prototype.method = function (...) {}

* This source code was highlighted with Source Code Highlighter .

We immediately appeared Function. method (...) Function. method (...) (no longer in the prototype, but in an instance of the Function class )

After executing the code above, we will have a new method Function.prototype. decorate (...) Function.prototype. decorate (...) . And again, the method appeared in the prototype Function , and therefore in all-all-all classes . But here it’s just not important to me, and what matters is the presence of the decorate(...) method for all functions.

What does the decorate (...) method do?
//
var oldMe = this ;

// , " " .
var newMe = f;
newMe.old = oldMe;
return newMe;


* This source code was highlighted with Source Code Highlighter .

Of course, this code can be greatly reduced, but so - more clearly.

But this is not all, the most interesting - in front. You think I for nothing called my method - decorate? No, not for nothing! This is the familiar decorator .

Decoration example


Let's create a simple class :
// -,
function MyCoolNumber(initNumber) {
this .value = initNumber;
}


* This source code was highlighted with Source Code Highlighter .

But is it a number? Of course not. I can pass there anything.
new MyCoolNumber( ' ' ) // " "

* This source code was highlighted with Source Code Highlighter .

But what to do? I absolutely do not want to change the designer . There is a way out: we will write a decorator to limit the passed parameters and apply it to our class .
function strictArgs() { // ,
var types = arguments; //
return function () {
var params = arguments;
if ( params .length != types.length)
throw "! " + types.length + " (), " + params .length;
for ( var i=0, l= params .length; i < l; i++) {
if (!( params [i] instanceof types[i]) && !( params [i].constructor == types[i]))
throw "! #" + (i+1) + " " + types[i].name;
}
arguments.callee.old.apply( this , arguments); // , ""
}
}


* This source code was highlighted with Source Code Highlighter .

Let's return to our experimental:
function MyCoolNumber(initNumber) {
this .value = initNumber;
}
MyCoolNumber = MyCoolNumber.decorate(strictArgs(Number))

new MyCoolNumber(); // ! 1 (), 0
new MyCoolNumber(1, 2, 3); // ! 1 (), 3
new MyCoolNumber( "" ); // ! #1 Number
var x = new MyCoolNumber(6); // OK!
alert(x.value) // 6, , .


* This source code was highlighted with Source Code Highlighter .

Consider all this ugliness more closely. Let's start with the application of the decorator.
  1. f-tion strictArgs with one argument is called - Number
  2. var types = arguments; // strictArgs(...) types, [Number] - 1 -: Number. Arguments - , ,
  3. strictArgs(...) returns a new strictArgs(...) , inside of which:
    1. var params = arguments; // , , MyCoolNumber;
    2. We begin to compare these 2 arrays for the coincidence of dimensions and types

  4. we decorate the MyCoolNumber that returned from strictArgs(...)
    Note: the connection of the original f-tion with the decorator is done through arguments.callee.old.apply(this, arguments) :
    • arguments - the standard object for describing the arguments of the function being called.
    • arguments.callee - the decorator function itself
    • arguments.callee.old - remember, what is - old? When we pass a decoder function to the decorate (...) method, it adds the old attribute to this function, which refers to the “old” function.
    • arguments.callee.old.apply(...) is the standard method of the Function class. I will not be about him, let me just say that he calls the function with the given scope and arguments
    • arguments.callee.old.apply(this, arguments) - actually, a confirmation of the above


So, it seems, I focused on the main points.
Oh yeah, I forgot! How to "throw off" the decorator from the function and return the old one? There is nothing easier:
Function.method( "recover" , function () {
return this .old || this ;
})


* This source code was highlighted with Source Code Highlighter .

Now let's continue!

We look at the object, listen to the methods


We smoothly come to the end of my recipe. Just a little bit of spice, and you can go into the furnace ... ugh, into the oven! :)
Object.method( 'before' , function (methodName, f){
var method = listenerInit.call( this , methodName);
if (method)
method.listenersBefore.push(f);
})

Object.method( 'after' , function (methodName, f){
var method = listenerInit.call( this , methodName);
if (method)
method.listenersAfter.push(f);
})


* This source code was highlighted with Source Code Highlighter .

As you might guess, all the most important things happen inside a certain function listenerInit(...) , but about it later. For now, just believe that she makes all the necessary preparations.
How to add a listener is understandable. Now we need the ability to "remove":
Object.method( 'removeBefore' , function (methodName, f){
var method = listenerInit.call( this , methodName);
if (method) {
var _nl = [];
while (method.listenersBefore.length) {
var _f = method.listenersBefore.shift();
if (_f != f)
_nl.push(_f);
}
method.listenersBefore = _nl;
}
})

Object.method( 'removeAfter' , function (methodName, f){
var method = listenerInit.call( this , methodName);
if (method) {
var _nl = [];
while (method.listenersAfter.length) {
var _f = method.listenersAfter.shift();
if (_f != f)
_nl.push(_f);
}
method.listenersAfter = _nl;
}
})


* This source code was highlighted with Source Code Highlighter .

Maybe this method is not optimal, I just took the first out of my head.
Finally, the crown of this recipe, the connection of the decorator and the event-listener of the schema is the same function as listenerInit :
function listenerInit(methodName) {

var method = this [methodName];
if ( typeof method != "function" )
return false ;

// , ?
if (!method.listenable) {
this [methodName] = method.decorate( function (){
var decorator = arguments.callee;
decorator.listenable = true ;

var list = decorator.listenersBefore;
for ( var i = 0, l = list.length; i < l; i++) {
if ( typeof list[i] == "function" && list[i].apply( this , arguments) === false )
return ;
}

var ret = decorator.old.apply( this , arguments);
list = decorator.listenersAfter;
for ( var i = 0, l = list.length; i < l; i++)
list[i].apply( this , arguments);

return ret;
});
method = this [methodName];
}

method.listenersBefore = method.listenersBefore instanceof Array ? method.listenersBefore : [];
method.listenersAfter = method.listenersAfter instanceof Array ? method.listenersAfter : [];

return method;
}


* This source code was highlighted with Source Code Highlighter .

This function can be divided into blocks.

Block 1: check - do they not deceive us? Is there such a method in this object?
var method = this [methodName];
if ( typeof method != "function" )
return false ;


* This source code was highlighted with Source Code Highlighter .

And at the end we see: the return method , i.e. listenerInit(...) returns either false or the already “decorated” method.

Block 2: creating the appropriate decorator, if one is not yet defined.
What about this decorator?
  1. We start all listeners from the listenersBefore array. If at least 1 of them returns a Boolean false , stop execution.
  2. Calling the base method
  3. We start all listeners from the listenersAfter array
  4. The decorator returns the value returned by the base method.

Block 3: initialization of the arrays method.listenersBefore and method.listenersAfter .

Buns


Modification 1: hide the listenerInit out of sight. To do this, use the JavaScript closure:
( function (){
// listenerInit(...) --
// .....
})()


* This source code was highlighted with Source Code Highlighter .

Modification 2: contaminating standard classes like Object is very bad, so you can modify your particular class:
YourClass.method( 'before' , function (methodName, f){
var method = listenerInit.call( this , methodName);
if (method)
method.listenersBefore.push(f);
})


* This source code was highlighted with Source Code Highlighter .

Well, and so on. As they say - there is no limit to perfection!

Everything! The cake is molded, now in the oven (that is, in your brain) for half an hour, and - ready. Enjoy your meal!

How it is


Well, a specific example of use:
//
var Num = function (x) {
this .x = x;
}
//
Num.prototype.x = null ;
Num.prototype.getX = function () {
return this .x;
};
Num.prototype.setX = function (x) {
return this .x = x;
}

//
var t = new Num(6);

// after
t.after( "getX" , function (){
alert( '! X == ' + this .x + '!' );
})

// after
t.after( "getX" , function (){
alert( ' !' );
})

// before,
var f = function (x){
if (x < 0 || x > 10) {
alert( '! [0, 10]' );
return false ;
}
}
t.before( "setX" , f)

// :
t.getX(); // ! X == 6! -> ' ! -> getX(...)
t.setX(100); // ! [0, 10] -> setX(100) -
alert(tx); // 6
t.setX(4); // , setX(4)
alert(tx); // 4
t.removeBefore( "setX" , f) // f(...)
t.setX(100); // , setX(100)
alert(tx); // 100


* This source code was highlighted with Source Code Highlighter .

By the way, all the above code is cross-browser, this is my principle of operation.

Conclusion


I hope you enjoyed my cooking. Write, I will be glad to answer questions. And believe me - JavaScript is really a very powerful, flexible and very cool programming language, albeit a script one.

Update: Decorator Mark II


UPD: 06/09/2009
Elapsed time since writing the article and I found in my implementation errors and shortcomings. But I do not want to redraw the article, and do not want to create a new one. So meet here and now: Decorator Mark II!

1. Improved Function.method(...)
// ,
Function.prototype.method = function (methodName, f) {
if ( typeof f != "undefined" )
this .prototype[methodName] = f;
return this .prototype[methodName];
}


* This source code was highlighted with Source Code Highlighter .


2. Improved restore(...) method
// "" -,
Function.method( "restore" , function (fullRestore){
var ret = this .old || this ;
while (fullRestore && ret.old) {
ret = ret.old;
}
return ret;
})


* This source code was highlighted with Source Code Highlighter .


3. Added decorateMethod(...) method
//
Function.method( "decorateMethod" , function (methodName, decorator){
var f = this .method(methodName);
if (!f)
return null ;
f.name = methodName;
f = f.decorate(decorator);
return this .method(methodName, f);
})


* This source code was highlighted with Source Code Highlighter .


4. ( important! ) Fixed / changed method decorate(...)
Function.method( "decorate" , function (decorator){
//
var oldFunc = this ;
// ! - .
// , -.
// ,
// -- original: oldFunc () decoratorInstance: f ( )
var f = function (){
return decorator.apply( this , [{original: oldFunc, decoratorInstance: f}].concat([].slice.apply(arguments)))
}
// - - decoratorInstance f
f.old = oldFunc;
// -.
// - .
f.prototype = this .prototype;
f.prototype.constructor = f;
// -. ? .
// . , decorateMethod: .
f.name = oldFunc.name;
//
return f;
})


* This source code was highlighted with Source Code Highlighter .

( decorate(...) ), .
:
// -
function strictArgs() {
var types = arguments;
return function () {
var params = arguments;
//...
return arguments.callee.old.apply( this , arguments);
}
}

* This source code was highlighted with Source Code Highlighter .

:
// -
function strictArgs() {
var types = arguments;
return function (dScope) {
var original = arguments[0].original; // dScope.original
var arguments = Array.prototype.slice.call(arguments, 1);
var params = arguments;
//...
return original.apply( this , arguments);
}
}

* This source code was highlighted with Source Code Highlighter .


, :)

decorate(...)
Function.method( "decorate" , function (decorator){
//
var oldFunc = this ;
// ! - .
// , -.
// ,
// -- original: oldFunc () decoratorInstance: f ( )
var f = function (){
return decorator.apply( this , [{original: oldFunc, decoratorInstance: f}].concat([].slice.apply(arguments)))
}
// - - decoratorInstance f
f.old = oldFunc;
// -.
// - .
f.prototype = this .prototype;
f.prototype.constructor = f;
// -. ? .
// . , decorateMethod: .
f.name = oldFunc.name;
//
return f;
})


* This source code was highlighted with Source Code Highlighter .

( decorate(...) ), .
:
// -
function strictArgs() {
var types = arguments;
return function () {
var params = arguments;
//...
return arguments.callee.old.apply( this , arguments);
}
}

* This source code was highlighted with Source Code Highlighter .

:
// -
function strictArgs() {
var types = arguments;
return function (dScope) {
var original = arguments[0].original; // dScope.original
var arguments = Array.prototype.slice.call(arguments, 1);
var params = arguments;
//...
return original.apply( this , arguments);
}
}

* This source code was highlighted with Source Code Highlighter .


, :)

decorate(...)
Function.method( "decorate" , function (decorator){
//
var oldFunc = this ;
// ! - .
// , -.
// ,
// -- original: oldFunc () decoratorInstance: f ( )
var f = function (){
return decorator.apply( this , [{original: oldFunc, decoratorInstance: f}].concat([].slice.apply(arguments)))
}
// - - decoratorInstance f
f.old = oldFunc;
// -.
// - .
f.prototype = this .prototype;
f.prototype.constructor = f;
// -. ? .
// . , decorateMethod: .
f.name = oldFunc.name;
//
return f;
})


* This source code was highlighted with Source Code Highlighter .


( decorate(...) ), .
:
// -
function strictArgs() {
var types = arguments;
return function () {
var params = arguments;
//...
return arguments.callee.old.apply( this , arguments);
}
}

* This source code was highlighted with Source Code Highlighter .

:
// -
function strictArgs() {
var types = arguments;
return function (dScope) {
var original = arguments[0].original; // dScope.original
var arguments = Array.prototype.slice.call(arguments, 1);
var params = arguments;
//...
return original.apply( this , arguments);
}
}

* This source code was highlighted with Source Code Highlighter .


, :)

decorate(...)
Function.method( "decorate" , function (decorator){
//
var oldFunc = this ;
// ! - .
// , -.
// ,
// -- original: oldFunc () decoratorInstance: f ( )
var f = function (){
return decorator.apply( this , [{original: oldFunc, decoratorInstance: f}].concat([].slice.apply(arguments)))
}
// - - decoratorInstance f
f.old = oldFunc;
// -.
// - .
f.prototype = this .prototype;
f.prototype.constructor = f;
// -. ? .
// . , decorateMethod: .
f.name = oldFunc.name;
//
return f;
})


* This source code was highlighted with Source Code Highlighter .


( decorate(...) ), .
:
// -
function strictArgs() {
var types = arguments;
return function () {
var params = arguments;
//...
return arguments.callee.old.apply( this , arguments);
}
}

* This source code was highlighted with Source Code Highlighter .
:
// -
function strictArgs() {
var types = arguments;
return function (dScope) {
var original = arguments[0].original; // dScope.original
var arguments = Array.prototype.slice.call(arguments, 1);
var params = arguments;
//...
return original.apply( this , arguments);
}
}

* This source code was highlighted with Source Code Highlighter .


, :)

decorate(...)
Function.method( "decorate" , function (decorator){
//
var oldFunc = this ;
// ! - .
// , -.
// ,
// -- original: oldFunc () decoratorInstance: f ( )
var f = function (){
return decorator.apply( this , [{original: oldFunc, decoratorInstance: f}].concat([].slice.apply(arguments)))
}
// - - decoratorInstance f
f.old = oldFunc;
// -.
// - .
f.prototype = this .prototype;
f.prototype.constructor = f;
// -. ? .
// . , decorateMethod: .
f.name = oldFunc.name;
//
return f;
})


* This source code was highlighted with Source Code Highlighter .


( decorate(...) ), .
:
// -
function strictArgs() {
var types = arguments;
return function () {
var params = arguments;
//...
return arguments.callee.old.apply( this , arguments);
}
}

* This source code was highlighted with Source Code Highlighter .

:
// -
function strictArgs() {
var types = arguments;
return function (dScope) {
var original = arguments[0].original; // dScope.original
var arguments = Array.prototype.slice.call(arguments, 1);
var params = arguments;
//...
return original.apply( this , arguments);
}
}

* This source code was highlighted with Source Code Highlighter .


, :)

decorate(...)
Function.method( "decorate" , function (decorator){
//
var oldFunc = this ;
// ! - .
// , -.
// ,
// -- original: oldFunc () decoratorInstance: f ( )
var f = function (){
return decorator.apply( this , [{original: oldFunc, decoratorInstance: f}].concat([].slice.apply(arguments)))
}
// - - decoratorInstance f
f.old = oldFunc;
// -.
// - .
f.prototype = this .prototype;
f.prototype.constructor = f;
// -. ? .
// . , decorateMethod: .
f.name = oldFunc.name;
//
return f;
})


* This source code was highlighted with Source Code Highlighter .


( decorate(...) ), .
:
// -
function strictArgs() {
var types = arguments;
return function () {
var params = arguments;
//...
return arguments.callee.old.apply( this , arguments);
}
}

* This source code was highlighted with Source Code Highlighter .

:
// -
function strictArgs() {
var types = arguments;
return function (dScope) {
var original = arguments[0].original; // dScope.original
var arguments = Array.prototype.slice.call(arguments, 1);
var params = arguments;
//...
return original.apply( this , arguments);
}
}

* This source code was highlighted with Source Code Highlighter .

, :)

decorate(...)
Function.method( "decorate" , function (decorator){
//
var oldFunc = this ;
// ! - .
// , -.
// ,
// -- original: oldFunc () decoratorInstance: f ( )
var f = function (){
return decorator.apply( this , [{original: oldFunc, decoratorInstance: f}].concat([].slice.apply(arguments)))
}
// - - decoratorInstance f
f.old = oldFunc;
// -.
// - .
f.prototype = this .prototype;
f.prototype.constructor = f;
// -. ? .
// . , decorateMethod: .
f.name = oldFunc.name;
//
return f;
})


* This source code was highlighted with Source Code Highlighter .


( decorate(...) ), .
:
// -
function strictArgs() {
var types = arguments;
return function () {
var params = arguments;
//...
return arguments.callee.old.apply( this , arguments);
}
}

* This source code was highlighted with Source Code Highlighter .

:
// -
function strictArgs() {
var types = arguments;
return function (dScope) {
var original = arguments[0].original; // dScope.original
var arguments = Array.prototype.slice.call(arguments, 1);
var params = arguments;
//...
return original.apply( this , arguments);
}
}

* This source code was highlighted with Source Code Highlighter .


, :)

Source: https://habr.com/ru/post/59761/


All Articles