понедельник, 29 сентября 2014 г.

JavaScript Hacks, Tips and Tricks

There are several JavaScript tricks that are used widely by experienced programmers. Many of them may not be instantly obvious, especially for beginners. These tricks use language features not by their direct purpose, but rather by their side-effect to achieve goals, that can’t be achieved by default language means. Here I made a little compilation of such tricks with explanation.

You should understand that most of these tricks are rather hacks and not something you should use in your daily development. The purpose of that article is to explain how they work, not to push to use them.

Using !! to convert value into a boolean

Everything in JavaScript can be interpreted either as truthy or falsy. This means that when you put object into an if expression, it will either let you go by true-branch (i.e. it’s ‘truthy’) of by false branch(‘falsy’).

0, false, "", null, undefined, NaN are all falsy, every other object is truthy. Sometimes you’d like to convert an object to a plain boolean value. To do this you can use double negation !!.

Another side of that coin is that instead of if (x == "test") you can simply write if (x). In case x is empty (hence falsy) it will run else block.

Converting string into a number with +str

In JavaScript + is a unary operator that returns a numeric representation of operand or NaN if not applicable. Sometimes you can enforce code (see underscorejs sources) such as x === +x, which is thus just checking whether x is numeric.

This one is not obvious at all. Normally you’d want to use parseFloat and parseInt(x, 10) for parsing numbers.

Providing default value with ||

In JavaScript || is an example of short-circuit evaluation, which is also commonly used in some other language. This operator will firstly evaluate expression on the left side, and then, if falsy, will proceed with on the right. In any case it will return first non-falsy result. Consider the following example

function setAge(age) {
  this.age = age || 10
}

setAge();

We didn’t provide age, thus age || 10 will return 10, which is a nice way to provide some defaults values. In fact this is equivalent to

var x;
if (age) {
   this.age = age;
} else {
   this.age = 10;
}

The former is obviously more succinct and that’s why you will see it used everywhere.

Personally, I use that pattern a lot. I like its conciseness and clarity. Notice, however, that since 0 is falsy you won’t have an ability to set age to 0. Therefore, it might be a better (but slightly more verbose) solution to use something like this:

this.age = (typeof age !== "undefined") ? age : 10;

Using void 0 instead of undefined

Keyword void takes one argument and always return undefined. Why not to simply use undefined? Because in some browsers undefined is just a variable that can be reassigned, and the former gives us a higher level of confidence. Although you can find this being used in source code of some libraries, I would not recommend using it on the regular basis, since all EC5-compliant browsers don’t allow to rewrite undefined.

Encapsulation with (function() {...})() pattern

You’ll want to wrap your code into an anonymous function and then immediately call it, when you want some encapsulation. There are only two types of scopes in JavaScript (but look at ECMA6 block scopes): global scope and function scope. Everything you write goes into global scope, which is accessible from everywhere. This includes vars and function declarations. Normally you’d want to encapsulate most of the code somewhere inside of a function and expose to global scope only interface. And that’s where this pattern gets really handy. Consider the following:

(function() {
  function div(a, b) {
    return a / b;
  }

  function divBy5(x) {
    return div(x, 5);
  }

  window.divBy5 = divBy5;
})()

div // => undefined
divBy5(10); // => 2

Among other hacks listed in this article this one is really harmless and you can and should use it in your code to prevent exposing some inner logic into the global scope.

In conclusion, I’d like to remind you that any code you write should be simple and clear to other programmers. And any natural constructions provided by language should be preferred to artificial ones.

Some of the problems, listed in that article are being solved in elegant way by ES6 standart (next version of JavaScript). For instance, you’ll probably won’t need the obscure age = age || 10 pattern in the future, since ES6 allows you to write default arguments in a better way:

function(age = 10) {
   ...
}

Another example, is (function() {...})() pattern, which you can leave in the past after EC6 modules get implemented by modern browsers.

Append an array to another array

var a = [4,5,6];
var b = [7,8,9];
Array.prototype.push.apply(a, b);

uneval(a); // is: [4, 5, 6, 7, 8, 9]

Milliseconds since epoch

+new Date() // 1259359833574

Simulate threads using yield operator
JavaScript 1.7

//// thread definition
function Thread( name ) {

    for ( var i = 0; i < 5; i++ ) {

        Print(name+': '+i);
        yield;
    }
}

//// thread management
var threads = [];

// thread creation
threads.push( new Thread('foo') );
threads.push( new Thread('bar') );

// scheduler
while (threads.length) {

    var thread = threads.shift();
    try {
        thread.next();
        threads.push(thread);
    } catch(ex if ex instanceof StopIteration) {}
}
prints:

foo: 0
bar: 0
foo: 1
bar: 1
foo: 2
bar: 2
foo: 3
bar: 3
foo: 4
bar: 4

prefix an integer with zeros

function PrefixInteger(num, length) {

    return (num / Math.pow(10, length)).toFixed(length).substr(2);
}

And more efficient:

function PrefixInteger(num, length) {

  return ( "0000000000000000" + num ).substr( -length );
}

Thanks to fritzthe...

And even better:

function PrefixInteger(num, length) {

  return (Array(length).join('0') + num).slice(-length);
}
Thanks to tobeytai...

shuffle the Array

JavaScript 1.?

var list = [1,2,3,4,5,6,7,8,9];

list = list.sort(function() Math.random() - 0.5);

Print(list); // prints something like: 4,3,1,2,9,5,6,7,8

multi-line text
JavaScript 1.6

var text = <>
this
is
my
multi-line
text
</>.toString();
Print(text);
prints:

this
is
my
multi-line
text
note
If you want to support special XML chars, you can use a CDATA section:

<><![CDATA[

>>> hello

]]></>.toString();

Escape and unescape HTML entities

const entityToCode = { __proto__: null,
apos:0x0027,quot:0x0022,amp:0x0026,lt:0x003C,gt:0x003E,nbsp:0x00A0,iexcl:0x00A1,cent:0x00A2,pound:0x00A3,
curren:0x00A4,yen:0x00A5,brvbar:0x00A6,sect:0x00A7,uml:0x00A8,copy:0x00A9,ordf:0x00AA,laquo:0x00AB,
not:0x00AC,shy:0x00AD,reg:0x00AE,macr:0x00AF,deg:0x00B0,plusmn:0x00B1,sup2:0x00B2,sup3:0x00B3,
acute:0x00B4,micro:0x00B5,para:0x00B6,middot:0x00B7,cedil:0x00B8,sup1:0x00B9,ordm:0x00BA,raquo:0x00BB,
frac14:0x00BC,frac12:0x00BD,frac34:0x00BE,iquest:0x00BF,Agrave:0x00C0,Aacute:0x00C1,Acirc:0x00C2,Atilde:0x00C3,
Auml:0x00C4,Aring:0x00C5,AElig:0x00C6,Ccedil:0x00C7,Egrave:0x00C8,Eacute:0x00C9,Ecirc:0x00CA,Euml:0x00CB,
Igrave:0x00CC,Iacute:0x00CD,Icirc:0x00CE,Iuml:0x00CF,ETH:0x00D0,Ntilde:0x00D1,Ograve:0x00D2,Oacute:0x00D3,
Ocirc:0x00D4,Otilde:0x00D5,Ouml:0x00D6,times:0x00D7,Oslash:0x00D8,Ugrave:0x00D9,Uacute:0x00DA,Ucirc:0x00DB,
Uuml:0x00DC,Yacute:0x00DD,THORN:0x00DE,szlig:0x00DF,agrave:0x00E0,aacute:0x00E1,acirc:0x00E2,atilde:0x00E3,
auml:0x00E4,aring:0x00E5,aelig:0x00E6,ccedil:0x00E7,egrave:0x00E8,eacute:0x00E9,ecirc:0x00EA,euml:0x00EB,
igrave:0x00EC,iacute:0x00ED,icirc:0x00EE,iuml:0x00EF,eth:0x00F0,ntilde:0x00F1,ograve:0x00F2,oacute:0x00F3,
ocirc:0x00F4,otilde:0x00F5,ouml:0x00F6,divide:0x00F7,oslash:0x00F8,ugrave:0x00F9,uacute:0x00FA,ucirc:0x00FB,
uuml:0x00FC,yacute:0x00FD,thorn:0x00FE,yuml:0x00FF,OElig:0x0152,oelig:0x0153,Scaron:0x0160,scaron:0x0161,
Yuml:0x0178,fnof:0x0192,circ:0x02C6,tilde:0x02DC,Alpha:0x0391,Beta:0x0392,Gamma:0x0393,Delta:0x0394,
Epsilon:0x0395,Zeta:0x0396,Eta:0x0397,Theta:0x0398,Iota:0x0399,Kappa:0x039A,Lambda:0x039B,Mu:0x039C,
Nu:0x039D,Xi:0x039E,Omicron:0x039F,Pi:0x03A0,Rho:0x03A1,Sigma:0x03A3,Tau:0x03A4,Upsilon:0x03A5,
Phi:0x03A6,Chi:0x03A7,Psi:0x03A8,Omega:0x03A9,alpha:0x03B1,beta:0x03B2,gamma:0x03B3,delta:0x03B4,
epsilon:0x03B5,zeta:0x03B6,eta:0x03B7,theta:0x03B8,iota:0x03B9,kappa:0x03BA,lambda:0x03BB,mu:0x03BC,
nu:0x03BD,xi:0x03BE,omicron:0x03BF,pi:0x03C0,rho:0x03C1,sigmaf:0x03C2,sigma:0x03C3,tau:0x03C4,
upsilon:0x03C5,phi:0x03C6,chi:0x03C7,psi:0x03C8,omega:0x03C9,thetasym:0x03D1,upsih:0x03D2,piv:0x03D6,
ensp:0x2002,emsp:0x2003,thinsp:0x2009,zwnj:0x200C,zwj:0x200D,lrm:0x200E,rlm:0x200F,ndash:0x2013,
mdash:0x2014,lsquo:0x2018,rsquo:0x2019,sbquo:0x201A,ldquo:0x201C,rdquo:0x201D,bdquo:0x201E,dagger:0x2020,
Dagger:0x2021,bull:0x2022,hellip:0x2026,permil:0x2030,prime:0x2032,Prime:0x2033,lsaquo:0x2039,rsaquo:0x203A,
oline:0x203E,frasl:0x2044,euro:0x20AC,image:0x2111,weierp:0x2118,real:0x211C,trade:0x2122,alefsym:0x2135,
larr:0x2190,uarr:0x2191,rarr:0x2192,darr:0x2193,harr:0x2194,crarr:0x21B5,lArr:0x21D0,uArr:0x21D1,
rArr:0x21D2,dArr:0x21D3,hArr:0x21D4,forall:0x2200,part:0x2202,exist:0x2203,empty:0x2205,nabla:0x2207,
isin:0x2208,notin:0x2209,ni:0x220B,prod:0x220F,sum:0x2211,minus:0x2212,lowast:0x2217,radic:0x221A,
prop:0x221D,infin:0x221E,ang:0x2220,and:0x2227,or:0x2228,cap:0x2229,cup:0x222A,int:0x222B,
there4:0x2234,sim:0x223C,cong:0x2245,asymp:0x2248,ne:0x2260,equiv:0x2261,le:0x2264,ge:0x2265,
sub:0x2282,sup:0x2283,nsub:0x2284,sube:0x2286,supe:0x2287,oplus:0x2295,otimes:0x2297,perp:0x22A5,
sdot:0x22C5,lceil:0x2308,rceil:0x2309,lfloor:0x230A,rfloor:0x230B,lang:0x2329,rang:0x232A,loz:0x25CA,
spades:0x2660,clubs:0x2663,hearts:0x2665,diams:0x2666
};

