Une présentation de Christophe Porteneuve à Paris Web 2014
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'
]
};
Tenter de bien comprendre les 3 plus gros concepts-clés de JavaScript, histoire d’enfin passer la cinquième.
this ?! »)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)
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.
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
});
En gros, le conteneur des méthodes d’instance.
Constructeur + Prototype ≈ concept habituel de Classe.
Toute fonction est dotée d’un prototype par JS.
Accessible via la propriété prototype de la fonction.
function foo() {}
foo.prototype // => Object {}
Par défaut, un simple Object avec une propriété constructor (non-énumérable*).
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() { … }
…
});
function Talk(options) {
// définition des « attributs », puis :
this.run = function run() { … };
this.toPDF = function toPDF() { … };
…
}
plus lourd, moins pratique…
obj.prop ou obj['prop'], peu importe…
obj)prop dans ses own properties, on s’arrête làObject.prototype, auquel cas le lookup est fini, et échoue (undefined).
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>'
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
};
function Workshop(options) {
Talk.call(this, options);
// Post-code, par exemple :
this.attendees = options.attendees;
this.resourcesURL = options.resourceURL;
// …
};
function Workshop(options) { … }
Workshop.prototype = new Talk();
$.extend(Workshop.prototype, {
run: function runWorkshop() { … }
…
});
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, { … });
// 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({ … });
Les fonctions sont des valeurs comme les autres.
function foo() { … }
var f = foo;
var obj = { yo: foo };
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)
(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).
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;
}
define('ui/cool', ['util', 'ui/base'], function(util, Widget) {
var widgets = {};
function CoolWidget(elt) { … }
util.inherits(CoolWidget, Widget);
// …
return Widget;
});
On peut s’en servir pour plein de choses, dont beaucoup formalisées par des design patterns :
Pour qu’une closure existe, il faut que :
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).
var Talk = (function() {
var privStatic = {}; // 100% privé
function Talk(options) {
// …
}
$.extend(Talk.prototype, {
print: function print() { … },
run: function run() { … },
toPDF: function toPDF() { … },
…
});
return Talk;
})();
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
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 :-)
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']
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.
this ?! »)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 };
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…)
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) { … }
})
Il y a un seul cas de binding implicite :
(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']();
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
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.
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)
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)
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 ?
C’est un peu le boss de fin de niveau : on reprend plein de concepts déjà vus, on mélange, et hop !
OK, et donc côté code, ça donne quoi ?
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…'
function Widget(elt) {
this.$elt = $(elt);
this.$elt.on('click', this.handleClick.bind(this));
…
}
$.extend(Widget.prototype, {
handleClick: function handleClick(e) { … },
…
});
// 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 …!`)
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 :

Christophe Porteneuve
Retrouvez les slides sur bit.ly/jsyoulove