19 января 2009

В прошлых статьях было немного освещены вопросы объектов в JavaScript. Теперь пришло время посмотреть как можно создавать и использовать их потенциал. В этой статье есть базовая часть этого большого айсберга под названием JavaScript. Подробности будут в продолжении :)

Создание и использование объектов

Итак, в отличии от языков, где реализована класс-объектная парадигма, нам не нужно создавать сначала класс, чтобы потом создать объект класса. Мы можем сразу создать объект, что и сделаем в следующем примере:

    test={
     simple_property: 'Hello',
     object_property: {
      user_1: 'Петя',
      user_2: 'Вася'
     },
     function_property: function(user) {
      alert(this.simple_property + ', ' + this.object_property[user]);
     }
    }

    test.function_property('user_1'); //Hello, Петя.

Перед нами объект test, имеющий 3 свойства, названия которых, как я надеюсь, говорят сами за себя. Больше всего нас в нем интересует свойство function_property, содержащее функцию. Такую функцию можно назвать методом объекта.

В нашей функции дважды используется ключевое слово this, которое является указателем (т.е. ссылкой) на объект, из которого вызывается функция. Таким образом, this.simple_property = test.simple_property = ‘Hello’, а this.object_property[user] = test.object_property[user] = ‘Петя’.

Необходимо четко осознавать, this всегда указывает именно на объект, из которого вызвана функция, а не на объект, к которому она принадлежит. Хотя в данном примере это один и тот же объект, это не всегда так.

    test.function_property('user_1'); //Hello, Петя.

    test2=new Object(); //Еще одна форма создания нового объекта, аналогичная test2={}

    test.function_property.call(test2, 'user_1'); //ошибка
    /* Метод call позволяет вызвать функцию от имени другого объекта. В данном случае, мы вызываем метод function_property объекта test, и его this указывает уже не на объект test, а на объект test2. А т.к. в нем нет свойства object_property, то при попытке получить this.object_property[user]скрипт выдаст ошибку */

    //попробуем исправить ситуацию
    test2.simple_property='Good day';
    test2.object_property=test.object_property; //В данном случае воспользуемся указанием объекта по ссылке, чтобы не дублировать код

    test.function_property.call(test2, 'user_1'); //Good day, Петя.

Из примера также должно быть видно, что нет четких этапов создания и использования объекта. Объект может быть как угодно модифицирован в любое время — до, после и даже во время использования. Это тоже важное отличие от «традиционного» ООП.

Конструктор

В примере выше мы создавали 2 объекта, обладающих некой схожестью. И там и там имелись свойства simple_property и object_property. Очевидно, что при написании реального кода также нередко встает задача создания одинаковых или просто похожих объектов. И разумеется, мы не должны каждый такой объект создавать вручную.

На помощь нам придет конструктор. Конструктор в JavaScript — это не часть класса (потому что здесь нет классов), а просто самостоятельная функция. Самая обычная функция.

    make_me=function(_name) {
    alert('меня запустили');
    this.name=_name;
    this.show_name=function() {alert(this.name);}
    }

    child=new make_me('Вася'); //меня запустили
    /* Давайте разберемся, что здесь происходит. Интерпретатор видит оператор new и проверяет, что находится справа от него. Т.к. make_me - это функция, и она может быть использована в качестве контруктора, то создается новый объект в памяти и запускается на выполнение функция make_me, причем ее this указывает как раз на этот новый объект. Далее этому объекту добавляется свойство name, которому присваивается значение из аргумента _name, и метод show_name. Также (не знаю в какой именно момент, но это и не важно) переменная child начинает указывать на наш новенький, только что рожденный объект */

    alert(child.name); //Вася
    child.show_name(); //Вася

    child2=new make_me('Петя');
    child2.show_name(); //Петя

    child2.show_name=function() {alert('Не буду говорить свое имя');} //Не забываем, что можем изменять наши объекты в любой момент
    child2.show_name(); //Не буду говорить свое имя

    child.show_name(); //Вася - дети никак не влияют друг на друга

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

Если мы вспомним про описание типов данных в начале статьи, то становится понятно, что Object и его подтипы (Function, Array и другие) — это на самом деле конструкторы, придающие создаваемому объекту возможности функции, массива и т.д.

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

Прототип

