Prototype, proto и оператор new

  • Tutorial
В этой статье я кратко в примерах объясню что такое свойства __proto__, prototype и работу оператора new в JavaScript.

Свойство __proto__


Абсолютно любой объект в JavaScript имеет свойство __proto__. Это скрытое системное свойство, и не во всех реализациях языка оно доступно пользователю.
При обращении к любому свойству объекта, оно в первую очередь ищется в самом объекте:
var obj = {ownProperty: 1};
console.log(obj.ownProperty);// 1
Но если его там нет, поиск происходит в свойстве __proto__:
obj.__proto__ = {propertyOfProto: 2};
console.log(obj.propertyOfProto);// 2
Если его нет и там, оно ищется дальше по цепочке:
obj.__proto__.__proto__ = {propertyOfProtosProto: 3};
console.log(obj.propertyOfProtosProto);// 3
Эта цепочка называется цепочкой прототипов (prototype chain).



__proto__ любого значения (кроме null и undefined) ссылается на prototype соответствующего ему типу данных:
(0).__proto__ === Number.prototype &&
false.__proto__ === Boolean.prototype &&
"string".__proto__ === String.prototype &&
(new Date).__proto__ === Date.prototype &&
(function(){}/* new Function */).__proto__ === Function.prototype
Все типы данных наследуются от Object, это означает что к примеру:
Number.prototype.__proto__ === Object.prototype
И наконец, завершение цепочки:
Object.prototype.__proto__ === null

Свойство prototype


А чем же тогда является свойство prototype? Это обычное свойство, ничем не отличающиеся от любых других свойств. За исключением двух особенностей:

1) Функции в JavaScript имеют свойство prototype. Оно по умолчанию является объектом с единственным свойством constructor, которое ссылается на саму функцию.



2) Свойство prototype используется при создании новых объектов оператором new.

Оператор new


Этот оператор делает следущее:

1) Создает пустой объект:
var instance = {};

2) Устанавливает __proto__ этому объекту ссылкой на prototype функции-класса:
instance.__proto__ = FnClass.prototype;

3) Применяет функцию-класс к нашему новосозданному объекту:
constructorReturns = FnClass.apply(instance, arguments);
(т.е. исполняет функцию FnClass, передавая ей instance в качестве this и аргументы в виде массива arguments)

4) Возвращает экземпляр функции-класса, но если FnClass нам вернул обьект, тогда его:
return constructorReturns instanceof Object ? constructorReturns : instance;

Функцией-классом я называю функцию, к которой впоследствии ожидается применение оператора new. Такие функции принято именовать с заглавной буквы.

Использование __proto__ в ваших скриптах


Т.к. свойство __proto__ является скрытым, и не описано в спецификации языка, то использование его в явном виде некорректно. Так что никогда не пишите так как я выше в примерах :) Этот код только для консоли.
Однако в последней (действующей) спецификации ECMA Script 5 наконец-то появились два метода, позволяющие манипулировать свойством __proto__, это Object.create и Object.getPrototypeOf.
Поясню их работу в двух простых примерах:


//var myObj = {__proto__: {property1_OfProto: 1}}
var myObj = Object.create({property1_OfProto: 1});


//myObj.__proto__.property2_OfProto = 2
Object.getPrototypeOf(myObj).property2_OfProto = 2;

Если вы используете более раннюю версию JavaScript, то метод Object.create можете создать самостоятельно:

if(!Object.create){
	Object.create = function(proto){
		var Fn = function(){};
		Fn.prototype = proto;
		return new Fn;
	}
}

C getPrototypeOf ситуация сложнее, его можно эмулировать только для функций, и только при условии что constructor этой функции не был изменен:

if(!Object.getPrototypeOf){
    if( (new Object).__proto__ !== Object.prototype ){
        // may return incorrect value if fn.prototype has been modified
        Function.getPrototypeOf = function(fn){
            if(typeof(fn)!=='function')
                throw new TypeError('Function.getPrototypeOf called on non-function');
            return fn.constructor.prototype;
        }
    }else{
        Object.getPrototypeOf = function(obj){
            return obj.__proto__;
        }
    }
}

А лучше, как посоветовали в комментах, используйте библиотеку github.com/kriskowal/es5-shim

Далее — о классах в JavaScript...