var charToEntity = {};
for ( var entityName in entityToCode )
        charToEntity[String.fromCharCode(entityToCode[entityName])] = entityName;

function UnescapeEntities(str) str.replace(/&(.+?);/g, function(str, ent) String.fromCharCode( ent[0]!='#' ? entityToCode[ent] : ent[1]=='x' ? parseInt(ent.substr(2),16): parseInt(ent.substr(1)) ) );

function EscapeEntities(str) str.replace(/[^\x20-\x7E]/g, function(str) charToEntity[str] ? '&'+charToEntity[str]+';' : str );

Remove an object from an array

JavaScript 1.8

function RemoveArrayElement( array, element ) !!let (pos=array.lastIndexOf(element)) pos != -1 && array.splice(pos, 1);

Creates a random alphabetic string

function RandomString(length) {
 
    var str = '';
    for ( ; str.length < length; str += Math.random().toString(36).substr(2) );
    return str.substr(0, length);
}

Brainfuck interpreter

var code = '++++++++++[>+++++++>++++++++++>+++>+<<<<-]>++.>+.+++++++..+++.>++.<<+++++++++++++++.>.+++.------.--------.>+.>.';
var inp = '23\n';
var out = '';

var codeSize = code.length;
var i = 0, ip = 0, cp = 0, dp = 0, m = {};

var loopIn = {}, loopOut = {};
var tmp = [];
for ( var cp = 0; cp < codeSize ; cp++ )
    if ( code[cp] == '[' )
        tmp.push(cp);
    else
        if ( code[cp] == ']' )
            loopOut[loopIn[cp] = tmp.pop()] = cp;

for (var cp = 0; cp < codeSize && i < 100000; cp++, i++) {

    switch(code[cp]) {

        case '>': dp++; break;
        case '<': dp--; break;
        case '+': m[dp] = ((m[dp]||0)+1)&255; break
        case '-': m[dp] = ((m[dp]||0)-1)&255; break;
        case '.': out += String.fromCharCode(m[dp]); break;
        case ',': m[dp] = inp.charCodeAt(ip++)||0; break;
        case '[': m[dp]||(cp=loopOut[cp]); break;
        case ']': cp = loopIn[cp]-1; break;
    }
}
Print(out);
This Brainfuck program just prints 'Hello World!'

Optional named function arguments

function foo({ name:name, project:project}) {

    Print( project );
    Print( name );
}

foo({ name:'soubok', project:'jslibs' })
foo({ project:'jslibs', name:'soubok'})

String converter

JavaScript 1.8

function CreateTranslator(translationTable) function(s) s.replace(new RegExp([k for (k in translationTable)].join('|'), 'g'), function(str) translationTable[str]);
exemple of use:

var translationTable = { a:1, bb:2, b:3, c:4 };
var MyTranslater = CreateTranslator( translationTable );
MyTranslater('aabbbc'); // returns: 11234

Display the current call stack

function Stack() { try { throw Error() } catch(ex) { return ex.stack } }

print( Stack() );
prints:

Error()@:0
Stack()@test.js:1
@test.js:3

Change the primitive value of an object with valueOf

function foo() {

   this.valueOf = function() {

     return 'this is my value';
   }
}

var bar = new foo();

Print( bar ); // prints: this is my value

Print( bar == 'this is my value' ) // prints: true

Print( bar === 'this is my value' ) // prints: false

Transform the arguments object into an array

JavaScript 1.6

function foo() {

  var argArray = Array.slice(arguments); // is ['aa',11]
}

foo('aa',11);
use also

var argArray = Array.prototype.slice.call(arguments);

Convert a string into a charcode list

Method 1: JavaScript 1.6

Array.map('foo', function(x) { return String.charCodeAt(x) }) // is [112,111,111]
Method 2: JavaScript 1.7

[ String.charCodeAt(x) for each ( x in 'foo' ) ] // is [112,111,111]

Array iteration pitfall

var foo = [3,4,5];

for ( var i in foo ) {

    if ( i == 1 ) {

        foo.unshift(6);
    }
    Print('item: '+foo[i])
}
Print( 'array: '+foo.toSource() )
output:

item: 3
item: 3
item: 4
array: [6, 3, 4, 5]

exceptions for non-fatal errors

JavaScript 1.7

function ERR() { throw ERR }
function CHK( v ) { return v || ERR() }

try {
     
  var data1 = 'a/b/c';
  var arr1 = CHK(data1).split('/');

  var data2 = '';
  var arr2 = CHK(data2).split('/'); // the exception is throw here

} catch(ex if ex == ERR) {

  Print('non fatal error while decoding the data')
}
prints:

a b c

A Switch function

JavaScript 1.8

function Switch(i) arguments[++i];
usage:

Print( Switch(2,'aa','bb','cc','dd') ); // prints: cc
Print( Switch(100,'aa','bb','cc','dd') ); // prints: undefined

A Match function

JavaScript 1.8

function Match(v) Array.indexOf(arguments,v,1)-1;
usage:

Print( Match('aa', '0', 'b', 123, 'aa', 8.999 ) ); // prints: 3
Print( Match('Z', 'b', 123, 'aa', 8.999 ) ); // prints: -2 (< 0 is not found)

new object override

JavaScript 1.5

function foo() {

  return new Array(5,6,7);
}

var bar = new foo();
bar.length // is 3

Kind of destructuring assignments

JavaScript 1.7

 var { a:x, b:y } = { a:7, b:8 };
 Print(x); // prints: 7
 Print(y); // prints: 8

Generator Expressions

JavaScript 1.7

[ y for ( y in [5,6,7,8,9] ) ] // is [0,1,2,3,4]
and

[ y for each ( y in [5,6,7,8,9] ) ] // is [5,6,7,8,9]
Because in for extracts index names, and for each extracts the values.

Advanced use of iterators

JavaScript 1.7

Number.prototype.__iterator__ = function() {

 for ( let i = 0; i < this; i++ )
  yield i;
};

for ( let i in 5 )
 print(i);
prints:

1
2
3
4
5
This make Number object to act as a generator.

Expression Closures
JavaScript 1.8

function(x) x * x;
Note that braces {...} and return are implicit

Function declaration and expression
Function declaration:

bar(); // prints: bar
function bar() {

   Print('bar');
}

function foo() {

   Print('foo');
}
foo(); // prints: foo
Function expression:

