Создание объектов в Javascript

Гибкость Javascript позволяет создавать объекты множеством способов. Но как это нередко случается, разнообразие таит в себе множество подводных камней. Из этой статьи Вы узнаете о том, как разглядеть и обогнуть эти опасные рифы.

Основы основ


Нелишним будет напомнить, что из себя представляют объекты в Javascript и как их можно создавать. Объект в Javascript — это всего лишь хэш-таблица ключей и значений. Если значения представляют собой базовые типы или другие объекты, их называют свойствами, если же это функции, их называют методами объекта.

Объекты, созданные пользователем, можно изменить в любой точке выполнения скрипта. Многие свойства встроенных в язык объектов также изменяемы. То есть можно просто создать пустой объект и добавлять к нему свойства и методы по мере необходимости. Проще всего это сделать с помощью литеральной нотации:
//создаем пустой объект
var cat = {};
//добавляем свойство:
cat.name = "Garfield";
//или метод:
cat.getName = function() {
  return cat.name;
};

Другим способом создания объекта является использование функций-конструкторов:
// литеральная нотация:
var cat = {name: "Garfield"};
// конструктор-функция:
var cat = new Object();
cat.name = "Garfield";

Очевидно, что литеральная нотация короче конструктора. Есть и философская причина предпочитать литеральную нотацию конструкторам: она подчеркивает, что объект — это всего лишь изменяемый хэш, а не нечто, создаваемое по шаблону, заданному классом.

Кроме того, использование конструктора Object вынуждает интерпретатор проверять, не переопределена ли эта функция в локальном контексте.

Подводный камень конструктора Object


Причин использовать конструктор Object нет. Но все мы знаем, что иногда приходится использовать какой-то старый код, и в этом случае полезно знать об одной особенности этого конструктора. Он принимает аргумент, и в зависимости от его типа может поручить создание объекта другому встроенному в язык конструктору; в итоге мы получим не тот объект, что ожидали:
//пустой объект
var o = new Object();
o.constructor === Object; // true

var o = new Object(1);
o.constructor === Number; //true 

var o = new Object("string");
o.constructor === String; //true
//мы не предполагали, что созданный объект будет иметь этот метод:
typeof o.substring; // "function"

Такое поведение конструктора Object может привести к неожиданным результатам, если мы передаем в него значение, неизвестное на этапе выполнения.

Мораль очевидна: не используйте конструктор Object.

Собственные конструкторы


Мы можем определять собственные конструкторы. Использование их выглядит так:
var cat = new Cat("Garfield");
cat.say(); // "I am Garfield"

Синтаксис похож на конструктор Java, но в Javascript конструктор является обычной функцией и поэтому определяется так:
var Cat = function(name) {
  this.name = name;
  this.say = function() {
    return "I am" + this.name;
  }
}

При вызове этого конструктора с оператором new внутри функции происходит следующее:
  • создается пустой объект, на который указывает переменная this; этот объект наследует прототип функции;
  • к объекту, хранимому в this, добавляются свойства и методы;
  • объект, хранимый в this, неявно возвращается в конце функции (т.к. мы ничего не возвращали явно).

Простоты ради в примере метод say() добавлен к объекту this. Это не очень хорошо, так как каждый раз при вызове new Person() в памяти будет создаваться новая функция. Так как метод say() одинаков для всех объектов, создаваемых с помощью этого конструктора, лучше добавить его к прототипу Cat:
Cat.prototype.say = function() {
  return "I am " + this.name;
};

Кроме того, не совсем корректно утверждать, что объект this, неявно создаваемый в конструкторе, пуст: он наследуется от прототипа Cat, однако рассмотрение прототипов выходит за рамки этой статьи.

Что возвращает конструктор


При использовании оператора new, конструктор всегда возвращает объект. По умолчанию, это объект, на который ссылается this. Конструктор возвращает this неявно, однако мы можем явно вернуть любой другой объект, например:
var Cat = function() {
  this.name = "I am Garfield";
  var that = {};
  that.name = "I am Cat-woman";
  return that;
};
var cat = new Cat();
cat.name // "I am Cat-woman"

Таким образом, мы можем вернуть из конструктора любое значение, но лишь при условии, что это объект. Если мы попытаемся вернуть, скажем, строку или false, это не приведет к ошибке, но оператор возврата будет проигнорирован, и конструктор вернет this.

