Upload
andreygeonya
View
1.701
Download
4
Embed Size (px)
DESCRIPTION
Citation preview
Как программировать на JavaScript и не выстрелить
себе в ногу
Андрей Геоня29.12.2011
С чем имеем дело?
● ОО подход основан на прототипах;● Отсутствие пространств имен;● Отсутствие ограничения области видимости;● Отсутствие строгой типизации;● Отсутствие проверки статических элементов.
Прототипное программированиеfunction Human() { console.log('Human constructor called');}Human.prototype = { sayHuman: function() { alert('I am a Human!'); }, sayHello: function() { alert('Hello world!'); }}// human.__proto__ = instance of Objectvar human = new Human();
function Man() { console.log('Man constructor called');}Man.prototype = new Human();Man.prototype.sayMan = function() { alert('I am a Man!');}
function Woman() { console.log('Woman constructor called');}Woman.prototype = new Human();Woman.prototype.sayWoman = function() { alert('I am a Woman!');}
var peter = new Man(); // peter.__proto__ = instance of Humanvar olga = new Woman(); // olga.__proto__ = instance of Human
Интерфейсы. Классика
interface MyInterface1 { public function add($param1); public function remove($param1);}interface MyInterface2 { public function display($param1, $param2);}
class MyClass implements MyInterface1, MyInterface2 { public function add($param1) { ... } public function remove(param1) { ... } public function display($param1, $param2) { ... }}
Эмуляция интерфейсов в JS с помощью комментариев
/*interface MyInterface1 { function add(param1); function remove(param1);}interface MyInterface2 { function display(param1, param2);}*/
var MyClass = function() { // implements MyInterface1, MyInterface2 ... };MyClass.prototype.add = function(param1) { ... };MyClass.prototype.remove = function(param1) { ... };MyClass.prototype.display = function(param1, param2) { ... };
Эмуляция интерфейсов в JS с помощью простой проверки
/*interface MyInterface1 { function add(param1); function remove(param1);}interface MyInterface2 { function display(param1, param2);}*/
var MyClass = function() { this.implementsInterfaces = ['MyInterface1', 'MyInterface2']; ... };
function someClient(someInstance) { if(!implements(someInstance, 'MyInterface1', 'MyInterface2')) { throw new Error("Object does not implement a required interface."); } ...}
// Проверяет, есть ли в массиве object.implementsInterfaces нужные названия интерфейсовfunction implements(object) { ...}
Эмуляция интерфейсов в JS. Объекты + проверка методов
var MyInterface1 = new Interface('MyInterface1', ['add', 'remove']);var MyInterface2 = new Interface('MyInterface2', ['display']);
var MyClass = function() { // implements MyInterface1, MyInterface2 ... };
function someClient(someInstance) { // выбросит Exception, если в object отсутствуют нужные методы Interface.ensureImplements(someInstance, MyInterface1, MyInterface2)); ...}
Конструктор Interface
var Interface = function(name, methods) { if(arguments.length != 2) { throw new Error("Interface constructor called with " + arguments.length + "arguments, but expected exactly 2."); } this.name = name; this.methods = []; for(var i = 0, len = methods.length; i < len; i++) { if(typeof methods[i] !== 'string') { throw new Error("Interface constructor expects method names to be passed in as a string."); } this.methods.push(methods[i]); }};
Статический метод Interface.ensureImplementsInterface.ensureImplements = function(object) { if(arguments.length < 2) { throw new Error("Function Interface.ensureImplements called with " + arguments.length + "arguments, but expected at least 2."); } for(var i = 1, len = arguments.length; i < len; i++) { var interface = arguments[i]; if(interface.constructor !== Interface) { throw new Error("Function Interface.ensureImplements expects arguments" + "two and above to be instances of Interface."); } for(var j = 0, methodsLen = interface.methods.length; j < methodsLen; j++) { var method = interface.methods[j]; if(!object[method] || typeof object[method] !== 'function') { throw new Error("Function Interface.ensureImplements: object " + "does not implement the " + interface.name + " interface. Method " + method + " was not found."); } } }};
Инкапсуляция на соглашенияхvar IHuman = new Interface('Human', ['getSurname', 'setSurname', 'getName', 'setName']);
var Human = function (surname, name) { this.setSurname(surname); this.setName(name);}Human.prototype = { getSurname: function() { return this._surname; }, setSurname: function(surname) { if (!this._isset(surname)) { throw new Error('Surname is required') }; this._surname = surname; }, getName: function() { return this._name; }, setName: function(name) { this._name = name || null; }, _isset: function(s) { if (s == undefined || typeof s != 'string') { return false; } else { return true; } }}
ЗамыканияЗамыкание — это особый вид функции. Она определена в теле другой функции и создаётся каждый раз во время её выполнения. При этом вложенная внутренняя функция содержит ссылки на локальные переменные внешней функции. Каждый раз при выполнении внешней функции происходит создание нового экземпляра внутренней функции, с новыми ссылками на переменные внешней функции.
function foo() { var a = 10; function bar() { a *= 2; return a; } return bar;}
var baz = foo(); // baz is now a reference to function barbaz(); // returns 20baz(); // returns 40baz(); // returns 80
var blat = foo(); // blat is another reference to barblat(); // returns 20
Инкапсуляция на замыканияхvar Human = function (newSurname, newName) { var name, surname; function isset(s) { if (s == undefined || typeof s != 'string') { return false; } else { return true; } }; // Privileged methods this.getSurname = function() { return surname; }; this.setSurname = function(newSurname) { if (!isset(newSurname)) { throw new Error('Surname is required') }; surname = newSurname; }; this.getName = function() { return name; }; this.setName = function(newName) { name = newName || null; }; // Constructor code this.setSurname(newSurname); this.setName(newName);}
Human.prototype = { // Public, non-privileged methods talk: function() { alert(this.getName() + this.getSurname()); }};
Статические атрибуты и методыvar Human = (function () { // Private static attribute and method var count = 0; function isset(s) { ... };
// Return the constructor return function (newSurname, newName) { // Private attributes var name, surname;
// Privileged methods this.getSurname = function() { ... }; this.setSurname = function(newSurname) { ... }; this.getName = function() { ... }; this.setName = function(newName) { ... }; // Constructor code count++; this.setSurname(newSurname); this.setName(newName); }})();
// Public static methodHuman.toUpperCase = function(s){ ... }
Human.prototype = { // Public, non-privileged methods talk: function() { ... }};
Константы - приватные статические атрибуты с getter-ом
var Human = (function () { // Constant (created as private static attribute) var CLASS_NAME = 'Human';
// Privileged static method this.getCLASS_NAME() { return CLASS_NAME; }
// Return the constructor return function (newSurname, newName) { ... }})();
Приватный объект с константами и getter-ом
var Human = (function () { // Private static attributes. var constants = { CLASS_NAME: 'Human', SOME_CONSTANT_1: 'Some value 1', SOME_CONSTANT_2: 'Some value 2' }
// Privileged static method. this.getConstant(name) { return constants[name]; }
// Return the constructor return function (newSurname, newName) { ... }})();
Эмуляция классического наследования
/* Class Person */function Person(name) { this.name = name;}Person.prototype.getName = function() { return this.name;}
/* Class Author */function Author(name, books) { Person.call(this, name); // Call the superclass's constructor in the scope of this this.books = books; // Add an attribute to Author}Author.prototype = new Person(); // Set up the prototype chainAuthor.prototype.constructor = Author; // Set the constructor attribute to AuthorAuthor.prototype.getBooks = function() { // Add a method to Author return this.books;}
var author1 = new Author('Jeff Six', ['Application Security for the Android Platform']);var author2 = new Author('Vandad Nahavandipoor', ['iOS 5 Programming Cookbook']);
Эмуляция классического наследования. Функция extendfunction extend(subClass, superClass) { var F = function() {}; F.prototype = superClass.prototype; subClass.prototype = new F(); subClass.prototype.constructor = subClass; subClass.superclass = superClass.prototype; if(superClass.prototype.constructor == Object.prototype.constructor) { superClass.prototype.constructor = superClass; }}/* Class Person */function Person(name) { this.name = name;}Person.prototype.getName = function() { return this.name;}/* Class Author. */function Author(name, books) { Author.superclass.constructor.call(this, name); this.books = books;}extend(Author, Person);Author.prototype.getBooks = function() { return this.books;};var author1 = new Author('Jeff Six', ['Application Security for the Android Platform']);var author2 = new Author('Vandad Nahavandipoor', ['iOS 5 Programming Cookbook']);
Инкапсуляция при эмуляции классического наследования
● В подклассе имеется доступ только к публичным и привилегированным членам родительского класса;
● Приватные члены родительского класса доступны в подклассах, а также извне только через привилегированные методы самого родительского класса;
● Если есть необходимость в protected членах, принято соглашение использовать подчеркивание, например var _myProtectedVar = null;
Прототипное наследование /* Clone function */ function clone(object) { function F() {} F.prototype = object; return new F; }
/* Person Prototype Object */ var Person = { name: 'default name', getName: function() { return this.name; } }
/* Author Prototype Object */ var Author = clone(Person); Author.books = []; // Default value Author.getBooks = function() { return this.books; }
var author1 = clone(Author); // Author-like object author1.name = 'Jeff Six'; author1.books = ['Application Security for the Android Platform']; var author2 = clone(Author); // Author-like object author2.name = 'Vandad Nahavandipoor'; author2.books = ['iOS 5 Programming Cookbook'];
Mixin/* Augment function. */function augment(receivingClass, givingClass) { for(var methodName in givingClass.prototype) { if(!receivingClass.prototype[methodName]) { receivingClass.prototype[methodName] = givingClass.prototype[methodName]; } }}
/* Regular class */var Class1 = function() {};Class1.prototype = { method1: function() {}, method2: function() {}}
/* Mixin class */var Class2 = function() {};Class2.prototype = { mixinMethod1: function() { alert('mixinMethod1 called'); }, mixinMethod2: function() { alert('mixinMethod2 called'); }}; augment(Class1, Class2);var myObj = new Class1();myObj.mixinMethod1();
Singleton/* Basic Singleton. */var Singleton = { attribute1: true, attribute2: 10, method1: function() { ... }, method2: function(arg) { ... }};
/* Singleton with Private Members */MyNamespace.Singleton = (function() { // Private members var privateAttribute1 = false; var privateAttribute2 = [1, 2, 3]; function privateMethod1() { ... } function privateMethod2(args) { ... }
return { // Public members publicAttribute1: true, publicAttribute2: 10, publicMethod1: function() { ... }, publicMethod2: function(args) { ... } };
})();
"Ленивое" инстанцированиеMyNamespace.Singleton = (function() { var instance; // All of the singleton code here function constructor() { var privateAttribute1 = false; function privateMethod1() { ... } return { publicAttribute1: true, publicMethod1: function() { ... } } } return { // Public members getInstance: function() { if(!instance) { instance = constructor(); } return instance; } }})();MyNamespace.Singleton.getInstance().publicMethod1(); // Call singleton method
Простая Фабрикаvar CarFactory = { createCar: function(model) { var car; switch(model) { case 'bmw': car = new BMW(); break; case 'ford': car = new Ford(); break; case 'ferrari': default: car = new Ferrari(); } Interface.ensureImplements(car, Car); return car; }};
Фабричный метод - паттерн, порождающий классы
/* CarShop class (abstract). */var CarShop = function() {};CarShop.prototype = { sellCar: function(model) { var car = this.createCar(model); car.prepare(); car.wash(); return car; }, createCar: function(model) { throw new Error('Unsupported operation on an abstract class'); }};
● Используем абстрактный класс;● Используем метод extend: extend(FordCarShop, CarShop);● Используем при необходимости метод ensureImplements: Interface.ensureImplements
(car, Car).
Компоновщик. Пример использования - запоминание форм
var contactForm = new CompositeForm('contact-form', 'POST', 'contact.php');
var nameFieldset = new CompositeFieldset('name-fieldset');nameFieldset.add(new InputField('first-name', 'First Name'));nameFieldset.add(new InputField('last-name', 'Last Name'));contactForm.add(nameFieldset);
var addressFieldset = new CompositeFieldset('address-fieldset');addressFieldset.add(new InputField('address', 'Address'));addressFieldset.add(new InputField('city', 'City'));addressFieldset.add(new SelectField('state', 'State', stateArray));contactForm.add(addressFieldset);
contactForm.add(new TextareaField('comments', 'Comments'));
body.appendChild(contactForm.getElement());
addEvent(window, 'unload', contactForm.save);addEvent(window, 'load', contactForm.restore);addEvent('save-button', 'click', nameFieldset.save);addEvent('restore-button', 'click', nameFieldset.restore);
Декораторы функцийВыглядит это так:
function upperCaseDecorator(func) { return function() { return func.apply(this, arguments).toUpperCase(); }}
Можно применять с пользой:
var gCollection = new YMaps.GeoObjectCollection();gCollection = new TimeProfiler(gCollection); // Декорируем все методы профайлером
gCollection.add([ new YMaps.Placemark(new YMaps.GeoPoint(37.518234, 55.708937)), new YMaps.Placemark(new YMaps.GeoPoint(37.514146, 55.722294)), new YMaps.Placemark(new YMaps.GeoPoint(37.514146, 55.722225)), new YMaps.Placemark(new YMaps.GeoPoint(37.514146, 55.722236))]);gCollection.removeAll();
С помощью декоратора TimeProfiler, в консоли будет напечатано время выполнения методов add и removeAll.
Тренируем внимание● Расширение встроенных прототипов языка - зло;● Не забываем писать var;● Учитываем зависимость производительности от длины цепочек прототипов;● Цикл for in проходит по всей цепочке прототипов. Решение - hasOwnProperty;● JavaScript не резервирует свойство с именем hasOwnProperty, но можно делать
так: ({}).hasOwnProperty.call(obj, 'propertyName');● Обходить обычный массив с помощью for in - зло;● Ключевое слово this ссылается на контекст вызова, но его можно явно задать при
call и apply;● arguments не является наследником Array, у него нет push, pop, slice и т.д.;● Помним про высасывание определений вверх ближайшей области видимости;● Используем instanceof только собственных типов. Для стандартных типов - зло;● eval - зло: а) безопасность; б) косвенный вызов и выход из локального scope;● for(var i = 0; i < 10; i++) {
setTimeout(function() { console.log(i); }, 1000); } Числа 0-9 не будут напечатаны, так как анонимная функция сохраняет ссылку на переменную i, которая в момент вызова будет равна 10. Решение: for(var i = 0; i < 10; i++) { (function(e) { setTimeout(function() { console.log(e); }, 1000); })(i); }
Сравниваем осторожно
Оператор нестрогого равенства: Оператор строгого равенства:
�"" == "0" false
0 == "" true
0 == "0" true
false == "false" false
false == "0" true
false == undefined false
false == null false
null == undefined true
" \t\r\n" == 0 true
"" === "0" false
0 === "" false
0 === "0" false
false === "false" false
false === "0" false
false === undefined false
false === null false
null === undefined false
" \t\r\n" === 0 false
Внимание, оператор typeof
Результат работы typeof (простите, но так исторически сложилось...):
Значение [[Class]] = Object.prototype.toString.call(myObj)
Тип
"foo" String stringnew String("foo") String object1.2 Number numbernew Number(1.2) Number objecttrue Boolean booleannew Boolean(true) Boolean objectnew Date() Date objectnew Error() Error object[1,2,3] Array objectnew Array(1, 2, 3) Array objectnew Function("") Function function/abc/g RegExp object (function в Nitro/V8)new RegExp("meow") RegExp object (function в Nitro/V8)
{} Object object
new Object() Object object
Почитать
● Pro JavaScript design patterns (Ross Harmes, Dustin Diaz);● JavaScript Garden: http://shamansir.github.com/JavaScript-Garden/;● MDN: https://developer.mozilla.org/en/JavaScript;● http://javascript.ru/.