Поделиться публикацией
Комментарии 44
    +18
    Это моя первая статья на хабре
      +1
      Кстати, для желающих реально разобраться в теме — очень рекомендую почитать ветку комментов от dsCode:
      habrahabr.ru/post/111393/#comment_3555912

      Начиная с «В ECMAScript нет (пока) сахара для классов».
      Очень классно и познавательно. Там можно и разобраться, что такое __proto__, prototype и new на нормальных, понятных примерах.
      +2
      В консоли Хрома:

      > function Test(){} var q=new Test(); console.log(q.constructor.prototype===q.__proto__);
      true


      т.е. __proto__ равно constructor.prototype?
        0
        Да.
          +2
          Только для объектов, созданных «простыми» конструкторами.

          > function Test() {} Test.prototype = {}; var q = new Test(); console.log(q.constructor.prototype===q.__proto__);

          false
            0
            Дополню ответ mayorovp

            Не стоит забывать что

            function Test() {} Test.prototype = {constructor:Test}; var q = new Test(); console.log(q.constructor.prototype===q.__proto__); // true

            true потому, что свойство constructor является свойством прототипа, а не родным свойством объекта. При использовании своего объекта в качестве прототипа такая связь теряется (раз ({}).__proto__ == Object.prototype
            значит ({}).constructor == Object), поэтому связь нужно восстановить (Test.prototype = {constructor:Test};). Советую почитать про это здесь.
            0
            А как это на практике можно использовать?
              +2
              Вы про __proto__?
              Ну например если в переопределенном методе какого либо класса вам нужно вызвать этод же метод родительского, тогда можете сделать так:
              Object.getPrototypeOf(this).myMethod.apply(this, arguments);
                0
                Неверно. Данный подход зациклится при использовании сложных иерархий (начиная с трех уровней) — можете проверить.

                Пусть иерархия выглядит так:

                var a = {};
                var b = Object.create(a);
                var c = Object.create(b);

                a.name = «a»;
                b.name = «b»;
                c.name = «c»;

                b.some_func = function() { console.log(Object.getPrototypeOf(this).name); }
                c.some_func(); // выведет b, т.е. нам не удалось получить a как прототип this
                  0
                  так все же верно,c inherits b inherits a, вызов c::parent вернет b, вызов c::parent::parent вернет 'a'.
                    +1
                    Хорошо, вот другой пример.

                    var a = {};
                    var b = Object.create(a);
                    var c = Object.create(b);

                    a.some_func = function() { console.log(«a»); }
                    b.some_func = function() { console.log(«b»); Object.getPrototypeOf(this).some_func.apply(this, arguments); }
                    c.some_func = function() { console.log(«c»); Object.getPrototypeOf(this).some_func.apply(this, arguments); }

                    a.some_func(); //a
                    b.some_func(); //b a
                    c.some_func(); //c b b b b…

                    Это я к чему? Да к тому, что конструкция Object.getPrototypeOf(this).myMethod.apply(this, arguments); делает нечто, совершенно отличающиеся от вызова родительского метода.
                      0
                      Угу, получается надо юзать Object.getPrototypeOf(this).some_func.apply(Object.getPrototypeOf(this), arguments);?
                        +1
                        Для метода, не меняющего состояние объекта — пойдет.

                        Если метод меняет состояние, то первым аргументом apply может быть только this.

                        Для решения данной проблемы надо вспомнить, что вообще-то мы знаем, от какого объекта унаследовались. То есть правильный вариант таков:

                        var a = {};
                        var b = Object.create(a);
                        var c = Object.create(b);

                        a.some_func = function() { console.log(«a»); }
                        b.some_func = function() { console.log(«b»); a.some_func.apply(this, arguments); }
                        c.some_func = function() { console.log(«c»); b.some_func.apply(this, arguments); }

                        a.some_func(); //a
                        b.some_func(); //b a
                        c.some_func(); //c b a

                        На случай неизвестной заранее иерархии или для дополнительной защиты, можно накрутить что-нибудь с замыканиями.
                        Но Object.getPrototypeOf(this) использовать для подобной цели нельзя.
              0
              >поиск происходит в другом объекте, свойстве __proto__
              >оно [свойство prototype] по умолчанию является объектом

              Такие формулировки вводят в заблуждение. Свойство __proto__ не содержит никаких объектов, оно содержит ссылку на объект. Это важно.

              >поиск происходит в другом объекте, на который ссылается свойство __proto__
              >оно [свойство prototype] по умолчанию ссылается на объект
                +2
                В JS Вообще все переменные содержат ссылку, кроме простых, так что в данном случае это не важно.
                +6
                Мне вот только одно не понятно, почему вы не поискали подобные темы на хабре, имхо уже топиков 20 обсудало эту тему.
                habrahabr.ru/post/108915/
                habrahabr.ru/post/133034/
                habrahabr.ru/post/120193/

                И таких тем еще куча, тема очередной дубль своими словами о том что уже давно написали, разжевали.
                  0
                  Да ладно Вам, никто из читающих не будет искать, а так хоть узнаю что-то новое.
                    +2
                    Если бы новое — ладно, но как сказал TheShock тему обсосали 200 раз, в свое время на хабре на эту тему по 1 посту в день писали. А так такую статью уже каждый написать может открываешь гугл ищешь готовую статью копируешь, меняешь пару предложение — все.
                    +1
                    Искал. Но так и не нашел простого и понятного объяснения свойства __proto__.

                    Вы хотите сказать что статья эта никому не нужна, и лучше бы ее не было?
                    Однако уже больше сотни человек добавили ее в избранное.
                      0
                      Но так и не нашел простого и понятного объяснения свойства __proto__.

                      Вы мне напомнили этот комикс:
                        0
                        Русская версия, на всяк случай:
                          0
                          я же не стандарт предлагаю, а просто хочу помочь некоторым людям разобраться в javascript
                            0
                            Расшифрую)
                            — Ситуация: есть 14 разных топиков, объясняющих __proto__ просто и понятно
                            — 14?! Долбануться! Нужно написать один универсальный топик, объясняющий __proto__ реально просто и понятно
                            — Ситуация: есть 15 разных топиков, объясняющих __proto__ просто и понятно
                              0
                              ну и в чем зло 15го топика?
                                0
                                Та ни в чём. Он просто не нужен)
                                  0
                                  Вам уже не нужен (т.к. вы уже полностью разобрались в теме), а кому-то еще нужен
                                +1
                                Мне кажется чем больше литературы по определенной теме, тем лучше — есть выбор.
                                  0
                                  С другой стороны — осложняется поиск)
                                  • НЛО прилетело и опубликовало эту надпись здесь
                                      +1
                                      Просто реально — есть столько не раскрытых или плохо раскрытых тем. Лучше эту энергию затратить на что-то полезное, а не на очередной клон той же статьи.
                                        0
                                        Без определенного фундамента — сложно раскрыть более сложные темы.
                                          0
                                          О чём вы говорите? Этим фундаментом уже заложено всё, что можно. Давным давно пора строить дом, а не продолжать в десятый раз ложить фундамент того же размера, но под другим углом.
                        +2
                        Если рекомендуете для старых браузеров эмулировать функции `Object.create` и `Object.getPrototypeOf`, то, лучше уж, давайте ссылку на
                        github.com/kriskowal/es5-shim — тут эти методы описаны максимально по стандарту и учитывают очень много разных нюансов.
                        Например: функция `Object.create` задаёт свойство `__proto__` у созданного объекта, а в функция `Object.getPrototypeOf` несколько универсальные приведённого вами кода:

                        Object.getPrototypeOf = function getPrototypeOf(object) {
                                return object.__proto__ || (
                                    object.constructor
                                        ? object.constructor.prototype
                                        : prototypeOfObject
                                );
                            };
                        
                          0
                          ок. спасибо за ссылочку, не знал раньше про эту библиотеку
                          0
                          Как корректно поменять __proto__ объекта, чтобы превратить его в массив (серверный JS)? Такой код работает:
                          a.__proto__=Array.prototype
                          Но вы говорите, что использовать его некорректно. Есть ли какой-нибудь корректный способ без копирования элементов массива?
                            0
                            var MyArray = function () {};
                            MyArray.prototype = new Array();
                            var a = new MyArray();
                            
                            // same as
                            // var a = {};
                            // a.__proto__=Array.prototype
                            
                            a.push(42);
                            console.log(a.length); // 1
                            

                            Если просто копировать аттрибуты прототипа массивы, то сломается length
                              0
                              А разве не надо ещё и конструктор прототипу прописать?
                              MyArray.prototype.constructor = MyArray;
                              А то получится, что конструктор у нас будет системным массивом, а не нашей функцией.
                                0
                                Если будет использоваться в instanceof — пишите. Лучше не изобретать свой массив (не наследовать), а написать обертку над существующим (см комент ниже).
                                0
                                Основная проблема такого подхода вот в чём:
                                var MyArray = function () {};
                                MyArray.prototype = new Array();
                                var a = new MyArray();
                                a[1] = 123;
                                a.length; // 0
                                
                                  0
                                  Угу, так же как и с a.__proto__=Array.prototype
                                  0
                                  Таким образом, вы сделали массив из пустого объекта. А если имеется объект типа {0:'x', 1:'y'}, то его можно превратить в «почти-массив» таким кодом:
                                  obj.__proto__ = Array.prototype;
                                  obj.length = <длина массива>; // Допустим, в нашей задаче она известна


                                  И такой подход точно работает (неправильно отрабатывает конструкция типа for (var i in obj), но ее можно заменить методами forEach и т. д.
                                  Есть ли способ проделать то же самое, не трогая свойство __proto__?
                                    0
                                    Эта конструкция всегда неправильно работает: for (var i in obj).
                                    То, что вы хотите — можно сделать так:

                                    var obj = {0:'x', 1:'y'};
                                    obj.length = 2;
                                    console.log( [].slice.call(obj) ); // ['x', 'y' ]
                                    


                                    Но если вас интересует теоретическая возможность, то я думаю, что кроссбраузерно и стандартно — это врядли реально.
                                      0
                                      Метод slice копирует элементы массива и создает новый массив. Соответственно, тратятся ресурсы на ненужную операцию.
                                      А кроссбраузерность не нужна — речь шла о серверном JS. На клиенте можно было бы, например, воспользоваться методом jQuery.makeArray (который работает примерно так же, как предложенный вами вариант).
                                      Вообщем, пока я решил проблему хаком указанным выше, правильного варианта, по-видимому, не существует.
                                +5
                                Поставил плюс, но, пожалуйста, в следующий раз выбирайте тему, не обсосаную 200 раз.

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

                                Самое читаемое