Как у каждого ребенка есть отец и мать (хотя бы в биологическом смысле), также они есть и у каждого объекта в JavaScript. И если отец, как мы определелись, работает конструктором, то мать — это как раз прототип. Посмотрим, как это происходит:

    make_me=function(_name) {
    alert('меня запустили');
    this.name=_name;
    this.show_name=function() {alert(this.name);}
    }
    /*
    Видя ключевое слово function, интерпретатор проверяет код справа от него, и т.к. все ок - создает новый объект в памяти, который одновременно является нашей функцией. Затем, автоматически (без участия программиста) для этой функции создается свойство prototype, ссылающееся на пустой объект. Если бы мы это делали вручную, это выглядело бы как make_me.prototype=new Object();

    Затем, данному объекту (на который указывает свойство prototype) также автоматически добавляется свойство constructor, указывающее обратно на функцию. Получается такая вот циклическая ссылка.

    Теперь этот объект, который можно описать как {constructor: ...здесь ссылка на фунцию...} - и есть прототип функции.
    */

    alert(typeof make_me.prototype); //Object - действительно, объект
    alert(typeof make_me.prototype.constructor); //Function - это наша функция
    alert(make_me.prototype.constructor === make_me); //true

    make_me.prototype.set_name=function(_name) {this.name=_name;} //Добавляем в прототип функции make_me новый метод

    child=new make_me('Вася'); //меня запустили
    /* Теперь помимо всего того, что описано в предыдущем примере, дополнительно в объекте child создается скрытое свойство [[Prototype]], которое указывает на тот же объект, что и make_me.prototype. Т.к. свойство скрыто, мы не можем ни просмотреть его значение, ни изменить его - однако оно играет важную роль в дальнейшей работе */

    alert(child.name); //Вася
    child.show_name(); //Вася

    child.set_name('Коля');
    /* Сначала, интерпретатор ищет метод set_name в объекте child. Так как его там нет, он продолжает поиск в свойстве child.[[Prototype]], находит его там и запускает. */
    child.show_name(); //Коля - теперь Васю зовут Коля :)

    make_me.prototype.show_name2=function() {alert('Привет, ' + this.name;} //Т.к. прототип - это обычный объект, мы точно также можем его менять на лету

    child2=new make_me('Петя');
    child2.show_name2(); //Привет, Петя
    child.show_name2(); //Привет, Коля - изменения в прототипе влияют не только на вновь созданные объекты, но и на все старые

    child2.show_name2=function() {alert('Не буду говорить свое имя');} //Мы по прежнему можем изменить сам объект, при этом новый метод show_name2 в данном объекте (и только в нем) как бы "затрет" старый метод из прототипа
    child2.show_name2(); //Не буду говорить свое имя - т.к. у нас теперь есть собственный метод show_name2, то он и вызывается, и поиск в прототипе не происходит

    child.show_name2(); //Привет, Коля - здесь все по прежнему

    make_me.prototype={prop: 'hello'} //Попробуем пересоздать прототип заново

    alert(child.prop); //undefined
    child.show_name2(); //Привет, Коля
    /* Если вспомнить, что такое работа по ссылке, то все понятно. Пересоздание прототипа рвет связь, и теперь свойство [[Prototype]] у объектов child и child2 указывают на один объект (который раньше был прототипом функции make_me), а свойство make_me.prototype - на другой объект, который является новым прототипом функции make_me */

    child3=new make_me('Олег');
    alert(child3.prop); //hello - что и следовало ожидать

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

Немного о терминологии

До тех пор, пока первичная связь между конструктором и прототипом не разорвана, мы можем наблюдать следующую картину:

    make_me=function(_name) {
    alert('меня запустили');
    this.name=_name;
    this.show_name=function() {alert(this.name);}
    }

    make_me.prototype.set_name=function(_name) {this.name=_name;}
    child=new make_me('Вася');

    alert(typeof make_me.prototype); //object - у функции есть свойство prototype
    alert(typeof child.prototype); //undefined - у созданного объекта НЕТ свойства prototype
    alert(child.constructor.prototype === make_me.prototype); //true - зато у объекта есть свойство constructor, которое указывает на функцию-конструктор make_me, у которой, в свою очередь, есть свойство prototype

Как я заметил после чтения многочисленных форумов на эту тему, основные проблемы возникают у людей, когда они путают свойство prototype у функции и скрытое свойство [[Prototype]] у объекта, созданного с помощью этой функции.
Оба этих свойства являются ссылкой на один и тот же объект (до тех пор, пока первичная связь прототипа с конструктором не нарушена), но это тем не менее разные свойства, с разными именами, одно из них доступно для программиста, а другое нет.

Необходимо всегда четко понимать, что если речь идет о прототипе конструктора — то это всегда свойство prototype, а если о прототипе созданного объекта — то это скрытое свойство [[Prototype]].

Возможно это тоже будет интересно
Комментариев нет

Оставить комментарий