Коварный new


Конструкторы — это всего лишь функции, вызываемые с оператором new. Что случится, если забыть этот оператор? Интерпретатор не выдаст предупреждений, но это приведет к логическим ошибкам. Переменная this будет указывать не на объект, унаследованный от прототипа конструктора, а на глобальный объект (window в случае браузера):
function Cat() {
  this.name = "Garfield";
}
// новый объект
var cat = new Cat();
typeof cat; // "object"
cat.name; // "Garfield"

//забываем new:
var cat = Cat();
typeof cat; // "undefined"
window.name; //  "Garfield"

В строгом режиме стандарта ECMAScript 5 this в этом случае не будет указывать на глобальный объект. Посмотрим, как можно избежать этой ошибки, если ECMAScript 5 недоступен.

Соглашения об именовании функций

Самым простым способом является неукоснительное соблюдение соглашений об именовании функций: начинаем обычные функции со строчной буквы (myFunction()), а функции-конструкторы — с заглавной (MyConstruction()). К сожалению, такой способ почти ни от чего не спасает.

Явный возврат объекта

Конструкторы могут возвращать любые объекты. Программисты могут воспользоваться этим:
function Cat() {
  var that = {};
  that.name = "Garfield";
  return that;
}

Имя переменной that выбрано произвольно, это не часть спецификации. С тем же успехом мы можем назвать возвращаемый объект me или self или как Вам заблагорассудится.

Для простых объектов, вроде создаваемого в примере, мы можем вообще обойтись без дополнительных переменных, используя литеральную нотацию:
function Cat() {
  return {
    name: "Garfield"
  };
}

Такой конструктор будет всегда возвращать объект, независимо от того, как его вызывать:
var first = new Cat(),
    second = Cat();
first.name; // "Garfield"
second.name; // "Garfield"

У этого способа есть серьезный недостаток: объект не наследует прототип конструктора, то есть методы и свойства, добавленные непосредственно к Cat, будут недоступны создаваемым с его помощью объектам.

Самовызывающий конструктор

Для решения этой проблемы достаточно проверить, является ли this в теле конструктора экземляром этого самого конструктора, и если нет, вызывать себя снова, но на этот раз с оператором new. Звучит страшно, но на деле просто:
function Cat() {
  if (!(this instanceof Cat)) {
    return new Cat();
  }
  this.name = "Garfield";
}
Cat.prototype.meow = "Meow!";

var first = new Cat(),
    second = Cat();

first.name; // "Garfield"
second.name; // "Garfield"

first.meow; // "Meow!"
second.meow; // "Meow!"

Если наш конструктор будет в дальнейшем переименован, придется править его тело. Избежать этого можно проверкой arguments.callee вместо имени конструктора:
if (!(this instanceof arguments.callee)) {
  return new arguments.callee();
}

Здесь мы воспользовались тем, что внутри каждой функции создается объект arguments, содержащий все параметры, передаваемые функции в момент вызова. Свойство callee этого объекта указывает на вызываемую функцию. Но и здесь нужно проявить осторожность: строгий режим ECMAScript 5 вызывает исключение TypeError при обращении к этому свойству, поэтому стоит заранее сделать выбор между удобством рефакторинга и светлым завтра.

Вместо заключения