(function foo() {
  Print( foo.name );
})(); // prints: foo
foo(); // rise a ReferenceError

!function foo() {
}
foo(); // rise a ReferenceError

var bar = function foo() {
}
foo(); // rise a ReferenceError

Factory method pattern

Complex = new function() {

        function Complex(a, b) {
                // ...
        }

        this.fromCartesian = function(real, mag) {

                return new Complex(real, imag);
        }

        this.fromPolar = function(rho, theta) {

                return new Complex(rho * Math.cos(theta), rho * Math.sin(theta));
        }
}


var c = Complex.fromPolar(1, Math.pi); // Same as fromCartesian(-1, 0);
see factory pattern on wikipedia

Closures by example
JavaScript 1.5

function CreateAdder( add ) {

  return function( value ) {

    return value + add;
  }
}
usage:

var myAdder5 = CreateAdder( 5 );
var myAdder6 = CreateAdder( 6 );

Print( myAdder5( 2 ) ); // prints 7
Print( myAdder6( 4 ) ); // prints 10
Further information about Nested functions and closures

SyntaxError

JavaScript 1.5

raised when a syntax error occurs while parsing code in eval()

try {

  eval('1 + * 5'); // will rise a SyntaxError exception
     
} catch( ex ) {

        Print( ex.constructor == SyntaxError ); // Prints true
}
JavaScript 1.7

try {

 eval('1 + * 5');

} catch( ex if ex instanceof SyntaxError  ) {

 Print( 'SyntaxError !' ); // prints: SyntaxError !
}

ReferenceError

raised when de-referencing an invalid reference.

try {

 fooBar(); // will rise a ReferenceError exception

} catch( ex ) {

 Print( ex.constructor == ReferenceError ); // Prints true
}
JavaScript 1.7

try {

 fooBar();

} catch( ex if ex instanceof ReferenceError ) {

 Print( 'ReferenceError !' ); // prints: ReferenceError !
}

JavaScript Minifier / comment remover
jslibs

var script = new Script('var a; /* this is a variable */ var b; // another variable');
Print( script.toString() );
prints:

var a;
var b;
javascript

function foo() {

 var a; // a variable
 var b = [1, 2, 3]; // an array
 var c = {x: {y: 1}};

 function bar() { // my function

  return 1 /* 2 */;
 }
}

Print( foo.toSource() );

JavaScript code beautifier (un-minifier)

function foo() {var a;var b=[1,2,3];var c={x:{y:1}};function bar(){return 1}}
Print( foo.toSource(1) );
prints:

function foo() {
 var a;
 var b = [1, 2, 3];
 var c = {x: {y: 1}};

 function bar() {
  return 1;
 }
}

Auto indent JavaScript code / unobfuscator
Spidermonkey JavaScript engine only

function foo() { function bar(){};var i=0;for(;i<10;++i) bar(i) }
Print(foo.toSource(2));
prints:

function foo() {

      function bar() {
      }

      var i = 0;
      for (; i < 10; ++i) {
          bar(i);
      }
  }

Objects, constructor and instanceof
JavaScript 1.5

function Foo() {
  // Foo class
}

var a = new Foo();


Bar.prototype = new Foo();
function Bar() {
  // Bar class
}

var b = new Bar();


Print( a.constructor == Foo ); // true
Print( a instanceof Foo ); // true

Print( b.constructor == Foo ); // true
Print( b instanceof Foo ); // true

Print( b.constructor == Bar ); // false !
Print( b instanceof Bar ); // true

Objects private and public members

function MyConstructor( pub, priv ) {

  var privateVariable = priv;
  this.publicVariable = pub;

  this.SetThePrivateVariable = function( value ) {

    privateVariable = value;
  }

  this.GetThePrivateVariable = function() {

    return privateVariable;
  }

  this.PrintAllVariables = function() {

    Print( privateVariable + ',' + this.publicVariable );
  }
}

var myObject = new MyConstructor( 123, 456 );

Print( myObject.privateVariable ); // prints: undefined
Print( myObject.publicVariable ); // prints: 123
myObject.PrintAllVariables(); // prints 456,123
myObject.SetThePrivateVariable( 789 );
myObject.PrintAllVariables(); // prints 789,123

Stack data structure
JavaScript 1.5 Array object has all needed methods to be used as a stack.

  var stack = [];
  stack.push( 111 );
  stack.push( 2.22 );
  stack.push( 'ccc' );

  Print( stack ); // prints: 111,2.22,ccc

  Print( stack.pop() ); // prints: ccc
  Print( stack.pop() ); // prints: 2.22

Singleton pattern
JavaScript 1.5 The singleton pattern is a design pattern that is used to restrict instantiation of a class to one object. This is useful when exactly one object is needed to coordinate actions across the system.

function MySingletonClass() {

  if ( arguments.callee._singletonInstance )
    return arguments.callee._singletonInstance;
  arguments.callee._singletonInstance = this;

  this.Foo = function() {
    // ...
  }
}

var a = new MySingletonClass()
var b = MySingletonClass()
Print( a === b ); // prints: true

Use Functions as an Object
JavaScript 1.5

function Counter() {
 
   if ( !arguments.callee.count ) {
 
        arguments.callee.count = 0;
    }
    return arguments.callee.count++;
}
Print( Counter() ); // prints: 0
Print( Counter() ); // prints: 1
Print( Counter() ); // prints: 2

E4X add nodes
JavaScript 1.6

var x = <store/>;
x.* += <item price="10" />;
x.* += <item price="15" />;
Print( x.toXMLString() );
prints:

<store>
  <item price="10"/>
  <item price="15"/>
</store>

E4X dynamic document creation
JavaScript 1.6

var html = <html/>;
html.head.title = "My Page Title";
html.body.@bgcolor = "#e4e4e4";
html.body.form.@name = "myform";
html.body.form.@action = "someurl.jss";
html.body.form.@method = "post";
html.body.form.@onclick = "return somejs();";
html.body.form.input[0] = "";
html.body.form.input[0].@name = "test";
Print(html.toXMLString());
prints:

<html>
  <head>
    <title>My Page Title</title>
  </head>
  <body bgcolor="#e4e4e4">
    <form name="myform" action="someurl.jss" method="post" onclick="return somejs();">
      <input name="test"></input>
    </form>
  </body>
</html>

E4X dynamic tag name
JavaScript 1.6

var nodeName = 'FOO';
if ( isXMLName(nodeName) ) { // assert that nodeName can be used as a node name
  var x = <{nodeName}>test</{nodeName}>
  Print( x.toXMLString() ); // prints: <FOO>test</FOO>
}

E4X dynamic content
JavaScript 1.6

var content = 'FOO';
var x = <item>{content}</item>
Print( x.toXMLString() ); // prints: <item>FOO</item>

E4X iteration
JavaScript 1.6

var sales =
<sales vendor="John">
  <item type="peas" price="4" quantity="5"/>
  <item type="carrot" price="3" quantity="10"/>
  <item type="chips" price="5" quantity="3"/>
</sales>;

for each( var price in sales..@price ) {
  Print( price + '\n' );
}
prints:

4
3
5

Listen a property for changes

function onFooChange( id, oldval, newval ) {

  Print( id + " property changed from " + oldval + " to " + newval );
  return newval;
}

var o = { foo:5 };
o.watch( 'foo', onFooChange );

o.foo = 6;

delete o.foo;

o.foo = 7;

o.unwatch('foo');
o.foo = 8;
prints:

foo property changed from 5 to 6
foo property changed from undefined to 7

Logical operators tricks

var a = 5;
a == 5 && Print( 'a is 5 \n' );
a == 7 || Print( 'a is not 7 \n' );
prints:

a is 5
a is not 7

Functions argument default value

function foo( a, b ) {

  a = a || '123';
  b = b || 55;
  Print( a + ',' + b );
}

foo(); // prints: 123,55
foo('bar'); // prints: bar,55
foo('x', 'y'); // prints x,y
 but:

foo(0,''); // prints: 123,55
because 0 and '' are evaluated as false !

Remove an item by value in an Array object

var arr = ['a', 'b', 'c', 'd'];
var pos = arr.indexOf( 'c' );
pos > -1 && arr.splice( pos, 1 );
Print( arr ); // prints: a,b,d

Multiple-value returns

JavaScript 1.7

function f() {

  return [1, 2];
}

var [a, b] = f();

Print( a + ' ' + b ); // prints: 1 2

Operator [ ] and strings ( like charAt() )

JavaScript 1.6

var str = 'foobar';
Print( str[4] );
prints:

a

indexOf() and lastIndexOf() Works on Array

JavaScript 1.6

var obj = {};
var arr = [ 'foo', 567, obj, 12.34 ];
Print( arr.indexOf(obj) ); // prints: 2

Using Array functions on a non-Array object
JavaScript 1.7

var obj = {};
Array.push(obj, 'foo');
Array.push(obj, 123);
Array.push(obj, 5.55);
Print( obj.toSource() ); // prints: ({0:"foo", length:3, 1:123, 2:5.55})

Change current object (this) of a function call

