JS + toi = ♥

Promis, ça ne fait pas mal

Une présentation de Christophe Porteneuve à Paris Web 2014

TÉKITOA


var christophe = {
  age:        36.94794520547945,
  city:       'Paris',
  company:    'Delicious Insights',
  trainings:  ['JS Total', 'Node.js', 'Git Total'],
  jsSince:    1995,
  claimsToFame: [
    'Prototype.js',
    'Ruby On Rails',
    'Bien Développer pour le Web 2.0',
    'Prototype and Script.aculo.us',
    'Paris Web'
  ]
};
          

En 30 minutes

Ne clignez pas des yeux !

Tenter de bien comprendre les 3 plus gros concepts-clés de JavaScript, histoire d’enfin passer la cinquième.


  1. Les prototypes (héritage compris)
  2. Les closures (fermetures lexicales)
  3. Le binding (« où est mon this ?! »)

Prototypes

Z'êtes trop classiques

JS est bien plus orienté objet que Java, C++, Python…

POO = « classique » (basique) + prototypale

La 2nde est un sur-ensemble de la 1ère.

Simplement parce qu'on n’a pas class, extends et super ne veut pas dire qu’on n’est pas objet… (cf. ES6 / CoffeeScript)

La POO prototypale

Chaque objet est construit en se basant sur un autre.

On a des langages « purs prototypes », genre io.

Pas de distinction classe / objet, pas de schéma figé.

Facilite la plupart des design patterns.

Constructeurs

Toute fonction est un constructeur potentiel.

Pas inhérent à la fonction : dû à l’appel avec new.


function Talk(options) {
  if (!(this instanceof Talk)) {
      throw new Error('Et le new, eh, patate !');
    }
  this.event    = options.event;
  this.title    = options.title;
  this.speaker  = options.speaker;
  this.startsAt = new Date(options.startsAt);
  this.duration = options.duration;
}

var talk = new Talk({
  event: 'Paris Web 2014',
  title: 'JS + toi = <3',
  speaker: 'Christophe',
  startsAt: '2014-10-16 10:30',
  duration: 30
});
            

Pensez-y comme à…

En gros, le conteneur des méthodes d’instance.

Constructeur + Prototype ≈ concept habituel de Classe.

Où sont-ils ?

Quels sont leurs réseaux ?

Toute fonction est dotée d’un prototype par JS.

Accessible via la propriété prototype de la fonction.


function foo() {}
foo.prototype // => Object {}
            

Que sont-ils ?

Leur stratégie pour la France

Par défaut, un simple Object avec une propriété constructor (non-énumérable*).

* ça veut dire invisible au for…in

Ajouter une méthode

d’instance à une « classe »



Talk.prototype.toPDF = function toPDF() {
  this.slides.forEach(…);
};


// Ou plus en série, quand on en a plein :
$.extend(Talk.prototype, {
  print: function print() { … },
  run:   function run() { … },
  toPDF: function toPDF() { … }
  …
});
            

La version débile

qu'on voit donc dans plein de tutos


function Talk(options) {
  // définition des « attributs », puis :

  this.run = function run() { … };
  this.toPDF = function toPDF() { … };
  …
}
            

plus lourd, moins pratique…

La chaîne de lookup

Passe le message à ton voisin

obj.prop ou obj['prop'], peu importe…

  1. On part de l’objet indexé (obj)
  2. Si on trouve prop dans ses own properties, on s’arrête là
  3. Sinon, on passe sur le prototype du niveau supérieur : celui du constructeur de l’objet en cours*
  4. On reprend à l’étape 2, sauf si on était déjà sur Object.prototype, auquel cas le lookup est fini, et échoue (undefined).
* conceptuellement, constructor.prototype ou __proto__

La chaîne de lookup

La preuve en images


function Person(first, last) {
  this.first = first;
  this.last = last;
}
Person.prototype.fullName = function fullName() {
  return this.first + ' ' + this.last;
};
var roiDeLaClasse = new Person('Georges', 'Abitbol');

roiDeLaClasse.first      // => 'Georges',         own property
roiDeLaClasse.fullName() // => 'Georges Abitbol', Person.prototype
roiDeLaClasse.toString() // => '[object Object]', Object.prototype

Person.prototype.toString = function personToString() {
  return '#Person<' + this.fullName() + '>';
};