Javascript — потрясающий язык. Его достаточно легко освоить, а существующие фреймворки позволят без труда использовать его в своих проектах. Но за простотой синтаксиса скрываются целые рифы подводных камней — и очень мощных инструментов. Иногда полезно смотреть, что же там на дне.
Share post
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 68

    +2
    если нужен класс с инициализацией то можно сделать так:

    var popup= function () { this.initialize.apply(this, arguments); };
    popup.prototype = {
    initialize: function (h) { /** вызывается при инициализации**/ } ,
    show: function (text) { ... } ,
    close: function () { ... }
    }
    }

    теперь можно делать так
    var p=new popup({caption:'Alert'}); //вызывается инициализация класса в которую передаем параметры
    p.show('Hello');
    p.close();


    давно еще подсмотрел это в prototype, теперь постоянно пользуюсь.
      0
      это как альтернатива если не делать через
      var pop=function(par){


      }

      имхо так читабельней когда инициализация вынесена в отдельную ф-цию
        0
        Может и читабельней, но приходится явно ее вызывать. Это не очень красиво.
          0
          Вы что-то не поняли.
          В плане пользования все выглядит одинаково
          var p=new popup(params);

          отличие только в коде класса
            0
            Вот я и говорю — в «коде класса», как вы это называете, ее приходится явно вызывать.
              0
              это всего лишь один раз в коде
              > в «коде класса», как вы это называете
              ну хорошо… имитация класса :)
                0
                Ну это какбэ конструктор.
                0
                Обычно тогда используется либа. что-то типа такого:

                var Class = function (Parent, proto) {
                	if (proto == null) {
                		proto = Parent;
                		Parent = null;
                	}
                	var Constructor = function () {
                		this.initialize.call( this, arguments );
                	};
                	
                	F.prototype = Parent.prototype
                	
                	Constructor.prototype = new F();
                	Constructor.prototype.constructor = Child;
                	Constructor.parent     = Parent;
                };
                
                var Foo = Class({
                	initialize: function () {
                		// constructing here
                	},
                	fooMethod: function () {
                		// fooMethod here
                	}
                });
                
                var Bar = Class(Foo, {
                	initialize: function () {
                		Bar.parent.initialize.apply( this, arguments );
                		
                		// construction here
                	},
                	
                	barMethod: function () {
                		var f = this.fooMethod();
                		// barMethod here
                	}
                });
                
                  0
                  Ну, имеет право на жизнь в таком случае, да:)
                    –3
                    Я вообще слабо понимаю, как сейчас можно писать на JS без фреймворков)
                      +6
                      Классы и наследование — это частные случаи code reuse. JS предлагает «из коробки» кучу других способов повторно использовать код — миксины, агрегация… Тысячи их, ну вы-то знаете:) Все обладают своими минусами и плюсами, все нуждаются в той или иной степени в «сахаре». Можно выбирать, в общем. Дело вкуса и частных случаев.

                      Ни в коем случае не умаляя достоинства фреймворков, я все же предпочитаю делать столько, сколько могу, непосредственно встроенными в язык средствами.
                    0
                    Угу, так построен Class.create в prototype.
                      0
                      И приблизительно так же в MooTools)
                0
                А если Вы имеете в виду лишнюю прослойку function () { this.initialize.apply(this, arguments); };
                то это такие пустяки в плане производительности, что не вижу смысла на этом экономить.
                  0
                  Просто эта строчка будет повторяться в неизменном виде во всех ваших конструкторах. Вас это не коробит?
                    0
                    в моем микро-фреймвоке это вызывается =$class();
                    var pop=$class();
                    так что не коробит
                      0
                      Могу я предложить вашему вниманию книгу?
                        0
                        спасибо, она у меня вторая на очереди после JavaScript Patterns :)
                          0
                          Good Parts более базовая. Хотя Patterns ее во многом повторяет, но не во всем.
                        • UFO just landed and posted this here
                            –3
                            Побойтесь бога, орайлевские переводные книги в бумажном виде у нас стоят около тысячи рублей. А в электронном виде вообще не купить.
                            Вам жалко 600 рублей за хорошую книгу, которая поможет вам профессионально вырасти?
                            я бы вам прислал этот пдф, благо у этого «жадного издателя» они без DRM, если бы ваша тяга к знаниям превосходила вашу тягу к халяве.
                              0
                              А что, я неправду сказал?
                                0
                                Не минусовал, но подозреваю, что минусуют за то, что учите людей жить. При этом — грубо.
                                  0
                                  Я считаю грубостью, когда меня пытаются использовать вместо гугла. Да еще «нахаляву».
                                    0
                                    Грубостью отвечать на грубость — это не оправдание ;)
                                      0
                                      Вы, конечно, правы. «Насилие — последнее прибежище некомпетентного», и все такое. Но, знаете, когда я очень устаю, то у меня не хватает сил оставаться рафинированным интеллигентом:) И на хамство хочется ответить хамством.
                                        0
                                        В такие моменты идеальный выход — это промолчать)
                                        Нагрубить в ответ, в принципе, тоже выход)
                                    • UFO just landed and posted this here
                                        0
                                        > 2100 рублей
                                        Вот видите, не так уж они жадные:) Да, это перебор.

                                        Я тоже не люблю покупать книги, особенно бумажные.

                                        Выслал вам в личку ссылку на PDF.
                                        • UFO just landed and posted this here
                                            0
                                            Да, очень бы хотелось.
                                –2
                                Напишите автору — я бедный студент из снежной России, последние деньги потратил на водку для медведя, а очень хочется стать крутым джаваскриптером, как вы, пришлите вашу книжку, пожалуйста.
                                Ну, про водку, пожалуй, лишнее, но вообще может и прокатить.
                                  0
                                  Ага, крутым джаваскриптером — чтобы зарабатывать много, — но не тратить.
                  +3
                  Отличный пост, спасибо! Всё грамотно изложено, способствует систематизации ранее накопленных знаний.
                    +2
                    Это вольный перевод главы из «JavaScript Patterns»? Хорошо изложено.
                      +1
                      Да, эта книга перевернула мои взгляды на Javascript. Все, что я знаю, почерпнуто оттуда.
                        +1
                        Я ее купил в пятницу. Пока ничего радикально нового не узнал, но денег не жалко.
                          +1
                          Мне она просто попалась в руки в нужный момент. Всегда сложно совершить прыжок от новичка к продвинутому пользователю. К тому моменту я знал синтаксис JS (невеликий подвиг), но многое было непонятно — что это за зверь prototype, что делать без наследования, статических функций и т.д. Так что эта книжка — как первая любовь, на всю жизнь.
                            0
                            Я бы посоветовал еще JavaScript: The Good Parts Крокфорда, но, боюсь, после «Patterns» там ничего нового не найдете.
                      –16
                      Потрясающий язык с множеством подводных камней. На первый взгляд простой синтаксис скрывает в себе кучу говна.
                        +1
                        arguments.callee? o_O может всетаки не стоит уже этим пользоваться? имхо
                          +1
                          Хорошая статья для начинающих.
                          А вот с этим
                          >Его достаточно легко освоить
                          Я бы поспорил. :) Прототипы, миксины, передача аргументов by sharing… Он прост на первый взгляд. Я не первый год пишу на нем и до сих пор иногда случаются «озарения» :)
                          PS Недавняя новость про запуск линукса в js эмуляторе просто взорвала мозг. Что-то до сих пор на место не встало.
                          А вы говорите, легко освоить…
                          PPS Я бы перефразировал — с ним легко начинать. И если распробуешь его — несложно расти.
                            +1
                            Полностью согласен. Сложность (и сила) JS в том, что у него своя, особенная парадигма. Главная задача при овладении JS — перестроить мозг с класс-ориентированного программирования на прототип-ориентированное и функциональное.

                            P.S. Под «достаточно легко освоить» подразумевал «прикрутить JQuery и радоваться новым свистелкам и перделкам на своем сайте»
                            +2
                            >>с оператором new. Что случится, если забыть этот оператор? Интерпретатор не выдаст предупреждений, но это приведет к логическим ошибкам

                            Меня одного коробят подобные фразы? Интерпретатор начнет ругаться при первой же попытке вызвать метод прототипа.
                            А что случится если скобки забыть? Тоже ведь ошибки не будет. Почему тут не предусмотрена защита?

                            Неужели я чего то не понимаю и городить кучу лишнего кода для подобной “защиты от дурака” считается нормальным?

                            И чем меньше кода — тем читабильнее и легче воспринимается. И с классами та же история. Много лишнего кода, исключительно для сахара. Чем не устраивает прототипное наследование? Абсолютно все те же возможности.
                              0
                              Согласен. Всегда считал, что «Простое лучше, чем сложное» (с)
                                +1
                                И чем меньше кода — тем читабильнее и легче воспринимается. И с классами та же история. Много лишнего кода, исключительно для сахара. Чем не устраивает прототипное наследование? Абсолютно все те же возможности.

                                Либы просто предоставляют удобную обёртку для уменьшения количества конечного кода, не более того. Это то же самое прототипное наследование. Не понимаю, чего некоторые люди стараются убедить других, что «используешь обёртку — не прототипное наследование, а хрень». Смысл много раз писать длинное слово «prototype»? Смысл много раз дублировать код, если его можно врапнуть в библиотеку, тем более, это полезно скажется на окончательном коде. Зачем вы усложняете?
                                F.prototype = Parent.prototype
                                Child.prototype = new F();
                                Child.prototype.constructor = Child;
                                Child.parent = Parent;
                                
                                  0
                                  Конечно от случая зависит много и от того что внутри Parent, но зачастую достаточно
                                  Child.prototype = new Parent();

                                  Хотя конечно если нужна именно куча “классов”. И обычное наследование не подходит — то конечно проще враппер.
                                  К тому же автокомплит никто не отменял если слово prototype уж слишком длинное.
                                    0
                                    Обычное наследование — это какое? Я, вроде, показал обычное.
                                    prototype длинное не только для письма, но и для чтения) да ещё и дублирующееся имя класса. Да и вес окончательного файла.
                                    я предпочитаю не задумываться над деталями и большим количеством «если» и следовать принципу KISS. =)
                                      0
                                      Обычное это которое через экземпляр объекта родителя.
                                        0
                                        Через экземпляр объекта родителя вызывается конструктор, а там может быть код, который не должен вызываться «зря» или должен вызываться только с параметрами.
                                          0
                                          Так поэтому и написал выше что зависит от того что внутри Parent
                                  0
                                  Меня одного коробят подобные фразы? Интерпретатор начнет ругаться при первой же попытке вызвать метод прототипа.
                                  А что случится если скобки забыть? Тоже ведь ошибки не будет. Почему тут не предусмотрена защита?


                                  Это может быть удобно в фреймворке. Например, как мог бы выглядеть jQuery:

                                  var $ = function (selector, context) {
                                  	if (! (this instanceof $)) {
                                  		return new $(selector, context);
                                  	}
                                  	this.find( selector, context );
                                  };
                                  
                                  $.fn = $.prototype = {
                                  	// prototype here
                                  };
                                  


                                  В итоге получится такой вызов:

                                  var $divs = $('div');
                                  // эквивалентно:
                                  var $divs = new $('div');
                                  


                                  То есть, доллар-функция как-бы получилась фабрикой для самой-себя.
                                    0
                                    ну и зачем в итого два эквивалента одного и того же? Исключительно ради красоты кода?
                                    ИМХО В том же jQuery — постороенном на краткости кода и благоприятствующем созданию цепочек — имеет смысл только фабрика.

                                    И писать лишний код — пусть его тут и всего пара строк, ради того чтобы можно было создавать объекты ещё и через new — лишнее.
                                      0
                                      в jQuery фабрика именно и создаёт объекты через new. И, кстати, jQuery можно вызывать через new, будет работать ;) Там именно такой подход и используется, только содержимое конструктора вынесено в $.prototype.init
                                        0
                                        Работать то будет. Я к тому что смысла в этом по сути нет. И если бы такой возможности не было — вряд ли бы кто то стал кидаться в него помидорами.
                                          0
                                          Дело в том, что «new» — необходимо, чтобы был доступ к прототипу, а «без new» — сахар, чтобы был круче результат.
                                            0
                                            new там разве не внутри $? И от внешнего new никак не зависит, если внутри все равно отдельная фабрика со своим new?

                                            Я в кишках jQuery не ковырялся, поэтому тут спорить не буду
                                              0
                                              Я недавно описывал.
                                              $ на самом деле создаёт инстанс $.prototype.init, который на самом деле есть инстансом $. Та всё немного запутанно.
                                              Если вы не против, я вам покажу на примере AtomJS:
                                              var dom = function (sel, context) {
                                              	if (! (this instanceof dom)) {
                                              		return new dom(sel, context);
                                              	}
                                              	// initialize
                                              }
                                              


                                              dom на самом деле класс, который надо вызывать через new dom('tag.class'), но ради упрощения api я добавил возможность не писать «new»: dom('tag.class'). С таким же успехом я мог вынести содержимое dom в какую-то функцию-конструктор, но зачем? ;)
                                                0
                                                Ну например, чтобы не плодить вариантов использования. Но в целом понятно
                                      0
                                      кстати, а не проще ли сделать так:

                                      var myFunc = function()
                                      {
                                      //используем this
                                      }
                                      myFunc = new myFunc();


                                      получается синлтон) — он всегда будет доступен только как объект, добавить нужно одну строчку сразу за определением, и никаких проверок при каждом вызове.
                                        0
                                        Синглтон — плохой паттерн, а в JS — вообще не нужен. То, что вы описали можно очень легко сократить:
                                        var myFunc = new function()
                                        {
                                        //используем this
                                        }
                                        


                                        Тем более для jQuery одиночка не подходит совершенно. Там обязан каждый раз инициироваться новый объект.
                                          0
                                          странная аргументация — «плохой паттерн».
                                          если я знаю, что объект у меня должен быть гарантированно один на странице, то очень даже неплохой.

                                          за шорткат спасибо, не додумался)
                                    0
                                    В строгом режиме стандарта ECMAScript 5 this в этом случае не будет указывать на глобальный объект. Посмотрим, как можно избежать этой ошибки, если ECMAScript 5 недоступен.


                                    Простите, но мне кажется, что вы — неправы. Строгий режим распространяется только на функции, вызванные при помощи call или apply:

                                    (function () {
                                    	var test = function () {
                                    		console.log(this);
                                    	};
                                    
                                    	test(); // window
                                    	test.call(null); // window
                                    })();
                                    
                                    
                                    (function () {
                                    	'use strict';
                                    
                                    	var test = function () {
                                    		console.log(this);
                                    	};
                                    
                                    	test(); // window
                                    	test.call(null); // null <=========
                                    })();
                                    
                                      0
                                      Вот что я прочитал в спецификации ECMA 5:
                                      If this is evaluated within strict mode code, then the this value is not coerced to an object. A this value of null or undefined is not converted to the global object and primitive values are not converted to wrapper objects. The this value passed via a function call (including calls made using Function.prototype.apply and Function.prototype.call) do not coerce the passed this value to an object.

                                      Я понимаю это как «Значение this в строгом режиме не приводится к объекту. Если this имеет значение null или undefined, this не будет приведено к глобальному объекту. Если значение this базового типа, оно не будет преобразовано в соответствующий объект. Значение this, переданное через вызов функции (в том числе, вызовы через Function.prototype.apply и Function.prototype.call), не будет преобразовано в объект.» В стандарте нет ограничений по способу вызова.

                                      Однако в Chromium 11.0.696.65 все так, как Вы и описываете. В Firefox 4.0.1 в строгом режиме прямой вызов печатает в консоль undefined, вызов через Function.prototype.callnull. Я опирался на стандарт, но любой стандарт разбивается о практику :(
                                      +1
                                      А ещё говорят что C++ не очевиден и содержит много подводных камней. Javascript, PHP и т.п. вот где настоящий ад! Компиляции нет, на этом этапе ошибок не словить. Типизация неявная. Проверяем только тестами. Интерпретатор в любой момент может выкинуть ошибку в рантайме. >_<
                                        +1
                                        Типизация — утиная, JavaScript не содержит подводных камней, просто люди не могут его понять. И да, в C++ тоже может быть ошибка в рантайме.
                                          0
                                          Конечно, может. Только часть ошибок всё же отсеивается на этапе компиляции, что уже является огромным подспорьем. Например, в случае отсутствия метода в классе будет ошибка сразу, а не чёрт знает когда.

                                          Хоть в С++ есть куча своих недостатков, но при грамотном подходе (не устраивать опасные игры с указателями и т.п.) ошибки получится словить раньше. Обычно ещё до запуска программы.

                                          А в скриптовых языках (я вообще, а не только про JS), жить без тестов невозможно совершенно, потому что в любой момент можно получить а-та-та.

                                          Но я не сравниваю конкретно эти языки. Я вообще про подход к программированию. Везде свои неудобства, свои преимущества, но мне как человеку который долго программировал на компилируемых языках со статической типизацией привыкнуть к закидонам скриптовых с динамических очень уж непросто.
                                        0
                                        Существует замечательная книжка от Николаса Закаса, «Professional JavaScript».
                                        Там описаны простым языком и очень доступно почти все основные методы наследования, включая смешанные прототипно-конструкторные, паразитические и др. Рекомендуется как дополнение к статье, вернее, как основа, а статья — дополнение.

                                        Only users with full accounts can post comments. Log in, please.