function test(arg) {

  Print( this[0]+' '+this[1]+' '+arg );
}

var arr = ['foo', 'bar'];
test.call(arr, 'toto'); // prints: foo bar toto

Filter / intercept a function call

function bar(a, b, c, d, e, f) {

  Print(a, b, c, d, e, f)
}

function foo() {

  bar.apply(this, arguments);
}

foo(1, 2, 3, 4, 5, 6); // prints: 123456

E4X Query ( like XPath )
JavaScript 1.6

var xml = <mytree>
 <data id="1" text="foo"/>
 <data id="2" text="bar"/>
</mytree>

Print( xml.data.(@id==1).@text );
var myid = 2;
Print( xml.data.(@id==myid).@text );
prints:

foo
bar

swap two variables
JavaScript 1.7

var a = 1;
var b = 2;
[a,b] = [b,a];

Destructuring assignment with function arguments
JavaScript 1.7

function foo( [a,b] ) {

        Print(a);
        Print(b);
}

foo( [12,34] );
Prints:

12
34

JavaScript scope is not C/C++ scope

if ( false ) { // never reach the next line

        var foo = 123;
}

Print(foo); // prints: undefined ( but is defined )
Print(bar); // failed: ReferenceError: bar is not defined

JavaScript scope and LET instruction
JavaScript 1.7

var x = 5;
var y = 0;
let (x = x+10, y = 12) {
  Print(x+y);
}
Print(x+y);
prints:

27
5
or,

for ( let i=0 ; i < 10 ; i++ ) {
  Print(i + ' ');
}
Print(i);
prints:

0 1 2 3 4 5 6 7 8 9 test.js:4: ReferenceError: i is not defined

Defer function calls

var opList = [];
function deferCall( text, value ) {
        opList.push(arguments)
}
function doCall(func) {
        while (opList.length)
                func.apply(this,opList.shift());
}

deferCall( 'one', 1 );
deferCall( 'titi', 'toto' );
deferCall( 5, 'foo' );

function callLater(a,b) {

Print(a+', '+b);
}

doCall( callLater )
Prints:

one, 1
titi, toto
5, foo

Insert an array in another array

var a = [1,2,3,7,8,9]

var b = [4,5,6]
var insertIndex = 3;

a.splice.apply(a, Array.concat(insertIndex, 0, b));
Print(a); // prints: 1,2,3,4,5,6,7,8,9

Multiple string concatenation

var html = ['aaa', 'bbb', 'ccc', ...].join('');
Is faster than:

var html = 'aaa' + 'bbb' + 'ccc' + ...;
This is true with a big number of elements ( > 5000 )

HTTP headers parser

var rexp_keyval = /(.*?): ?(.*?)\r?\n/g;
function headersToObject( allHeaders ) {

        var res, hdrObj = {};
        for ( rexp_keyval.lastIndex = 0; res = rexp_keyval.exec(allHeaders); hdrObj[res[1]] = res[2])
        return hdrObj;
}

Using 'with' scope

with({ a:5 }) function toto() { return a }
toto() // returns 5

(object).toString()

var a = { toString:function() { return '123'; }  }
Print(a); // prints '123', and not [Object object]

RegExpr.$1

var re = /a(.*)/
'abcd'.match(re)
Print( RegExp.$1 ) // prints 'bcd'

Binary with XmlHTTPRequest

browser related example

var req = new XMLHttpRequest();
req.open('GET', "http://code.google.com/images/code_sm.png",false);
req.overrideMimeType('text/plain; charset=x-user-defined');
//req.overrideMimeType('application/octet-stream');
req.send(null);
var val = req.responseText;
Print( escape(val.substr(0,10)) );

Iterate on values
JavaScript 1.6

for each ( var i in [3,23,4] )
        Print(i)
Prints:

3
23
4

Exceptions Handling / conditional catch (try catch if)

function Toto(){}
function Titi(){}

try {

        throw new Titi()

} catch ( err if err instanceof Toto ) {
        Print('toto')
} catch ( err if err instanceof Titi ) {
        Print('titi')
} catch(ex) {
        throw(ex);
}

Special chars

$=4
_=5
Print( _+$)
prints:

9

object's eval method

var foo = { bar:123 };
foo.eval('bar') // returns 123
var foo = { bar:123 };
with ( foo )
 var val = eval( 'bar' );
Print( val ); // returns 123

eval this

function test() {

        Print(eval('this'));
}
test.call(123)
prints:

123

No Such Method ( noSuchMethod )

var o = {}
o.__noSuchMethod__ = function(arg){ Print('unable to call "'+arg+'" function') }
o.foo(234)
prints:

unable to call "foo" function

RegExp replace

function Replacer( conversionObject ) {

        var regexpStr = '';
        for ( var k in conversionObject )
                regexpStr += (regexpStr.length ? '|' : '') + k;
        var regexpr = new RegExp(regexpStr,'ig'); // g: global, m:multi-line i: ignore case
        return function(s) { return s.replace(regexpr, function(str, p1, p2, offset, s) { var a = conversionObject[str]; return a == undefined ? str : a }) }
}

var myReplacer = Replacer( { '<BR>':'\n', '&amp;':'&', '&lt;':'<', '&gt;':'>', '&quot;':'"' } );

Print( myReplacer('aa<BR>a &amp;&amp;&amp;&lt;') );
prints:

aa
a &&&<

Values comparison

[4] === 4 // is: false
[4] == 4 // is: true

'0' == 0 // is: true
'0' === 0 // is: false

undefined, null, 0, false, '', ...

var a = { b:undefined, c:null, d:0, f:'' }

a['b'] // is: undefined
a['e'] // is: undefined

'b' in a // is: true
'e' in a // is: false

Boolean(a.b) // is: false
Boolean(a.c) // is: false
Boolean(a.d) // is: false
Boolean(a.e) // is: false !
Boolean(a.f) // is: false

typeof( asvqwfevqwefq ) == 'undefined' // =true

Print( '' == false ); // prints: true
Print( 0 == false ); // prints: true
Print( [] == false ); // prints: true

Print( [] == '' ); // prints: true
Print( '' == 0 ); // prints: true
Print( [] == 0 ); // prints: true

constructor property ( InstanceOf + Type )

var a = {};
a.constructor === Object // is: true

AJAX evaluation

var a = {

        b:function() { Print(123) }
}

var body = 'b();';

with(a) { eval(body) }; // Prints 123

Comma operator

var a = 0;
var b = ( a++, 99 );

a // is: 1
b // is: 99

var i = 0;
while( Print('x '), i++<10 )
        Print(i + ' ')
prints: x 1 x 2 x 3 x 4 x 5 x 6 x 7 x 8 x 9 x 10 x

closures pitfall

var a = [];

for ( var i=0; i<10; i++ ) {

 a[i] = function() { Print(i); }
}

a[0](); // is: 10
a[1](); // is: 10
a[2](); // is: 10
a[3](); // is: 10
simpler case:

var i = 5;
function foo() { Print(i) }
foo(); // prints 5
i = 6;
foo(); // prints 6
Closure definition
A closure occurs when one function appears entirely within the body of another, and the inner function refers to local variables of the outer function.

links
obligated lecture to understand closures

In the first example, you can avoid the behavior using the let instruction:

var a = [];

for ( var i=0; i<10; i++ ) {
 let j = i;
 a[i] = function() { Print(j); }
}
In this case, each instances of the (anonymous) inner function refer to different instances of variable j.

sharp variable