roiDeLaClasse.toString() // => '#Person<George Abitbol>'
            

Lookup ➜ héritage

Version héritée

d’une méthode


Workshop.prototype.run = function runWorkshop() {
  // Pré-code

  Talk.prototype.run.apply(this, arguments); // "super()"
  // ou plus spécifique, par exemple :
  Talk.prototype.run.call(this, this.attendees); // "super(this.attendees)"

  // Post-code
};
            

Version héritée

du constructeur


function Workshop(options) {
  Talk.call(this, options);

  // Post-code, par exemple :
  this.attendees = options.attendees;
  this.resourcesURL = options.resourceURL;
  // …
};
            

Les méchants tutos

qui disent n'importe nawak


function Workshop(options) { … }
Workshop.prototype = new Talk();

$.extend(Workshop.prototype, {
  run: function runWorkshop() { … }
  …
});
            
  1. Dans la vraie vie, le constructeur parent ne peut pas être appelé comme ça ! Il prend des arguments, il fait des trucs…
  2. Conceptuellement, instancier le parent juste pour connecter les deux constructeurs, c’est juste chelou.

Le nœud du problème

  1. Il faut que le prototype du constructeur fils soit une instance d’un truc connecté au prototype parent, mais…
  2. …il ne faut pas instancier le constructeur parent !

La solution

On crée un constructeur synthétique pour l’occasion !


function inherit(Child, Parent) {
  var Synth = function() {};           // constructeur synthétique…
  Synth.prototype = Parent.prototype;  // connecté au bon prototype…
  Child.prototype = new Synth();       // le proto fils l’instancie…
  Child.prototype.constructor = Child; // mais on recale constructor
}

inherit(Workshop, Talk);
$.extend(Talk.prototype, { … });
            

Tout le monde le fait

Profitez-en


// ES5
Workshop.prototype = Object.create(Talk.prototype, {
  constructor: { value: Workshop }
});

// ES6
class Workshop extends Talk

// Node.js
require('util').inherits(Workshop, Talk);

// Klass.js
var Workshop = Talk.extend(function Workshop(options) {
  // …
}).methods({ … });

// Backbone.js, Ember.js…
var Workshop = Talk.extend({ … });
            

Closures

(« fermetures lexicales »)

Fonctions de 1er ordre

Les fonctions sont des valeurs comme les autres.


function foo() { … }
var f = foo;
var obj = { yo: foo };
            

À quoi ça sert ?

Une closure équipe une fonction d’un état privé qui a le bon goût de persister d’un appel à l’autre.

On évite de « pourrir le global »

On peut faire du vrai privé (absolument incontournable)

Les modules

à la base, sont juste des closures


(function($) {
  var widgets = {}, wId = 0;

  $(init);
  $(document).on('ui:update', init);

  function init() {
    $('*[data-widget]').each(initWidget);
  }

  function initWidget(__stupid, elt) {
    elt.id = elt.id || ('__widget' + (++wId));
    widgets[elt.id] = widgets[elt.id] || new Widget(elt);
  }
})(jQuery);
            

Tout le contenu de ce module (cette closure) est 100% privé.

On parle d’IIFE (Immediately Invoked Function Expression).

Genre, CommonJS…


function yourModule(require, module, exports) {
  var widgets = {};
  var util = require('util');
  var Widget = require('widgets/base');

  function CoolWidget(elt) { … }
  util.inherits(CoolWidget, Widget);
  // …

  module.exports = Widget;
}
            

Genre, AMD…


define('ui/cool', ['util', 'ui/base'], function(util, Widget) {
  var widgets = {};

  function CoolWidget(elt) { … }
  util.inherits(CoolWidget, Widget);
  // …

  return Widget;
});
            

Au fond, c’est de l’AOP

(Aspect-Oriented Programming)

On peut s’en servir pour plein de choses, dont beaucoup formalisées par des design patterns :

  • Décorateur
  • Façade
  • Singleton
  • Mémoïsation
  • ACL
  • Benchmarking
  • Logging
  • Throttling / Debouncing
  • etc.

3 conditions

Pour qu’une closure existe, il faut que :

  1. une fonction soit imbriquée dans une autre ;
  2. l’imbriquée utilise au moins une partie de la portée imbriquante ;
  3. l’imbriquée échappe à sa portée.

