Organizacja ucząca się Javascript - wprowadzenie 07-03-2014 Wiktor Zychla
Agenda Wprowadzenie Funkcje/domknięcia Obiekty this Dziedziczenie Enkapsulacja Perspektywy Egzamin
Wprowadzenie - historia 1995 Brendan Eich dla Netscape 1996 ECMA 262 1996 Microsoft wprowadza do IE3 1998 ES2 1999 ES3 2009 ES5 2011 ES5.1 201? ES6 http://kangax.github.io/es5-compat-table/
Host Kod Javascript wykonuje się w ramach hosta, typowo jest to przeglądarka. Do obiektu hosta jest dostęp jawny: window.x = 3; x = 3; // równoważne, zmienna globalna Zmienne lokalne var x = 3;
Host Host daje dostęp do struktury dokumentu który jest aktualnie załadowany (DOM). Javascript może tę strukturę dynamicznie modyfikować. Obecnie większość przeglądarek ma silniki kompilujące Javascript do kodu natywnego przed wykonaniem. Javascript nie tylko jest więc coraz bardziej zgodny, ale też jest bardzo szybki. http://www.wiktorzychla.com/2014/02/animated-javascript-juliafractals.html http://jsdosbox.appspot.com/
Funkcje Funkcje nazwane/nienazwane function() { function f() {
Funkcje Funkcje zdefiniowane w compile-time vs run-time f(); function f() { vs f(); var f = function() {
Zasięg widoczności zmiennych Globalny var x = 5; var f = function() { console.log('x='+x); f();
Zasięg widoczności zmiennych Lokalny var x = 5; var f = function() { var x = 1; console.log('x='+x); f();
Zasięg widoczności zmiennych Uwaga na hoisting var x = 5; var f = function() { console.log('x='+x); var x = 3; f();
Zasięg widoczności zmiennych Hoisting polega na tym, że w zasięgu bloku wszystkie zmienne są wirtualnie zdefiniowane na początku bloku. var x = 5; var f = function() { var x; console.log('x='+x); x = 3; f();
Funkcje Javascript jest językiem funkcyjnym funkcje mogą być przekazywane do funkcji jako argumenty i zwracane jako wyniki.
Funkcje Funkcja przekazywana jako argument function f(g) { return g(); f(function() { return 1; );
Funkcje Funkcja zwracana jako wartość function f() { function g() { return 1; return g; f()();
Domknięcia Domknięcie to ukryty parametr funkcji, który łapie wszystkie zmienne z których funkcja korzysta, ale nie definiuje ich. function sum1(x,y) { return x + y; console.log( sum1(2,3) );
Domknięcia To samo z domknięciem function sum2(x) { function helper(y) { return x + y; return helper; console.log(sum2(2)(3)); Kontekst funkcji wewnętrznej helper domyka zmienną x.
Domknięcia Ekstremalnie: function sum3(x) { var _s = x; function helper(y) { _s += y; return helper; helper.tostring = function() { return _s; return helper; var result = sum3(1)(2)(3)(4); // dowolna liczba parametrów console.log(result);
Domknięcia Kontekst jest obiektem, więc może być modyfikowany: function f(person) { function g(message) { console.log( 'message [' + message + '] to ' + person.name ); return g; var p = { name : 'jan' var messenger = f(p); messenger('witaj!'); // witaj to jan p.name = 'tomasz'; messenger('ukłony'); // ukłony to tomasz
Domknięcia Co czasem bywa nieoczekiwane function createfs(n) { // tworzy tablicę n funkcji var fs = []; // i-ta funkcja z tablicy ma zwrócić i var i; for ( i=0; i<n; i++ ) { fs[i] = function() { return i; ; // <- funkcja się nie wykonuje, jest obiekt kontekstu do którego trafia i // ale wartość i się zmienia zanim funkcje zaczną się wykonywać ; return fs; var myfs = createfs(10); console.log( myfs[0]() ); // zerowa funkcja miała zwrócić 0 console.log( myfs[2]() ); // druga miała zwrócić 2 console.log( myfs[7]() ); // output : 10,10,10, oops, wszystkie zwracają 10!
Domknięcia Ale nietrudno to naprawić function createfs(n) { var fs = []; var i; for ( i=0; i<n; i++ ) { fs[i] = function(x) { return function() { return x; ; (i); // <- funkcja od razu się wykonuje i za każdym razem tworzy nowy kontekst // (n różnych kontekstów vs jeden i ten sam) ; return fs; var myfs = createfs(10); console.log( myfs[0]() ); console.log( myfs[2]() ); console.log( myfs[7]() ); // output: 0, 2, 7
Obiekty Najprościej var p = {; p.name = 'jan'; console.log(p.name); // albo new Object(); Notacja. vs [] var p = { p.name = 'jan'; p['name'] = 'tomasz'; console.log(p.name); Notacja [] jest bardziej ogólna bo parametr może być wyrażeniem var p = { p.name = 'jan'; p['na'+'me'] = 'tomasz'; console.log(p.name);
Obiekty Składnia literalna (literal syntax) var p = { name : 'jan', age : 15 console.log( p.name ); Drzewa obiektów (obiekty kompozytowe) var p = { name : 'jan', address : { city : 'wrocław' console.log( p.address.city );
Obiekty - dygresja Iteracja po tablicy vs iteracja po polach obiektu var a = []; a.push(1); a.push(2); for ( var i=0; i<a.length; i++ ) console.log( a[i] ); vs var p = { name : 'jan', age : 17 for ( var key in p ) { console.log( key + ' : ' + p[key] );
Obiekty - dygresja Parametry są przekazywane przez wartość function f( x ) { x = 123; var a = 1; f(a); console.log(a); // 1. funkcja nie modyfikuje wartości czyli w przypadku obiektów przez wartość referencji function f( x ) { x.age = 123; var p = { age : 1 ; f(p); console.log(p.age); // 123. // referencja nie jest modyfikowana ale pole za nią tak
This v.1 W funkcjach instancji obiektu this odnosi się do instancji obiektu var person = { name : 'jan', say : function() { return this.name; console.log( person.say() );
This v.2 W funkcjach spoza obiektów this odnosi się domyślnie do hosta (window) var name = 'jan'; say = function() { return this.name; console.log( say() );
This v.2 ale można wywołać funkcję nie wprost, tylko przez call, które jako pierwszy parametr przyjmuje obiekt dowiązywany to this wewnątrz funkcji p = { name : 'jan' ; say = function() { return this.name; console.log( say.call( p ) );
This v.2 - dygresja Wywołania przez call a apply p = { name : 'jan' ; say = function(m1, m2) { return this.name + ' has ' + m1 + ' and ' + m2; console.log( say.call( p, 'a', 'b' ) ); // lista parametrów p = { name : 'jan' ; say = function(m1, m2) { return this.name + ' has ' + m1 + ' and ' + m2; console.log( say.apply( p, ['a', 'b'] ) ); // tablica parametrów
Dziedziczenie W Javascript nie ma klas. Tyle że w języku obiektowym nie potrzeba klas potrzeba obiektów. Nowe obiekty tworzy się albo ad-hoc albo klonując istniejące wzorcowe obiekty mówimy o nich prototypy. var personproto = { name : 'jan', say : function() { return this.name; // prototyp var p1 = Object.create( personproto ); // klonowanie prototypu p1.name = 'tomasz'; var p2 = Object.create( personproto ); p2.name = 'janusz'; console.log( p1.say() ); console.log( p2.say() );
Dziedziczenie Nieoczekiwany problem - obiekt kompozytowy w prototypie var personproto = { name : 'jan', address : { city : 'wrocław', say : function() { return this.address.city; var p1 = Object.create( personproto ); p1.address.city = 'kraków'; var p2 = Object.create( personproto ); p2.address.city = 'łódź'; console.log( p1.say() ); console.log( p2.say() ); // output : łódź, łódź zamiast kraków, łódź Jak to obejść?
Dziedziczenie Można tak var personproto = { name : 'jan', address : { city : 'wrocław', say : function() { return this.address.city; var p1 = Object.create( personproto ); p1.address = { city : 'kraków' ; // za każdym razem twórz nowy podobiekt var p2 = Object.create( personproto ); p2.address = { city : 'łódź' ; console.log( p1.say() ); console.log( p2.say() ); Ale to nie wygląda dobrze, bo trzeba o tym pamiętać.
Dziedziczenie Rozwiązaniem jest taki prototyp, który klonuje się dedykowaną metodą tworzącą (factory function): var person = { create : function(name, city) { var _ = Object.create( this ); _.name = name; _.address = { city : city ; return _;, say : function() { return this.address.city; var p1 = person.create( 'jan', 'kraków' ); var p2 = person.create( 'tomasz', 'łódź' ); console.log( p1.say() ); console.log( p2.say() );
Dziedziczenie W ten sposób można zaimplementować dziedziczenie var person = { create : function(name, city) { var _ = Object.create( this ); _.name = name; _.address = { city : city ; return _;, say : function() { return this.address.city; var pupil = { create : function(name, city, school) { var _ = person.create.call( this, name, city ); _.school = school; return _;, say : function() { var _ = person.say.call( this ); return _ + ' ' + this.school; var p1 = person.create( 'jan', 'kraków' ); var p2 = pupil.create( 'tomasz', 'łódź', 'sp1' ); console.log( p1.say() ); console.log( p2.say() );
Dziedziczenie Wszystko pięknie tylko co się stanie jeśli pupil nie ma dostarczonej implementacji metody say, a zamiast tego oczekujemy że zawoła się metoda z prototypu (mówiąc żargonem obiektowym: z klasy bazowej)? Zakomentowanie metody say w pupil pokazuje że całość przestaje działać. pupil jako prototyp nie ma metody say! Można próbować to ratować: var person = { create : function(name, city) { var _ = Object.create( person ); _.name = name; _.address = { city : city ; return _;, say : function() { return this.address.city; ale wtedy przestaje działać przeciążanie metody say w pupil
Dziedziczenie Prawdziwym źródłem problemu jest to, że pupil nie powstał z person w taki sam sposób w jaki konstruujemy pozostałe obiekty przez sklonowanie. Zamiast tego zarówno pupil jak i person powstały w sposób literalny. Niestety, dostęp do prototypu obiektu nie jest jawny (jeśli się da to nie jest to standard). Nie można więc tego łatwo naprawić. Jedyny uniwersalny sposób to wprowadzić nieładną funkcję do klonowania prototypów i rozszerzania ich o dodatkowe właściwości: var extend = function(base, extension) { // klonuj prototyp var object = Object.create(base); // wkopiuj mu rozszerzone właściwości var hasownproperty = Object.hasOwnProperty; for (var property in extension) if (hasownproperty.call(extension, property) typeof object[property] === "undefined") object[property] = extension[property]; ; return object;
Dziedziczenie W takim środowisku rozszerzanie obiektów (dziedziczenie) działa tak jak powinno: var pupil = extend( person, { create : function(name, city, school) { var _ = person.create.call( this, name, city ); _.school = school; return _; //, //say : function() { // var _ = person.say.call( this ); // return _ + ' ' + this.school; // ); Metodę say można zakomentować (i wtedy działa implementacja z prototypu, z person) albo odkomentować (i wtedy działa implementacja z pupil). Wiele frameworków mimo to podąża tą ścieżką.
Dziedziczenie Ale dziedziczenie można osiągnąć inaczej, przez funkcje konstruktorowe (constructor function) function Foo() {... var f = new Foo(); to jest równoważne var f = new Object(); f.[[prototype]] = Foo.prototype; f.foo(); // binduje this gdzie Foo.prototype to prototyp który funkcja konstruktorowa przypina jako prototyp do nowo tworzonego obiektu. W przeciwieństwie do poprzedniego podejścia, prototyp ten jest jawny i można go modyfikować.
Dziedziczenie Mamy wtedy function Person(name, city) { this.name = name; this.address = { city : city ; this.say = function() { return this.name + ' ' + this.address.city; ; object Prototype1 p1 name = name say = function Person.prototype p2 name = name say = function var p1 = new Person('jan', 'wrocław'); console.log( p1.say() );
Dziedziczenie albo (uwaga!) function Person(name, city) { this.name = name; this.address = { city : city ; Person.prototype.say = function() { return this.name + ' ' + this.address.city; ; object Prototype2 Person.prototype say = function p1 p2 name = name name = name var p1 = new Person('jan', 'wrocław'); console.log( p1.say() );
Dziedziczenie Dziedziczenie jest łatwiejsze: function Person(name, city) { this.name = name; this.address = { city : city ; Person.prototype.say = function() { return this.name + ' ' + this.address.city; ; function Pupil(name, city, school) { Person.call( this, name, city ); this.school = school; var p1 = new Person('jan', 'wrocław'); console.log( p1.say() ); var p2 = new Pupil('tomasz', 'łódź', 'sp1'); console.log( p2.say() ); // problem! Oops, nie działa say w pupil!
Dziedziczenie Tym razem obrona jest łatwa: class Javascript Person.prototype Pupil.prototype p2 function Person(name, city) { this.name = name; this.address = { city : city ; Person.prototype.say = function() { return this.name + ' ' + this.address.city; ; function Pupil(name, city, school) { Person.call( this, name, city ); this.school = school; Pupil.prototype = new Person(); // lub Pupil.prototype = Person.prototype jeżeli metody są ustawiane tylko w prototypie var p1 = new Person('jan', 'wrocław'); console.log( p1.say() ); var p2 = new Pupil('tomasz', 'łódź', 'sp1'); console.log( p2.say() );
Dziedziczenie Wywołanie metody z klasy bazowej jest łatwe: function Person(name, city) { this.name = name; this.address = { city : city ; Person.prototype.say = function() { return this.name + ' ' + this.address.city; ; function Pupil(name, city, school) { Person.call( this, name, city ); this.school = school; Pupil.prototype = new Person(); Pupil.prototype.say = function() { var _ = Person.prototype.say.call( this ); return _ + ' ' + this.school; var p1 = new Person('jan', 'wrocław'); console.log( p1.say() ); var p2 = new Pupil('tomasz', 'łódź', 'sp1'); console.log( p2.say() );
Enkapsulacja Składowe prywatne w klasie przez domknięcie function Person(name) { var _private = 5; this.name = name; this.say = function() { return this.name + ' ' + _private; var p = new Person('jan'); console.log( p.say() ); console.log( p._private ); // niedostępne
Enkapsulacja Moduły zagnieżdżone obiekty function CreateNamespace( modulename ) { var parent = window; var seg = modulename.split('.'); for ( var part=0; part<seg.length; part++ ) { var segname = seg[part]; if ( typeof parent[segname] === 'undefined' ) parent[segname] = {; parent = parent[segname]; CreateNamespace( 'Vulcan.Architekt.CentralneSlowniki' );
Enkapsulacja I teraz Vulcan.Architekt.CentralneSlowniki = { // klasa publiczna Person : function(name) { this.name = name; this.say = function() { return name; var p = new Vulcan.Architekt.CentralneSlowniki.Person('jan'); console.log( p.say() );
Enkapsulacja Można nawet mieć w module prywatną klasę: Vulcan.Architekt.CentralneSlowniki = (function() { // klasa prywatna bo w domknięciu funkcji która się od razu wykonuje function Private() { this.helper = function(s) { return s + ' from helper!'; return { // klasa publiczna Person : function(name) { this.name = name; this.say = function() { var _pr = new Private(); return _pr.helper(this.name); ()); var p = new Vulcan.Architekt.CentralneSlowniki.Person('jan'); console.log( p.say() ); var q = new Vulcan.Architekt.CentralneSlowniki.Private(); // nie
ECMAScript 6 Wiele fajnych nowych rzeczy, m.in. lambda wyrażenia: var a = [ "Wlazł kotek na płotek", "I mruga" ]; // po staremu var a2 = a.map(function(s){ return s.length ); // po nowemu var a3 = a.map( s => s.length );
Podsumowanie Javascript to nowoczesny i wydajny język tworzenia aplikacji. Jedynym minusem jest uboga biblioteka standardowa, co oznacza, że trzeba wspierać się zewnętrznymi frameworkami.
Dziękuję za uwagę