var a = { titi:#1={}, toto:#1# };
a.titi === a.toto; // is: true

var a = { b:#1={ c:#1# } }
// a.b.c.c.c.c.c.c...

common object between 2 objects

function a() {}

a.prototype = { b:{} }

c = new a;
d = new a;

c.b.e = 2;

c.b === d.b // is: true !
d.b.e // is: 2

constructor

function a( b ) {

        Print(b);
}

c.prototype = new a;

function c( arg ) {

        this.constructor.apply( this, arg );
};

o = new c( [1,2,3] );
prints:

undefined
1

JavaScript string can contain null chars

var test = '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00';
test.length // is: 10

'new' operator precedence

function test() {

        this.toto = 1234;
}

(new test()).toto // ok ( logical way )

new test().toto // ok

new test.toto // bad !

(new test).toto // ok

constructor usage

function toto( val ) { // parent class

        this.print = function() {

                Print( val );
        }
}

titi.prototype = new toto;

function titi( val ) { // child class

        this.constructor(val);
}

(new titi(7)).print(); // prints 7

To object

var obj = Object(123);
obj.foo = 678;
Print( typeof( obj ) + ', ' + obj + ', ' + obj.foo ); // prints: object, 123, 678

Serialize or uneval a variable or an object ( can be nested )

var o = { a:123, b:'test', c:function() { return 987; } }

Print( o.toSource() ); // prints: ({a:123, b:"test", c:(function () {return 987;})})

Print( uneval(o) ); // prints: ({a:123, b:"test", c:(function () {return 987;})})

JavaScript labels

xxx: {
        Print( 111 )
        break xxx;
        Print( 222 )
}
prints:

111

for in loop

for ( var i in function(){ return [1, 2, 3] }() )
  Print( i );
prints:

0
1
2

proto ( prototype property )

var a = {}
var b = {}
a.__proto__ == b.__proto__ // is: true

Numbers and floating point error

Print( 1000000000000000128 ); // prints 1000000000000000100

Number base conversion ( hexadecimal )

(255).toString(16); // is: ff

parseInt( 'ff', 16 ) // is: 255

parseInt('0xff'); // is 255

try / catch / finally

try {

} catch(ex) {

        Print('catch')
} finally {

        Print('finally')
}
prints:

finally

Object argument

var proto = {

        x: function() { return 'x' }
}

var o1 = new Object( proto );
var o2 = new Object( proto );

o1 == o2 // is: true

object and its prototype

function obj() {}

obj.prototype = { x:1 };
var b = new obj;

obj.prototype = { x:2 };
var c = new obj;

c.x == b.x // is: false

Runtime prototype object

var proto1 = {
        a:function(){ return 1 }
}

var proto2 = {
        a:function(){ return 2 }
}

function createObject( proto ) {

        var cl = function() {}
        cl.prototype = proto;
        return new cl;
}

var v1 = createObject( proto1 ).a
var v2 = createObject( proto2 ).a

Print( v1() );
Print( v2() );

Print( createObject( proto1 ).a === createObject( proto1 ).a );
prints:

1
2
true

for in loop and undefined value

var o = { x:undefined }
for ( var i in o )
        Print(i)
prints:

x

Call function in parent class

toto.prototype = new function() {

        this.a = function() {

                Print(456)
        }
};


function toto() {

        this.a=function(){

                Print(123)
                toto.prototype.a.call(this); // or: this.__proto__.a();
        }
}

var o = new toto;
o.a();
prints:

123
456

getter and setter function

abcd getter = function() {

        Print(345);
}

abcd;
abcd;
abcd;
prints:

345
345
345
Beware
 The previous syntax used to define a getter is highly deprecated, and should be replaced with:

__defineGetter__('abcd', function() {

        Print(345);
});

defineGetter ( define getter )

o = {}
o.__defineGetter__('x', function(){ Print('xxx')} )
o.x
prints:

xxx

check if an object or its prototype has a propery (hasOwnProperty)

var o = { a:1 }
o.__proto__ = { b:2 }

o.hasOwnProperty('a'); // is: true
o.hasOwnProperty('b'); // is: false
check this article about hasOwnProperty function

check if a property is enumerable (propertyIsEnumerable)

var o = { a:1 }

o.propertyIsEnumerable( 'a' ); // is: true

[].propertyIsEnumerable( 'splice' ); // is: false

find the getter/setter function of an object ( lookup getter )

function test() {

        this.x getter = function(){ Print( 'getter' ) }
        this.x setter = function(){ Print( 'setter' ) }
}

var t = new test
Print( t.__lookupGetter__( 'x' ) );
prints:

function () {
    Print("getter");
}

suppress array element while iterating it

the following example will failed :

var a = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ];
for ( var i in a ) {

        if ( a[i] == 1 || a[i] == 2 )
        a.splice( i, 1 );
}
Print( a ); // prints: 0,2,3,4,5,6,7,8,9
Print( a.length ); // prints: 9
We can use : a[i] == undefined; Or start from the end :

var a = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ];

for ( var i = a.length - 1; i >= 0; i-- ) {

        if ( a[i] == 0 || a[i] == 8 )
        a.splice( i, 1 );
}
Print( a ); // prints: 1,2,3,4,5,6,7,9
Print( a.length ); // prints: 8

JavaScript arrays

var a = [ 1,2,3 ];
a.x = 'xxx';

for ( var i = 0; i < a.length; i++ )
        Print('a: '+a[i]);

for ( var i in a )
        Print('b: '+a[i]);
prints:

a: 1
a: 2
a: 3
b: 1
b: 2
b: 3
b: xxx

delete array element

 The following do not work:

var ti = [5,6,7,8];
ti.length // is: 4
delete ti[3]
ti.length // is: 4
Use 'splice' instead:

ti.splice(3, 1);

Принципы работы script, script async и script defer


пятница, 26 сентября 2014 г.

Усовершенствование стандартного Array

Arr.js - это класс, унаследованный от стандартного Array. Отличительными особенностями являются: наличие события change для отслеживания любых изменений в массиве, и методы insert(), update(), remove(), set(), get() для упрощенной работы с массивом. Доступны все родные методы стандартного Array.



(function(rootScope) {

  var
    arrayPop = Array.prototype.pop,
    arrayPush = Array.prototype.push,
    arrayReverse = Array.prototype.reverse,
    arrayShift = Array.prototype.shift,
    arraySort = Array.prototype.sort,
    arraySplice = Array.prototype.splice,
    arrayUnshift = Array.prototype.unshift;

  /**
   *
   */
  var Arr = function() {
    arrayPush.apply(this, arguments);
 
    this.events = [];
  };

  Arr.prototype = [];

  /**
   *
   */
  Arr.prototype.events = [];
 
  /**
   *
   */
  Arr.prototype.get = function(index, defaultValue) {
    defaultValue = typeof defaultValue === 'undefined' ? undefined: defaultValue;
    return typeof this[index] === 'undefined' ? defaultValue: this[index];
  };
 
  /**
   *
   */
  Arr.prototype.on = function(eventName, handler) {
    if (! handler instanceof Function) {
      throw new Error('handler should be an Function');
    }
 
    this.events.push({
      name: eventName,
      handler: handler
    });
 
    return this;
  };
 
  /**
   *
   */
  Arr.prototype.trigger = function(eventName, args) {
    args = args || [];
 
    for (var i=0,len=this.events.length; i<len; i++) {
      if (this.events[i].name == eventName) {
        this.events[i].handler.apply(this, [args]);
      }
    }
 
    return this;
  };
 
  /**
   *
   */
  Arr.prototype.update = function(handler) {
    if (! handler instanceof Function) {
      throw new Error('handler should be an Function');
    }
 
    var oldValue, newValue;
    var result = [];
 
    for (var i=0,len=this.length; i<len; i++) {
      oldValue = this[i];
      newValue = handler.apply(this, [oldValue, i]);
   
      if (typeof newValue !== 'undefined') {
        this[i] = newValue;
        result.push(newValue);
      }
    }
 
    if (result.length > 0) {
      this.trigger('change', {
        type: 'update',
        items: result
      });
    }
 
    return this;
  };

  /**
   *
   */
  Arr.prototype.insert = function(items) {
    if (! items instanceof Array) {
      throw new Error('items should be an Array');
    }
 
    arrayPush.apply(this, items);
    this.trigger('change', {
      type: 'insert',
      items: items
    });
    return this;
  };

  /**
   *
   */
  Arr.prototype.remove = function(handler) {
    if (! handler instanceof Function) {
      throw new Error('handler should be an Function');
    }
 
    var result = [];
    var stay = [];

    for (var i=0, len=this.length; i<len; i++) {
      isRemove = handler.apply(this, [this[i], i]);
   
      if (isRemove === true) {
        result.push(this[i]);
      } else {
        stay.push(this[i]);
      }
    }

    arraySplice.apply(this, [0, this.length]);
    arrayPush.apply(this, stay);

    if (result.length > 0) {
      this.trigger('change', {
        type: 'remove',
        items: result
      });
    }
 
    return this;
  };

  /**
   *
   */
  Arr.prototype.set = function(index, value) {
    if (! index instanceof Number) {
      throw new Error('index should be an Number');
    }
 
    this[index] = value;
    this.trigger('change', {
      type: 'update',
      items: [this[index]]
    });
    return this;
  };
 
  /**
   * Removes the last element from an array and returns that element.
   */
  Arr.prototype.pop = function() {
    var result = arrayPop.apply(this, arguments);
    this.trigger('change', {
      type: 'remove',
      items: [result]
    });
    return result;
  };
 
  /**
   * Adds one or more elements to the end of an array and returns the new length of the array.
   */
  Arr.prototype.push = function() {
    var result = arrayPush.apply(this, arguments);
    this.trigger('change', {
      type: 'insert',
      items: Array.prototype.slice.call(arguments, 0)
    });
    return result;
  };
 
  /**
   * Reverses the order of the elements of an array — the first becomes the last, and the last becomes the first.
   */
  Arr.prototype.reverse = function() {
    var result = arrayReverse.apply(this, arguments);
    this.trigger('change', {
      type: 'update',
      items: result.slice(0)
    });
    return result;
  };
 
  /**
   * Removes the first element from an array and returns that element.
   */
  Arr.prototype.shift = function() {
    var result = arrayShift.apply(this, arguments);
    this.trigger('change', {
      type: 'remove',
      items: [result]
    });
    return result;
  };
 
  /**
   * Sorts the elements of an array in place and returns the array.
   */
  Arr.prototype.sort = function() {
    var result = arraySort.apply(this, arguments);
    this.trigger('change', {
      type: 'update',
      items: result
    });
    return result;
  };
 
  /**
   * Adds and/or removes elements from an array.
   */
  Arr.prototype.splice = function() {
    var items = this.slice(arguments[0], arguments[0]+arguments[1]);
    var result = arraySplice.apply(this, arguments);
    this.trigger('change', {
      type: 'remove',
      items: items
    });
    return result;
  };
 
  /**
   * Adds one or more elements to the front of an array and returns the new length of the array.
   */
  Arr.prototype.unshift = function() {
    var result = arrayUnshift.apply(this, arguments);
    this.trigger('change', {
      type: 'insert',
      items: [result]
    });
    return result;
  };

  // export

  if (typeof module !== 'undefined') {
    module.exports = Arr;
  } else {
    rootScope.Arr = Arr;
  }
})(this);