Dans la pratique, le point 3 est avéré dès qu’on renvoie la fonction imbriquée, ou qu’on la passe en paramètre à un tiers (ex. callback).

Exemple : état privé


var Talk = (function() {
  var privStatic = {}; // 100% privé

  function Talk(options) {
    // …
  }
  $.extend(Talk.prototype, {
    print: function print() { … },
    run:   function run() { … },
    toPDF: function toPDF() { … },
    …
  });

  return Talk;
})();
            

Exemple : call count


var sayHi = (function() {
  var callCount = 0;

  function sayHi() {
    console.log(++callCount, 'Hiiiiii…');
  }

  return sayHi;
})();

sayHi()   // => "1 - Hiiiiii…'
sayHi()   // => "2 - Hiiiiii…'
sayHi()   // => "3 - Hiiiiii…'
callCount // => ReferenceError
            

Exemple : mémoïsation


function memoize(fx) {
  var called = false, result;

  return function memoized() {
    if (called) {
      return result;
    }
    result = fx.apply(this, arguments);
    called = true;
    return result;
  };
}

function demo() { console.log('Yoooo'); return 42; }
var f = memoize(demo);

f(); // => 42.  Logue 'Yoooo'
f(); // => 42.  Aucun log.
f(); // => 42.  Rien à faire :-)
            

Exemple : invocateur


function get(propName) {
  return function propGetter(obj) {
    if ('function' === typeof obj[propName]) {
      return obj[propName]();
    }
    return obj[propName];
  }
}

var names = ['Alice', 'Bob', 'Claire', 'David', 'Élodie'];
var getLength = get('length');
var lowerCaser = get('toLocaleLowerCase');

names.map(getLength)  // => [5, 3, 6, 5, 6]
names.map(lowerCaser) // => ['alice', 'bob', 'claire', … 'élodie']
            

Exemple : throttling


function throttle(fx, minInterval) {
  var latestCall;

  return function throttled() {
    var now = Date.now();
    if (latestCall + minInterval > now) {
      return;
    }
    latestCall = now;
    var result = fx.apply(this, arguments);
    // Pour un debounce, on mettrait à jour latestCall ici plutôt.
    return result;
  }
}

function hiCoquine() { console.log(Date.now(), 'Hiiii…'); }

setInterval(throttle(hiCoquine, 1000), 50);
// => Seulement un Hiiii toutes les 1000+ ms, pas toutes les 50.
            

Binding

(« Où est mon this ?! »)

Tu ne me possèdes pas

Je suis une fonction liiibre !

Rappel : qui dit fonctions de premier ordre, dit qu’une fonction n’appartient pas implicitement à quelque objet/prototype que ce soit.


function foo() { … }
var f = foo;
var obj = { yo: foo };
            

Méfiez-vous des copies

Surtout celles de références


var obj1 = {
  run: function run() { … }
};

var obj2 = {
  run: obj1.run
};
            

À qui appartient la fonction maintenant ? obj1 ou obj2 ?

(Réponse : à personne, on vient de vous le dire…)

Méthodes singletons

Tout objet peut avoir une méthode qui lui est propre, sur l’instance elle-même, sans exister sur un prototype…

C’est ce qui se passe dès que vous filez un callback dans un hash d’options, par exemple :


$.ajax('/api/v1/ohai', {
  type: 'GET',
  dataType: 'json',
  success: function ohaiSucceeded(res) { … }
})
            

L’exception

qui confirme la règle…

Il y a un seul cas de binding implicite :

sujet + verbe + complément

(Objet + indexation + appel immédiat)


var abrasiveGuy = {
  name: 'Linus',
  review: function review() {
    console.log("I am " + this.name + " and I say your code is shit!");
  }
};

abrasiveGuy.review();
abrasiveGuy['review']();
            

Binding par défaut

Dans tous les autres cas

Tout le reste = on référence la fonction sans l’appeler.

Mode laxiste : this par défaut (window ou global).

Mode strict : this est undefined.


var name = 'a troll';
var harshReview = abrasiveGuy.review;
harshReview() // => "I am a troll and I say your code is shit!"

setTimeout(abrasiveGuy.review, 0);
// => "I am a troll and I say your code is shit!"

