Замечательный JavaScript ч.1
декабря 4, 2011 | Published in ClientSide, JavaScript | 5 Comments
Сразу оговорюсь о том, что JavaScript не самый удобный язык программирования — в нем много чего сделано неправильно или недоделано, много простейших вещей отсутствует, много неочевидных моментов, например начать программировать на Ruby можно уже через день почитав созданные кропотливым трудом статьи на RubyDev, а для того, чтобы научиться программировать на JavaScript необходимо большее время. Под «научиться программировать» я не подразумеваю умение организовывать архитектуру и создавать большие приложения, но подразумеваю знание синтаксиса и философии языка.
Несмотря на множество недостатков JavaScript — чудесный язык программирования. Если бы не проблемы с кроссбраузерностью и нормальная поддержка браузеров современных стандартов (я о HTML5 JavaScript APIs) то удовольствие от программирования на JavaScript можно было бы сравнить с удовольствием программирования на Ruby.
В этой огромнейшей статье я постараюсь вам поведать об объектах в JavaScript, наследовании на прототипах и прочих вещах, а также познакомить вас с философией языка, точнее с тем, как ее понимаю я. Статья не будет раскрывать тему программирования на JavaScript всецело, я не ставлю целью написание еще одного справочника по языку — это глупо, вместо этого данная статья научит вас понимать то как и почему JavaScript работает так, как работает, как организовать код и т.д. Также оговорюсь, что в статье приводится не кроссбраузерный код, он работает в FireFox и Chrome о том, работает ли он в остальных браузерах я не знаю.
1. Объектная ориентированность и прототипы
JavaScript не поддерживает традиционной парадигмы ООП так как в JavaScript отсутствует концепция классов. В JavaScript наследуются не классы, но объекты. Это не совсем наследование, а делегация к прототипу. Видите ли, каждый объект в JavaScript имеет собственный объект-прототип и сам может выступать в роли прототипа. Свойство — ссылку на прототип условно называют [[Prototype]]. [[Prototype]] — это скрытое свойство и доступа к нему на прямую вы не можете получить, однако в FireFox имеется специальное свойство __prototype__, которое позволяет получить прямой доступ к прототипу. Делегирование к прототипу работает по очень простому принципу: если для объекта вызывается неопределенный в нем метод или свойство, то объект передает этот вызов прототипу в надежде, что у прототипа это свойство или метод определено. Объект — прототип в свою очередь являясь совершенно обыкновенным прототипом также имеет свой собственный прототип и передает вызов ему если в самом себе не нашел вызываемого свойства или метода. Таким образом образуется цепочка прототипирования.
Роль классов в JavaScript берут на себя функции — конструкторы. Конструкторы — это совершенно обыкновенные функции, которые становятся конструкторами при условии, что с их вызовом используется ключевое слово new:
var myObj = new Object();
В примеры выше мы создали объект myObj используя конструктор Object(). Object — можно назвать базовым объектом (да, функции в JavaScript — это тоже объекты) потому, что Object.prototype всегда является последним в цепочке прототипов.
Давайте напишем собственный конструктор:
var Constr = function(){} var myObj = new Constr();
Constr — пустотелая функция, она ничего не делает кроме того, что просто существует, однако магическое ключевое слово new делает Constr удивительно полезной вещью. При создании нового объекта конструктор передает ему некоторые свойства, которые в теле конструктора определены с приемником this, а также присваивает создаваемому объекту ссылку на прототип. В то время, когда прямой доступ к прототипу из объекта не возможен, сам конструктор имеет свойство prototype, которое ссылается на него:
myObj.constructor === Constr //true myObj.constructor.prototype // объект типа Object
Свойство .constructor объекта myObj на самом деле не принадлежит самому объекту, но принадлежит его прототипу. Свойство .constructor занимается тем, что возвращает функцию — конструктор объекта то есть функцию Constr. Далее для возвращенного объекта Constr мы вызываем метод prototype, который возвращает объект-прототип порождаемых Constr объектов, то есть прототип объекта myObj. В FireFox и Chrome ссылку на прототип объекта предоставляет свойство __proto__, которое не описано в стандарте:
myObj.__proto__ === Constr.prototype //true Constr.prototype === myObj.constructor.prototype //true
Давайте напишем более сложный конструктор:
var ComplexConstr = function(name, lastName){ this.name = name; this.lastName = lastName; } var vladimir = new ComplexConstr("Vladimir", "Melnick"); vladimir.name //"Vladimir" vladimir.lastName //"Melnick"
Как видите, свойства определяемые в теле функции-конструктора для приемника this преобразуются в свойства порождаемого объекта, причем в собственные свойства, а не свойства прототипа:
vladimir.hasOwnProperty("name"); //true vladimir.hasOwnProperty("lastName"); //true
Это происходит благодаря тому, что в контексте конструктора this указывает на порождаемый объект, но о this мы поговорим позже.
Помимо собственных свойств конструктор предоставил объекту vladimir еще и ссылку на прототип.
ComplexConstr.prototype.fullName = function(){ return this.name + " " + this.lastName; } vladimir.fullName(); //"Vladimir Melnick" vladimir.hasOwnProperty("fullName"); //false
В примере выше мы определили метод для прототипа объекта vladimir и вызвали его для приемника — объекта vladimir. Обратите на то, как метод определен и то, как сработал. this в контексте метода fullName ссылается на объект — приемник, то есть на vladimir. Привыкните к тому, что в JavaScript this не всегда указывает на объект в котором используется, но всегда на объект для которого выполняется вызов функции.
2. Функциональная магия языка JavaScript
JavaScript — не только объектно-ориентированный язык программирования (ООЯП), но и функциональный. В JavaScript функциональность проявляется в наличии функций высшего порядка. Это такие функции, которые могут оперировать другими функциями, например, принимать функции в качестве аргументов и возвращать по выполнению значение — функцию или несколько функций, причем все функции в JavaScript являются одновременно и функциями первого порядка (работают с не функциональными типами) и высшего порядка (работают с функциональными типами). Пример:
var alertMsg = function(msg){ alert(msg); } var superFunc = function(msg, alertFunc) { alertFunc(msg); } superFunc("Welcome to RubyDev.ru!", alertMsg);
В примере выше функция superFunc принимает в качестве аргументов строку msg и другую функцию — alertMsg, и внутри себя вызывает эту alertMsg передавая ей в качестве аргумента строку msg. При желании мы можем легко возвратить в качестве значения по выполнении функции другую функцию. Перепишем немного пример, что приведен выше:
var alertMsg = function(msg){ alert(msg) } var superFunc = function(msg) { return function(alertFunc) { alertFunc(msg); } } var sf = superFunc("Welcome to RubyDev.ru!"); sf(alertMsg);
Выше приведен практически классический пример каринга или каррирования. Карринг — это представление функции в таком виде, когда она принимает не весь набор аргументов сразу, а по одному.
В примере выше функция superFunc принимает в качестве аргумента msg — строку в которой содержится некое сообщение и возвращает анонимную функцию, которая в качестве аргумента принимает функцию и вызывает эту функцию передавая в нее переменную msg, которая была получена родительской функцией в качестве аргумента. Такое поведение функция возможно благодаря тому, что называется замыканием. Замыкание — это сохранение внутреннего состояния родительской функции (функции в которой объявлены другие функции) после ее выполнения для того, чтобы с ее переменными могли работать другие — определенные внутри нее функции. Это может показаться сложным, по этому я приведу совсем традиционный и простой пример замыканий:
function plus(a) { return function(b){ alert(a + b); } } var plusFive = plus(5); plusFive(10); //15 var plusSix = plus(6); plusSix(4); //10
Большим недостатком в JavaScript является отсутствие возможности определять значение атрибутов функции по умолчанию. Если в Ruby значения по умолчанию можно определить сразу при перечислении аргументов метода, то в JavaScript для этого необходимо писать специальные проверки.
function plus(a, b) { console.log(a + b); }
Если мы вызовем функцию представленную выше и не передадим в нее все аргументы, то функция напечатает NaN. NaN — это особый объект, который представляет собой некоторое значение, которое не является числом (Not a Number). NaN будет возвращено как результат попытки сложения двух объектов undefined:
undefined + undefined // NaN
undefined — это особый объект, который возвращается при попытке обращения к неопределенной переменной. В контексте функции аргументы переданные в функцию превращаются в локальные переменные, а поскольку аргументы в функцию не переданы, то и соответствующие им локальные переменные не определены (объявлены, но им не присвоено значение, а undefined является значением по умолчанию для объявляемых переменных).
Давайте перепишем функцию так, чтобы ее аргументы имели значения по умолчанию:
function plus(a, b) { a = a || 5; b = b || 10; console.log(a + b); } plus(1); //11 plus(2,5); //7 plus(); //15
Значения undefined, null и NaN (а также число 0 и пустая строка) оцениваются как false:
undefined ? true : false //false null ? true : false //false NaN ? true : false //false
По этому оператор || (или) из двух значений при условии, что a (и b) оцениваются как false (значение для них не определено = undefined), выберет то,что оценивается как true.
Функции в JavaScript имеют возможность работать с неопределенным (произвольным) количеством аргументов. Эта возможность реализована не в самый удачный способ, однако то, что есть вполне устраивает потребности разработчиков. В функциях существует псевдомассив arguments, который содержит в себе все аргументы, что передаются в функцию. Псевдомассивом arguments называется потому, что он хоть и является упорядоченным набором значений с целочисленными ключами, но не является объектом порожденным конструктором Array.
var superSum = function(){ var sum = 0; var argLength = arguments.length; var i = 0; for(i; i < argLength; i++) { sum += arguments[i]; } alert(sum); } superSum(1,2,3,4,5,100,500,666); //1281
Несмотря на то, что у функций имеется такой объект как arguments я все же рекомендую в случаях, когда количество аргументов заранее не может быть известно передавать все аргументы в виде массива:
var superSum = function(args){ var sum = 0; var argLength = args.length; var i = 0; for(i; i < argLength; i++) { sum += args[i]; } alert(sum); } superSum([1,2,3,4,5,100,500,666]); //1281
Эта техника хороша тогда, когда у нас помимо однотипных аргументов имеются аргументы особого назначения:
var superSum = function(div, args){ var sum = 0; var argLength = args.length; var i = 0; for(i; i < argLength; i++) { sum += args[i]; } alert(sum/div); } superSum(10, [1,2,3,4,5,100,500,666]); //128.1
В случае, если мы будем использовать объект arguments результат будет не такой, какой мы ожидали потому, что аргумент div будет входить в состав псевдомассива arguments. Можно конечно обойти стороной N-первых аргументов, но мне выдается этот вариант не таким красивым, как передача аргументов в виде массива или хэша.
var superSum = function(div){ var sum = 0; var argLength = arguments.length; var i = 1; //пропускаем 0 элемент в котором содержится аргумент div for(i; i < argLength; i++) { sum += arguments[i]; } alert(sum/div); } superSum(10,1,2,3,4,5,100,500,666); //129.1
Функции в JavaScript позволяют себе выполнять вызов в своем контексте самих себя, то есть выполнять рекурсию. Без рекурсий можно обойтись и они часто вредны так как могут значительно увеличить объем потребляемой памяти, однако необходимо быть знакомым с ними. Классический пример рекурсивной функции нахождения факториала числа:
var factorial = function(number){ var result = 0; (number == 1 || number == 0) ? result = 1 : result = number * factorial(number-1); return result; } factorial(5); //120
3. Продвинутое ООП в JavaScript и Паттерны проектирования
Когда конструктор порождает объект, конструктор наделяет объект собственными свойствами, которые в теле конструктора определены для приемника this, а также присваивает объекту ссылку на прототип, который связывает объект с конструктором свойством constructor. По сути ссылка на конструктор через Obj.[[Prototype]].constructor необходима только для определения типа объекта ведь объект не пользуется кодом из конструктора, а использует только свой код и делегирует цепочке прототипов.
В «сыром» виде конструкторы используются редко, чаще всего они используются в различных паттернах, например в очень популярном паттерне реализации правильного «истинного» прототипного наследования в JavaScript, который был популяризован известным адептом и евангелистом JavaScript — , а изобретен Лассом Рейчштейном Нильсеном (Lasse Reichstein Nielsen).
function object(o) { function F() {} F.prototype = o; return new F(); }
Смысл очень прост — мы создаем промежуточный функцию-конструктор F, присваиваем ей прототип — объект от которого мы хотим наследоваться, а дальше возвращаем порождаемый промежуточным конструктором объект.
function A(){ this.protoConstructor = A; this.alertMsg = function(msg){ alert(msg); } } var b = object(new A) console.log(b.protoConstructor); //A console.log(b.constructor); //A b.alertMsg("Welcome to RubyDev.ru!");
Этот паттерн очень удобен так как нам не нужно создавать специальных функций конструкторов, мы просто используем функцию object() для прототипного наследования, сам Крокфорд комментирует этот паттерн так: «Объект наследуется от другого объекта — что может быть еще более объектно-ориентированным чем это?».
— является, наверное самым знаменитым, цитируемым и уважаемым адептом JavaScript. Сейчас Крокфорд работает главным архитектором JavaScript приложений в компании Yahoo!, активно участвует в разработке YUI (Yahoo! User Interface framework), участвует в конференциях и активно пишет в блог YUI.
Крокфорд также предоставляет несколько других паттернов основанных на приведенном выше. Например например называемыем им maker — функции, которые внутри себя вызывают функцию object() внутри себя и производят некоторую модификацию объекта, например снабжают его приватными функциями. Если такую make-функцию вызвать в другой make-функции, то будет реализован паттерн паразитического наследования.
У приведенного выше паттерна имеется модификация:
Object.prototype.begetObject = function () { function F() {} F.prototype = this; return new F(); }; newObject = oldObject.begetObject();
Как видите, данная модификация позволяет наследоваться от объекта oldObject используя его метод begetObject() вместо использования функции object(). Это модификация является объектно-ориентированным клоном оригинального паттерна и имеет один недостаток — мы занимаемся тем, что переписываем базовый прототип, который может быть переписан и из других мест из-за чего имеется вероятность того, что наш метод begetObject() будет переписан. Это маловероятно, но все же не рекомендуется модернизировать Object.prototype. Для решения этой проблемы и решения проблемы использования глобальной функции object() Крокфорд предлагает использовать следующий паттерн:
if (typeof Object.create !== 'function') { Object.create = function (o) { function F() {} F.prototype = o; return new F(); }; } newObject = Object.create(oldObject);
Примеры кода взяты из .
JavaScript является сверх динамичным и мультипарадигмальным языком программирования, что позволяет реализовывать абсолютно различные паттерны. Очень полезным и интересным паттерном является паттерн method-chaining, который заключается в возможности построения более лаконичного кода используя цепочки вызова методов. Пример:
function Client(server){ this.server = server; } Client.prototype.connect = function() { alert("Connecting to " + this.server + "..."); return this; } Client.prototype.doSomething = function(something) { alert("We " + something + " on server."); return this; } Client.prototype.disconnect = function() { alert("Disconnect compleated."); } var twitterClient = new Client("Twitter.com"); twitterClient.connect().doSomething("get list of followers").disconnect();
В следующей части мы поговорим еще о чем-то очень интересном.
Лучшая благодарность автору — ваши комментарии.
декабря 5, 2011 at 07:43 (#)
Спасибо за статью. Хоть немного понятнее стало. Но главный вопрос остался. Зачем? В чем фишка этого прототипного подхода. Чем им обычные классы не нравились?
декабря 5, 2011 at 17:33 (#)
tankard, это просто как альтернативное видение решение проблемы. Концепция классов показалась авторам такого подхода лишней, ведь можно реализовать наследование, когда один объект наследуется от другого. И авторы такого подхода посчитали, что фокус на классификации не так важен, как фокус на функциональности объектов. В любом случае, если сравнивать, например Ruby с JavaScript то различий не так уж и много.
марта 9, 2012 at 15:09 (#)
Никак не могу понять вот этого:
Что здесь происходит?
марта 10, 2012 at 22:31 (#)
Алгоритм:
1. Принимаем объект o
2. Делаем объект прототипом объектов порождаемых конструктором F
3. Используем конструктор F и создаем объект с прототипом o
Смысл в том, что мы реализуем наследование некоторого объекта от объекта передаваемого в функцию object.
июня 25, 2013 at 09:08 (#)
А я не могу понять чем последний пример отличается от, например, такой реализации:
Понятно вроде, что в первом случае методы connect, doSomething, disconnect являются методами прототипа, но непонятно, для чего это сделано.