Примеры работы.

var fruits = new Arr('apple', 'orange', 'pineapple');

fruits.on('change', function() {
  alert('I changed fruits: ' + fruits.join(', '));
});

fruits.push('banana');

var fruits = new Arr('apple', 'orange', 'pineapple');

fruits.get(0);
// apple

fruits.get(10, 'lime'); // trying to get undefined element - return defaultValue
// lime

fruits.get(20); // trying to get undefined element
// null

fruits.set(1, 'nut');
// ['nut', 'orange', 'pineapple']

fruits.insert(['lime', 'banana', 'kivi']);
// ['nut', 'orange', 'pineapple', 'lime', 'banana', 'kivi']

fruits.remove(function(item, index) {
  if (item.indexOf('apple') !== -1) { // remove only items where word "apple" is founded
    return true;
  }
});
// ['nut', 'orange', 'lime', 'banana', 'kivi']

fruits.update(function(item, index) {
  if (item.indexOf('nut') !== -1) { // update "nut" to "apple"
    return 'apple';
  }
});
// ['apple', 'orange', 'lime', 'banana', 'kivi']

Зачем событие change и как с ними работать.

Наличие события позволяет сделать:
подобие FRP: когда изменение одних данных должно повлечь за собой изменение других данных и так далее
отложенный рендеринг: что то изменилось в массиве — обновили HTML (ala angular)
автоматическое сохранение данных на сервер при любых изменениях

Поддерживается одно событие — change.

var fruits = new Arr('apple', 'orange', 'pineapple');

fruits.on('change', function(event) { // handler
  console.log(event);
});

fruits.push('banana');
// { "type": "insert", "items": ["banana"] }

fruits.remove(function(item) { return item == 'banana'; });
// { "type": "remove", "items": ['banana"] }

Понять что произошло в массиве можно по передаваемому в handler объекту, event. Свойства объекта event: type может принимать значения: insert, update, remove. Свойство items позволяет узнать какие элементы массива были затронуты.

Наглядный пример

// Массив в котором планируем хранить данные о погоде
var weatherList = new Arr;

// При изменении в массиве - перересовываем список
weatherList.on('change', function() {
  var el = $('#weather');
  var celsius, maxCelsius, minCelsius, weather;

  el.html('');

  weatherList.forEach(function(item) {
    celsius = Math.floor(item.main.temp - 273);
    maxCelsius = Math.floor(item.main.temp_max - 273);
    minCelsius = Math.floor(item.main.temp_min - 273);
    weather = item.weather.pop().main;
    el.append('<li><b>' + item.name + '</b> ' + ' ' + celsius + ' (max: ' + maxCelsius + ', min: ' + minCelsius + ') ' + weather + '</li>');
  });
});

// Загрузка погоды из сервиса, обновление массива weatherList
function loadWeather(lat, lng) {
  $.get('http://api.openweathermap.org/data/2.5/find?lat=' + lat + '&lon=' + lng + '&cnt=10').done(function(data) {
    // clear weather list
    weatherList.remove(function() { return true; });

    // insert items
    weatherList.insert(data.list);
  });
}

// Погода в Киеве
loadWeather(50.4666537, 30.5844519);

Примеры работы основных методов.

new Arr([item1, item2, ..., itemN])
Initialize new array items: item1, item2, ..., itemN.

Properties
events

List of Attached Events.

var fruits = new Arr('apple', 'orange', 'pineapple');

fruits.on('change', function(event) {
  console.log('fruits list is changed.');
});

// fruits.events
// [{
//  "name": "change",
//  "handler": function() { ... }
// }]

fruits.push('mango');
// fruits list is changed.
// fruits
// ['apple', 'orange', 'pineapple', 'mango']
length

Standard property length.

Accessor methods
get(index [, defaultValue])

Example:

var fruits = new Arr('apple', 'orange', 'pineapple');

fruits.get(0);
// apple

fruits.get(10, 'lime'); // trying to get undefined element - return defaultValue
// lime

fruits.get(20); // trying to get undefined element
// undefined
Standard accessor methods are supported

concat() Returns a new array comprised of this array joined with other array(s) and/or value(s).
join() Joins all elements of an array into a string.
slice() Extracts a section of an array and returns a new array.
toString() Returns a string representing the array and its elements. Overrides the Object.prototype.toString() method.
toLocaleString() Returns a localized string representing the array and its elements. Overrides the Object.prototype.toLocaleString() method.
indexOf() Returns the first (least) index of an element within the array equal to the specified value, or -1 if none is found.
lastIndexOf() Returns the last (greatest) index of an element within the array equal to the specified value, or -1 if none is found.
and other methods.

Mutator methods
Notice: Traditional mutator arr[index] = value do not trigger event change. Use method set(index, value) instead arr[index] = value.

set(index, value)

Set value by index. Will be triggered event change.

Example:

var fruits = new Arr('apple', 'orange', 'pineapple');

fruits.set(0, 'banana');
// ['banana', 'orange', 'pineapple']

fruits.set(1, 'lime');
// ['banana', 'lime', 'pineapple']

fruits.set(3, 'nut');
// ['banana', 'lime', 'pineapple', 'nut']
insert([item1, item2, ..., itemN])

Insert array of items. Will be triggered event change.

Example:

var fruits = new Arr();

fruits.inser('apple', 'orange', 'pineapple');
// ['apple', 'orange', 'pineapple']
update(handler)

Update item if handler return true. Will be triggered event change if one or more items updated.

handler can recive data:

value (mixed) current value
index (number) current index
Example:

var fruits = new Arr('apple', 'orange', 'pineapple');

fruits.update(function(value, index) {
  if (index === 2) {
    return 'lime';  // "return" not undefined value for update item
  }
});
// ['apple', 'orange', 'lime']

fruits.update(function(value, index) {
  return index;
});
// [0, 1, 2]
remove(handler)

Remove item if handler return true. Will be triggered event change if one or more items removed.

Example:

var fruits = new Arr('apple', 'orange', 'pineapple');

fruits.remove(function(value, index) {
  if (value.indexOf('apple') !== -1) {
    return true;
  }
});
// ['orange']
Standard mutator methods are supported

Each mutator method throw event change. How? You can read in section Events.

pop() Removes the last element from an array and returns that element.
push() Adds one or more elements to the end of an array and returns the new length of the array.
reverse() Reverses the order of the elements of an array — the first becomes the last, and the last becomes the first.
shift() Removes the first element from an array and returns that element.
sort() Sorts the elements of an array in place and returns the array.
splice() Adds and/or removes elements from an array.
unshift() Adds one or more elements to the front of an array and returns the new length of the array.
and other methods.

Sometimes you need to push array of items to Arr. You can push array of items in this way (note: now you can use method insert()):

var fruits = new Arr;

fruits.push.apply(fruits, ['apple', 'orange', 'pineapple']);
// fruits
// ['apple', 'orange', 'pineapple']
For remove item(s) from Arr you can use traditional method splice() (note: now you can use method remove()).

var fruits = new Arr('apple', 'orange', 'pineapple');

fruits.splice(0, 1); // remove first item
// fruits
// ['orange', 'pineapple']
Iteration methods
forEach() Calls a function for each element in the array.
every() Returns true if every element in this array satisfies the provided testing function.
some() Returns true if at least one element in this array satisfies the provided testing function.
filter() Creates a new array with all of the elements of this array for which the provided filtering function returns true.
map() Creates a new array with the results of calling a provided function on every element in this array.
reduce() Apply a function against an accumulator and each value of the array (from left-to-right) as to reduce it to a single value.
reduceRight() Apply a function against an accumulator and each value of the array (from right-to-left) as to reduce it to a single value.
and other methods.

You can use method filter() for searching items by conditions.

Example:

var fruits = new Arr('apple', 'orange', 'pineapple');

var fruitsWithWordApple = fruits.filter(function(value, index) {
  if (value.indexOf('apple') !== -1) {
    return true;
  } else {
    return false;
  }
});
// fruitsWithWordApple
// ['apple', 'pineapple']
Events
Instance of Arr throw only one event change.

How to use events? You can use array events like events in Backbone Collection.

var fruits = new Arr('apple', 'orange', 'pineapple');

fruits.on('change', function(event) {
  // event
  // {
  //   "type": "insert",
  //   "items": ['mongo']
  // }

  console.log('fruits list is changed.');
});