$(document).on('click', abrasiveGuy.review);
// => "I am undefined and I say your code is shit!"

// Si la fonction review avait été définie en mode strict :
// => Uncaught TypeError: Cannot read property 'name' of undefined
            

Spécifier le binding

Sinon on serait dans la m…ouise

Si JS est cohérent, l’absence de binding implicite exige qu’il fournisse de quoi être explicite, donc un moyen programmatique de spécifier this.

JS est cohérent :-)

Les fonctions, qui sont des objets (elles sont des instances de Function), ont deux méthodes* : apply et call, qui servent à ça.

* Oui, les fonctions ont des méthodes. Respire, ça va passer.

Call me maybe

call : quand tu connais la sémantique des arguments.

On l’a utilisé tout à l’heure pour appeler les versions héritées des méthodes et constructeurs, tu te rappelles ?

theFunction.call(thisObj [, arg1 [, arg2…]])


var cryptoArray = { 0: 'JS', 1: 'ça', 2: 'torche', length: 3, x: 42 };

Array.prototype.join.call(cryptoArray, ' ') // => 'JS ça torche'
            

Les arguments sont passés à la volée, il faut donc en connaître le nombre et le sens.

fx.call(obj, 1, 2)obj.fx(1, 2)

apply

Quand tu ne connais pas à l’avance la sémantique des arguments. Donc pour du code générique, genre AOP.

theFunction.apply(thisObj, [arg1]])


function genMethodCall(name) {
  var args = Array.prototype.slice.call(arguments, 1);

  return function methodCall(obj) {
    return obj[name].apply(obj, args);
  };
}

var stripSides = genMethodCall('slice', 1, -1);

['<strong>', '<em>', '<code>'].map(stripSides)
// => ['strong', 'em', 'code']
            

fx.apply(obj, [1, 2])obj.fx(1, 2)

La vigilance constante

j’ai comme un doute

Si on doit penser à faire le call ou le apply à chaque fois, on peut toujours se brosser…

Sans compter que dans bien des cas on n’aura pas la référence de l’objet approprié, juste la référence de la fonction à appeler in fine.

Du coup que faire ?

Binding persistant ?

En voilà une idée qu’elle est bonne

C’est un peu le boss de fin de niveau : on reprend plein de concepts déjà vus, on mélange, et hop !

  1. On prend la méthode d’origine (par exemple, sur le prototype, ou une méthode singleton sur l’instance)
  2. On la remplace sur l’instance par un enrobage qui se souviendra le moment venu qu’il faut assurer le binding.

OK, et donc côté code, ça donne quoi ?

bind


Function.prototype.bind = function bind(context) {
  var fx = this;

  return function boundFx() {
    return fx.apply(context, arguments);
  };
};

var harshReview = abrasiveGuy.review.bind(abrasiveGuy);
setTimeout(harshReview, 0) // => 'I am Linus and I say your code…'
            
(on ne s’occupe ici que du côté binding pur, la plupart des implémentations existantes permettent aussi l’application partielle)

Bind classique

Du prototype vers l’instance


function Widget(elt) {
  this.$elt = $(elt);
  this.$elt.on('click', this.handleClick.bind(this));
  …
}

$.extend(Widget.prototype, {
  handleClick: function handleClick(e) { … },
  …
});
            

Tout le monde le fait

Puisqu’on vous dit d’en profiter !


// ES5, Prototype.js, Ext
this.review = this.review.bind(this);

// Underscore.js
this.review = _.bind(this.review, this);
_.bindAll(this, 'review'); // pour plein d’un coup…

// jQuery
this.review = $.proxy(this.review, this);

// YUI
this.review = Y.bind(this.review, this);

// Dojo
this.review = dojo.hitch(this, 'review'); // module lang si > 1.7

// ES6
review: => console.log(`I am ${this.name} and I say your code is …!`)
            

Envie d’en savoir plus ?

On fait des super formations de ouf sur
Git, JavaScript et le dev web front et Node.js.


Et ce qui est encore plus cool, c’est que pour l’auditoire Paris Web, c’est −15% jusqu’à fin janvier :

bit.ly/pw14love

Merci !

Et que JS soit avec vous


Christophe Porteneuve

@porteneuve

Retrouvez les slides sur bit.ly/jsyoulove