2. С чем имеем дело?
● ОО подход основан на прототипах;
● Отсутствие пространств имен;
● Отсутствие ограничения области видимости;
● Отсутствие строгой типизации;
● Отсутствие проверки статических элементов.
3. Прототипное программирование
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 Object
var 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 Human
var olga = new Woman(); // olga.__proto__ = instance of Human
4. Интерфейсы. Классика
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) { ... }
}
5. Эмуляция интерфейсов в 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) { ... };
6. Эмуляция интерфейсов в 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) {
...
}
7. Эмуляция интерфейсов в 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));
...
}
8. Конструктор 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]);
}
};
9. Статический метод Interface.ensureImplements
Interface.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.");
}
}
}
};
10. Инкапсуляция на соглашениях
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; }
}
}
11. Замыкания
Замыкание — это особый вид функции. Она определена в теле другой функции и создаётся
каждый раз во время её выполнения. При этом вложенная внутренняя функция содержит ссылки
на локальные переменные внешней функции. Каждый раз при выполнении внешней функции
происходит создание нового экземпляра внутренней функции, с новыми ссылками на переменные
внешней функции.
function foo() {
var a = 10;
function bar() {
a *= 2;
return a;
}
return bar;
}
var baz = foo(); // baz is now a reference to function bar
baz(); // returns 20
baz(); // returns 40
baz(); // returns 80
var blat = foo(); // blat is another reference to bar
blat(); // returns 20
12. Инкапсуляция на замыканиях
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());
}
};
13. Статические атрибуты и методы
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 method
Human.toUpperCase = function(s){ ... }
Human.prototype = { // Public, non-privileged methods
talk: function() { ... }
};
14. Константы - приватные статические атрибуты с 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) {
...
}
})();
15. Приватный объект с константами и 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) {
...
}
})();
16. Эмуляция классического наследования
/* 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 chain
Author.prototype.constructor = Author; // Set the constructor attribute to Author
Author.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']);
17. Эмуляция классического наследования. Функция extend
function 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']);
18. Инкапсуляция при эмуляции классического наследования
● В подклассе имеется доступ только к публичным и привилегированным членам
родительского класса;
● Приватные члены родительского класса доступны в подклассах, а также извне
только через привилегированные методы самого родительского класса;
● Если есть необходимость в protected членах, принято соглашение использовать
подчеркивание, например var _myProtectedVar = null;
19. Прототипное наследование
/* 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'];
20. 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();
21. Singleton
/* Basic Singleton. */ /* Singleton with Private Members */
var Singleton = { MyNamespace.Singleton = (function() {
attribute1: true,
attribute2: 10, // Private members
method1: function() { var privateAttribute1 = false;
... var privateAttribute2 = [1, 2, 3];
}, function privateMethod1() {
method2: function(arg) { ...
... }
} function privateMethod2(args) {
}; ...
}
return {
// Public members
publicAttribute1: true,
publicAttribute2: 10,
publicMethod1: function() {
...
},
publicMethod2: function(args) {
...
}
};
})();
22. "Ленивое" инстанцирование
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
23. Простая Фабрика
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;
}
};
24. Фабричный метод - паттерн, порождающий классы
/* 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).
25. Компоновщик. Пример использования - запоминание форм
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);
26. Декораторы функций
Выглядит это так:
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.
27. Тренируем внимание
● Расширение встроенных прототипов языка - зло;
● Не забываем писать 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);
}
29. Внимание, оператор typeof
Результат работы typeof (простите, но так исторически сложилось...):
Значение [[Class]] = Object.prototype.toString. Тип
call(myObj)
"foo" String string
new String("foo") String object
1.2 Number number
new Number(1.2) Number object
true Boolean boolean
new Boolean(true) Boolean object
new Date() Date object
new Error() Error object
[1,2,3] Array object
new Array(1, 2, 3) Array object
new Function("") Function function
/abc/g RegExp object (function в Nitro/V8)
new RegExp("meow") RegExp object (function в Nitro/V8)
{} Object object
new Object() Object object