// fruits.events
// [{
//  "name": "change",
//  "handler": function() { ... }
// }]

fruits.push('mango');
// fruits list is changed.
// fruits
// ['apple', 'orange', 'pineapple', 'mango']
Events Handling and Triggering
Notice: Traditional mutator arr[index] = value do not trigger event change. Use method set(index, value) instead arr[index] = value.

on(eventName, handler)

Attach event handler.

handler can recive event that have data:

type (string) can be insert, update, remove
items (array) are items that was inserted, updated or removed
Example:

var fruits = new Arr('apple', 'orange', 'pineapple');

fruits.on('change', function(event) {
  // event
  // {
  //   "type": "insert",
  //   "items": ['mango']
  // }

  console.log('fruits list is changed.');
});

fruits.push('mango');
trigger(eventName [, args])

Trigger some event.

var fruits = new Arr('apple', 'orange', 'pineapple');

fruits.on('change', function(event) {
  console.log('fruits list is changed.');
});

fruits.trigger('change');
// fruits list is changed.
Use Cases
Send server updates when Arr is changed.

var products = new Arr(
  {
    id: 1,
    name: 'apple'
  },
  {
    id: 2,
    name: 'orange'
  },
  {
    id: 3,
    name: 'pineapple'
  }
);

products.on('change', function(event) {
  // products are changed
  // you can use event.type == 'insert' or 'update' or 'remove' to detect items that you need to update on the server

  // $ is link on jQuery
  $.post('/products', products)
    .fail(function() {
      alert('error');
    })
    .done(function(updatedProducts) {
      alert('products are updated');
    });
});

products.push({
  name: 'lime'
});

четверг, 25 сентября 2014 г.

Простой набор паттернов JavaScript

Паттерны JavaScript

1) Class

function Car (model) {
    this.model = model;
    this.year = 2012;
}

Car.prototype.getInfo = function () {
    return this.model + ' ' + this.year;
};

var myCar = new Car('Ford');
myCar.year = 2010;

console.log(myCar.getInfo());

2) Mixin

var Person = function (firstName , lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
    this.gender = 'male';
};

var Superhero = function (firstName, lastName , powers) {
    // Invoke the superclass constructor on the new object
    // then use .call() to invoke the constructor as a method of
    // the object to be initialized.

    Person.call(this, firstName, lastName);

    // Finally, store their powers, a new array of traits not found in a normal "Person"
    this.powers = powers;
};

// или

function applyMixins(derivedCtor, baseCtors) {
    baseCtors.forEach(function (baseCtor) {
        Object.getOwnPropertyNames(baseCtor.prototype).forEach(function (name) {
            derivedCtor.prototype[name] = baseCtor.prototype[name];
        });
    });
}

// Disposable Mixin
var Disposable = (function () {
    function Disposable() {}
    Disposable.prototype.dispose = function () {this.isDisposed = true;};
    return Disposable;
})();

// Activatable Mixin
var Activatable = (function () {
    function Activatable() {}
    Activatable.prototype.activate = function () {this.isActive = true;};
    Activatable.prototype.deactivate = function () {this.isActive = false;};
    return Activatable;
})();

var SmartObject = (function () {
    function SmartObject() {
        var _this = this;
        this.isDisposed = false; // Disposable
        this.isActive = false; // Activatable
        setInterval(function () {return console.log(_this.isActive + " : " + _this.isDisposed);}, 500);
    }
    SmartObject.prototype.interact = function () {this.activate();};
    return SmartObject;
})();

applyMixins(SmartObject, [Disposable, Activatable]);

var smartObj = new SmartObject();

3) Extend

var extend = this.extend || function (childClass, parentClass) {
    for (var key in parentClass) {
        if (parentClass.hasOwnProperty(key)) {
            childClass[key] = parentClass[key];
        }
    }
    function F () {this.constructor = childClass;}
    F.prototype = parentClass.prototype;
    childClass.prototype = new F();
};

var ParentClass = (function () {

    // Constructor
    function ParentClass(message) {
        // Constructor body
        this.greeting = message;
        this.x = 1;
        this.y = 2;
        this.z = 3;
    }

    // Public function
    ParentClass.prototype.greet = function () {
        return "Hello, " + this.greeting;
    };

    return ParentClass;

})();

var ChildClass = (function (parentClass) {

    // Extend
    extend(ChildClass, parentClass);

    // Constructor
    function ChildClass(message) {
        parentClass.apply(this, arguments); // или можно сделать частично parentClass.call(this, message);
        this.greeting = message;
    }

    // Public variable
    ChildClass.prototype.name = 'Parent';

    // Public function
    ChildClass.prototype.greet = function () {
        return 'Hello, ' + this.greeting;
    };

    // Public function
    ChildClass.prototype.hey = function () {
        return 'Hey! ' + privateFunc();
    };

    // Private variable
    var privateVar = 'Hello';

    // Private function
    function privateFunc () {
        return privateVar + ' my friend!';
    }

    // Static variable
    ChildClass.staticVar = 'I am static variable';

    // Static function
    function staticFunc () {
        return 'I am static function';
    }

    return ChildClass;

})(ParentClass);

var SubClass = (function (parentClass) {

    extend(SubClass, parentClass);

    function SubClass(message) {
        parentClass.apply(this, arguments);
        this.greeting = message;
    }

    SubClass.prototype.greet = function () {
        return 'Hello, ' + this.greeting;
    };

    SubClass.prototype.hey = function () {
        return 'Hey!';
    };

    return SubClass;

})(ChildClass);

var parentObject = new ParentClass('world');
var childObject = new ChildClass();
var subObject = new SubClass();

console.log(parentObject.greet());
console.log(childObject.hey());
console.log(subObject.hey());

4) Module

var module = (function($){

    var privateVar = 1
       , publicVar = 2 + privateVar;

    function Boat (name) {
        this.name = name;
        this.year = 2012;
    }

    Boat.prototype.getInfo = function () {
        return this.name + ' ' + this.year;
    };

    function Plane (name) {
        this.name = name;
        this.year = 2012;
    }

    Plane.prototype.getInfo = function () {
        return this.name + ' ' + this.year;
    };

    function insertHTML (tag, HTMLcode) {
        $(tag).html(HTMLcode);
    }

    return {
          publicVar: publicVar
        , Boat: Boat
        , Plane: Plane
        , insertHTML: insertHTML
    };
})(jQuery);

console.log(model.publicVar);

var myBoat = new model.Boat('Ann');
myBoat.year = 2010;

console.log(myBoat.getInfo());

model.insertHTML('div', '<h1>Hello!</h1>');

// Using CommonJS, AMD or browser globals to create a module

(function (root, factory) {
             if (typeof exports === 'object') {factory(exports, require('b')); // CommonJS
    } else if (typeof define === 'function' && define.amd) {define(['exports', 'b'], factory);  // AMD. Register as an anonymous module.
    } else {factory((root.commonJsStrict = {}), root.b); // Browser globals
    }
}(this, function (exports, b) {
    // use b in some fashion.
    // attach properties to the exports object to define
    // the exported module properties.
    exports.action = function () {};
}));

5) Namespace

var myNamespace = myNamespace || {};

myNamespace.someVariable = 1;
myNamespace.someFunction = function () {alert(myNamespace.someVariable);};

// Namespace injection

var myApp = myApp || {};
myApp.utils =  {};

(function () {
  var val = 5;

  this.getValue = function () {return val;};
  this.setValue = function(newVal) {val = newVal;}

  // also introduce a new sub-namespace
  this.tools = {};

}).apply(myApp.utils);

(function () {
    this.diagnose = function(){return "diagnosis";}
}).apply(myApp.utils.tools);

console.log(myApp); // Outputs: 5
console.log(myApp.utils.getValue());

myApp.utils.setValue(25);
console.log(myApp.utils.getValue());

console.log(myApp.utils.tools.diagnose());

6) Singleton

var singleton = (function(){
    var instance;

    function init () {

         var privateVariable = 'I am private variable.';

         function privatMethod () {
             console.log('I am private method.');
         }

         return {
               publicProperty: 'I am public property.'
             , publicMethod: function () {
                 console.log('I am public method.');
               }
         };

    }

    return {
        getInstance: function () {
            if (!instance) {
                instance = init();
            }
            return instance;
        }
    }

})();

var mySingleton1 = singleton.getInstance()
    , mySingleton2 = singleton.getInstance();

console.log(mySingleton1 === mySingleton2); // true

console.log(mySingleton1.publicProperty);
mySingleton1.publicMethod();

7) Observer

function Observer () {
    this.events = [];
}

Observer.prototype.on = function (eventName, callbackFunction) {
    return this.events.push({
          eventName: eventName
        , callbackFunction: callbackFunction
    });
};

Observer.prototype.off = function (eventName) {
    var eventsLength = this.events.length;
    for (;eventsLength--;) {
        if (this.events[eventsLength].eventName === eventName) {
            this.events.splice(eventsLength, 1);
        }
    }
};

Observer.prototype.trigger = function (eventName) {
    var eventsLength = this.events.length;
    for (;eventsLength--;) {
        if (this.events[eventsLength].eventName === eventName) {
            this.events.callbackFunction();
        }
    }
}

Observer.prototype.count = function () {
    return this.events.length;
};

8) Command

var carManager = {

      requestInfo: function (model, id) {
          return "The information for " + model + " with ID " + id + " is foobar";
      }

    , buyVehicle: function (model, id) {
          return "You have successfully purchased Item " + id + ", a " + model;
      }

    , arrangeViewing: function (model, id) {
          return "You have successfully booked a viewing of " + model + " ( " + id + " ) ";
     }

};

var car = {
      model: 'Ford'
    , id: 52738
};

console.log(carManager.call(car));

// или

carManager.execute = function (name) {
    return carManager[name] && carManager[name].apply(null, [].slice.call(arguments, 1));
};

console.log(carManager.execute('buyVehicle', 'Ford Escort', '34232'));

9) Facade

var addMyEvent = function (element, eventName, callbackFunction) {
             if (element.addEventListener) {element.addEventListener(eventName, callbackFunction, false);
    } else if (element.attachEvent) {element.attachEvent('on' + eventName, callbackFunction);
    } else {element['on' + eventName] = callbackFunction;
    }
};

addMyEvent(document.getElementById('myButton'), 'click', function(){alert('OK');});

10) Factory

function Car (options) {
    this.doors = options.doors || 4;
    this.state = options.state || 'brand new';
    this.color = options.color || 'silver';
}

function Truck (options) {
    this.state = options.state || 'used';
    this.wheelSize = options.wheelSize || 'large';
    this.color = options.color || 'blue';
}

function VehicleFactory() {}

VehicleFactory.prototype.vehicleClass = Car;
VehicleFactory.prototype.createVehicle = function (options) {
    switch(options.vehicleType){
        case 'car':this.vehicleClass = Car; break;
        case 'truck':this.vehicleClass = Truck; break;
    }
    return new this.vehicleClass(options);
};

var carFactory = new VehicleFactory();

var car = carFactory.createVehicle( {
      vehicleType: 'car'
    , color: 'yellow',
    , doors: 6
});

var movingTruck = carFactory.createVehicle( {
      vehicleType: 'truck'
    , state: 'like new'
    , color: 'red'
    , wheelSize: 'small'
});

11) Decorator

function door () {
    return 'metal door';
}

function decorator (fn) {
    return function(){
        return 'black ' + fn();
    }
}

door = decorator(door);

console.log(door());

12) Iterator

function forEachInArrayDo (array, func) {
    for (var i = 0, len = array.length; i < len; i++) {
        func(array[i]);
    }
}

var arr = [1,2,3,4,5];

forEachInArrayDo(arr, function(element){console.log(element);});

13) Curry

function curry (func) {
    return function (args) {
        return func (args);
    };
}

['11','11','11','11'].map(parseInt) // [11, NaN, 3, 4]

['11','11','11','11'].map(curry(parseInt)); // [11, 11, 11, 11]

function curry2(fun) {
    return function(secondArg) {
        return function(firstArg) {
            return fun(firstArg, secondArg);
        };
    };
}

function div(n, d) {return n / d;}
var div10 = curry2(div)(10);
div10(50); // 5

function curry3(fun) {
    return function(last) {
        return function(middle) {
            return function(first) {
                return fun(first, middle, last);
            };
        };
    };
};

var songsPlayed = curry3(_.uniq)(false)(songToString);
songsPlayed(plays);

// [{artist: "Burial", track: "Archangel"},
//  {artist: "Ben Frost", track: "Stomp"},
//  {artist: "Emeralds", track: "Snores"}]

14) Proxy

$('button').on('click', function () {

    setTimeout($.proxy(function () {
        $(this).addClass('active'); // "this" теперь ссылается на внешний "this" функции "click"
    }, this), 500); // <-- Этот "this" передается из функции "click" во внутреннюю функцию внутри "proxy"

});

// jQuery's implementation of jQuery.proxy() can be found below:
// Bind a function to a context, optionally partially applying any
// arguments.
proxy: function(fn, context) {
  if (typeof context === "string") {
    var tmp = fn[context];
    context = fn;
    fn = tmp;
  }

  // Quick check to determine if target is callable, in the spec
  // this throws a TypeError, but we will just return undefined.
  if (!jQuery.isFunction( fn ) ) {return undefined;}

  // Simulated bind
  var args = slice.call( arguments, 2 ),
    proxy = function() {
      return fn.apply( context, args.concat( slice.call( arguments ) ) );
    };

  // Set the guid of unique handler to the same of original handler, so it can be removed
  proxy.guid = fn.guid = fn.guid || proxy.guid || jQuery.guid++;

  return proxy;
}

15) jQuery plugin

/*!
 * jQuery lightweight plugin boilerplate
 * Original author: @ajpiano
 * Further changes, comments: @addyosmani
 * Licensed under the MIT license
 */


// the semi-colon before the function invocation is a safety
// net against concatenated scripts and/or other plugins
// that are not closed properly.
;(function ( $, window, document, undefined ) {

    // undefined is used here as the undefined global
    // variable in ECMAScript 3 and is mutable (i.e. it can
    // be changed by someone else). undefined isn't really
    // being passed in so we can ensure that its value is
    // truly undefined. In ES5, undefined can no longer be
    // modified.

    // window and document are passed through as local
    // variables rather than as globals, because this (slightly)
    // quickens the resolution process and can be more
    // efficiently minified (especially when both are
    // regularly referenced in our plugin).

    // Create the defaults once
    var pluginName = "defaultPluginName",
        defaults = {
            propertyName: "value"
        };

    // The actual plugin constructor
    function Plugin( element, options ) {
        this.element = element;

        // jQuery has an extend method that merges the
        // contents of two or more objects, storing the
        // result in the first object. The first object
        // is generally empty because we don't want to alter
        // the default options for future instances of the plugin
        this.options = $.extend( {}, defaults, options) ;

        this._defaults = defaults;
        this._name = pluginName;

        this.init();
    }

    Plugin.prototype.init = function () {
        // Place initialization logic here
        // We already have access to the DOM element and
        // the options via the instance, e.g. this.element
        // and this.options
    };

    // A really lightweight plugin wrapper around the constructor,
    // preventing against multiple instantiations
    $.fn[pluginName] = function ( options ) {
        return this.each(function () {
            if ( !$.data(this, "plugin_" + pluginName )) {
                $.data( this, "plugin_" + pluginName,
                new Plugin( this, options ));
            }
        });
    }

})( jQuery, window, document );

$("#elem").defaultPluginName({
      propertyName: "a custom value"
});

16) jQuery UI widget factory

/*!
 * jQuery UI Widget-factory plugin boilerplate (for 1.8/9+)
 * Author: @addyosmani
 * Further changes: @peolanha
 * Licensed under the MIT license
 */


;(function ( $, window, document, undefined ) {

    // define our widget under a namespace of your choice
    // with additional parameters e.g.
    // $.widget( "namespace.widgetname", (optional) - an
    // existing widget prototype to inherit from, an object
    // literal to become the widget's prototype );

    $.widget( "namespace.widgetname" , {

        //Options to be used as defaults
        options: {
            someValue: null
        },

        //Setup widget (e.g. element creation, apply theming
        // , bind events etc.)
        _create: function () {

            // _create will automatically run the first time
            // this widget is called. Put the initial widget
            // setup code here, then we can access the element
            // on which the widget was called via this.element.
            // The options defined above can be accessed
            // via this.options this.element.addStuff();
        },

        // Destroy an instantiated plugin and clean up
        // modifications the widget has made to the DOM
        destroy: function () {

            // this.element.removeStuff();
            // For UI 1.8, destroy must be invoked from the
            // base widget
            $.Widget.prototype.destroy.call( this );
            // For UI 1.9, define _destroy instead and don't
            // worry about
            // calling the base widget
        },

        methodB: function ( event ) {
            //_trigger dispatches callbacks the plugin user
            // can subscribe to
            // signature: _trigger( "callbackName" , [eventObject],
            // [uiObject] )
            // e.g. this._trigger( "hover", e /*where e.type ==
            // "mouseenter"*/, { hovered: $(e.target)});
            this._trigger( "methodA", event, {
                key: value
            });
        },

        methodA: function ( event ) {
            this._trigger( "dataChanged", event, {
                key: value
            });
        },

        // Respond to any changes the user makes to the
        // option method
        _setOption: function ( key, value ) {
            switch ( key ) {
            case "someValue":
                // this.options.someValue = doSomethingWith( value );
                break;
            default:
                // this.options[ key ] = value;
                break;
            }

            // For UI 1.8, _setOption must be manually invoked
            // from the base widget
            $.Widget.prototype._setOption.apply( this, arguments );
            // For UI 1.9 the _super method can be used instead
            // this._super( "_setOption", key, value );
        }
    });

})( jQuery, window, document );

var collection = $("#elem").widgetName({
    foo: false
});

collection.widgetName("methodB");