Статьи

Свойство прототипа функции

Свойство prototype — это объект, созданный JavaScript для каждого экземпляра Function() . В частности, он связывает экземпляры объектов, созданные с new ключевым словом, с функцией конструктора, которая их создала. Это делается для того, чтобы экземпляры могли совместно использовать или наследовать общие методы и свойства. Важно отметить, что обмен происходит во время поиска свойств. Помните из первой статьи, что каждый раз, когда вы просматриваете или обращаетесь к свойству объекта, свойство будет искать как в объекте, так и в цепочке прототипов.

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

В следующем коде я создаю массив из конструктора Array() , а затем вызываю метод join() .

Образец: sample118.html

1
2
3
4
5
6
7
<!DOCTYPE html><html lang=»en»><body><script>
 
    var myArray = new Array(‘foo’, ‘bar’);
 
    console.log(myArray.join());
 
</script></body></html>

Метод join() не определен как свойство экземпляра объекта myArray , но у нас есть доступ к join() как если бы он был. Этот метод определен где-то, но где? Ну, это определяется как свойство свойства prototype конструктора Array() . Поскольку join() не найден в экземпляре объекта массива, JavaScript ищет цепочку прототипов для метода, называемого join() .

Хорошо, так почему все так? Действительно, речь идет об эффективности и повторном использовании. Почему каждый экземпляр массива, созданный из функции конструктора массива, должен иметь уникально определенный метод join() когда join() всегда функционирует одинаково? Для всех массивов имеет больше смысла использовать одну и ту же функцию join() не создавая новый экземпляр функции для каждого экземпляра массива.

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


Вы должны заботиться о свойстве prototype по четырем причинам.

Первая причина заключается в том, что свойство prototype используется встроенными функциями конструктора ( Object() , Array() , Function() и т. Д.), Чтобы экземпляры конструктора могли наследовать свойства и методы. Это механизм, который сам JavaScript использует, чтобы экземпляры объектов могли наследовать свойства и методы от свойства prototype функции конструктора. Если вы хотите лучше понять JavaScript, вам нужно понять, как сам JavaScript использует prototype объекта.

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

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

Используя прототип наследования, вы можете создавать эффективные экземпляры объектов, которые используют одни и те же методы. Как уже упоминалось, не все объекты массива, которые являются экземплярами конструктора Array() , нуждаются в собственных методах join() . Все экземпляры могут использовать один и тот же метод join() поскольку этот метод хранится в цепочке прототипов.

Все функции создаются из конструктора Function() , даже если вы напрямую не вызываете конструктор Function() ( var add = new Function('x', 'y', 'return x + z'); ) и вместо этого используете буквенное обозначение ( var add = function(x,y){return x + z}; ).

Когда создается экземпляр функции, ему всегда присваивается свойство prototype , которое является пустым объектом. В следующем примере мы определяем функцию myFunction и затем получаем доступ к свойству prototype которое является просто пустым объектом.

Образец: sample119.html

1
2
3
4
5
6
7
<!DOCTYPE html><html lang=»en»><body><script>
 
    var myFunction = function () { };
    console.log(myFunction.prototype);
    console.log(typeof myFunction.prototype);
 
</script></body></html>

Убедитесь, что вы полностью понимаете, что свойство prototype исходит из конструктора Function() . Только когда мы намереваемся использовать нашу функцию в качестве определяемой пользователем функции конструктора, свойство прототипа используется, но это не меняет того факта, что конструктор Function() дает каждому экземпляру свойство прототипа.


Все эти разговоры о prototype могут стать немного тяжелыми. По сути, prototype — это просто пустое свойство объекта с именем «prototype», созданное JavaScript за кулисами и сделанное доступным с помощью конструктора Function() . Если бы вы делали это вручную, это выглядело бы примерно так:

Образец: sample120.html

1
2
3
4
5
6
7
8
9
<!DOCTYPE html><html lang=»en»><body><script>
 
    var myFunction = function () { };
 
    myFunction.prototype = {};
 
    console.log(myFunction.prototype);
 
</script></body></html>

На самом деле, этот пример кода работает просто отлично, по сути, просто дублируя то, что JavaScript уже делает.

Значением свойства прототипа может быть любое из сложных значений (объектов), доступных в JavaScript. JavaScript будет игнорировать любое свойство прототипа, установленное на примитивное значение.


Хотя его prototype является единственным объектом, он является особенным, поскольку цепочка прототипов связывает каждый экземпляр со свойством прототипа функции конструктора. Это означает, что каждый раз, когда объект создается из функции конструктора с использованием ключевого слова new (или когда обертка объекта создается для примитивного значения), он добавляет скрытую ссылку между созданным экземпляром объекта и свойством prototype используемой функции конструктора. создать это. Эта ссылка известна внутри экземпляра как __proto__ (хотя она предоставляется / поддерживается только через код в Firefox 2+, Safari, Chrome и Android). JavaScript связывает это вместе в фоновом режиме, когда вызывается функция конструктора, и это ссылка this, которая позволяет цепочке прототипов быть, ну, в общем, цепочкой. В следующем примере мы добавляем свойство к собственному prototype конструкторов Array() , к которому мы затем можем получить доступ из экземпляра Array() используя свойство __proto__ установленное для этого экземпляра.

Образец: sample121.html

1
2
3
4
5
6
7
8
9
<!DOCTYPE html><html lang=»en»><body><script>
 
    // This code only works in browsers that support __proto__ access.
    Array.prototype.foo = ‘foo’;
    var myArray = new Array();
 
    console.log(myArray.__proto__.foo);
 
</script></body></html>

Поскольку доступ к __proto__ не является частью официального стандарта ECMA, существует более универсальный способ отследить ссылку от объекта к объекту-прототипу, который он наследует, и это с помощью свойства constructor . Это продемонстрировано в следующем примере.

Образец: sample122.html

01
02
03
04
05
06
07
08
09
10
11
12
13
<!DOCTYPE html><html lang=»en»><body><script>
 
    Array.prototype.foo = ‘foo’;
    var myArray = new Array();
 
    // Trace foo in a verbose way leveraging *.constructor.prototype
    console.log(myArray.constructor.prototype.foo);
 
    // Or, of course, leverage the chain.
    console.log(myArray.foo) // Logs foo.
    // Uses prototype chain to find property at Array.prototype.foo
 
</script></body></html>

В этом примере свойство foo находится внутри объекта-прототипа. Вы должны понять, что это возможно только из-за связи между экземпляром Array() и объектом-прототипом конструктора Array() ( Array.prototype ). Проще говоря, myArray.__proto__ (или myArray.constructor.prototype ) ссылается на Array.prototype .


Поскольку свойство prototype является объектом, последняя остановка в цепочке или поиске прототипа находится в Object.prototype . В следующем коде я создаю myArray , который является пустым массивом. Затем я пытаюсь получить доступ к свойству myArray , которое еще не определено, задействуя цепочку поиска прототипа. Объект myArray проверяется на наличие свойства foo. При отсутствии свойства свойство Array.prototype в Array.prototype , но его там тоже нет. Итак, последнее место, где JavaScript выглядит, это Object.prototype . Поскольку оно не определено ни в одном из этих трех объектов, свойство не undefined .

Образец: sample123.html

1
2
3
4
5
6
7
8
9
<!DOCTYPE html><html lang=»en»><body><script>
 
    var myArray = [];
 
    console.log(myArray.foo) // Logs undefined.
 
    /* foo was not found at myArray.foo or Array.prototype.foo or Object.prototype.foo, so it is undefined.
 
</script></body></html>

Обратите внимание, что цепочка остановилась с Object.prototype . Последнее место, где мы искали foo, было Object.prototype .

Осторожный! Все, что добавлено в Object.prototype, будет отображаться в цикле for.


Как и в цепочке областей действия, цепочка prototype будет использовать первое значение, найденное во время поиска цепочки.

При изменении предыдущего примера кода, если мы добавили одно и то же значение в объекты Object.prototype и Array.prototype , а затем попытались получить доступ к значению в экземпляре массива, возвращаемое значение будет Array.prototype объекта Array.prototype .

Образец: sample124.html

01
02
03
04
05
06
07
08
09
10
11
12
13
<!DOCTYPE html><html lang=»en»><body><script>
 
    Object.prototype.foo = ‘object-foo’;
    Array.prototype.foo = ‘array-foo’;
    var myArray = [];
 
    console.log(myArray.foo);
 
    myArray.foo = ‘bar’;
 
    console.log(myArray.foo) // Logs ‘bar’, was found at Array.foo
 
</script></body></html>

В этом примере значение foo в Array.prototype.foo представляет собой затенение или маскирование значения foo найденного в Object.prototype.foo . Просто помните, что поиск заканчивается, когда свойство найдено в цепочке, даже если то же имя свойства также используется дальше по цепочке.


Возможно заменить значение по умолчанию свойства prototype новым значением. Однако это исключит свойство конструктора по умолчанию, найденное в «готовом» объекте- prototype если вы не укажете его вручную.

В следующем коде мы создаем функцию конструктора Foo , заменяем свойство prototype новым пустым объектом и проверяем, что свойство конструктора нарушено (теперь оно ссылается на менее полезный прототип Object ).

Образец: sample125.html

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
<!DOCTYPE html><html lang=»en»><body><script>
 
    var Foo = function Foo() { };
 
    Foo.prototype = {};
 
    var FooInstance = new Foo();
 
    console.log(FooInstance.constructor === Foo);
    console.log(FooInstance.constructor);
 
    // Compare to code in which we do not replace the prototype value.
 
    var Bar = function Bar() { };
 
    var BarInstance = new Bar();
 
    console.log(BarInstance.constructor === Bar);
    console.log(BarInstance.constructor);
 
</script></body></html>

Если вы намереваетесь заменить свойство prototype по умолчанию (общее с некоторыми шаблонами JS OOP), установленное в JavaScript, вы должны соединить вместе свойство конструктора, которое ссылается на функцию конструктора. В следующем примере мы изменим наш предыдущий код, чтобы свойство constructor снова предоставило ссылку на соответствующую функцию конструктора.

Образец: sample126.html

01
02
03
04
05
06
07
08
09
10
11
12
<!DOCTYPE html><html lang=»en»><body><script>
 
    var Foo = function Foo() { };
 
    Foo.prototype = { constructor: Foo };
 
    var FooInstance = new Foo();
 
    console.log(FooInstance.constructor === Foo);
    console.log(FooInstance.constructor);
 
</script></body></html>

Свойство prototype является динамическим в том смысле, что экземпляры всегда будут получать последнее значение из прототипа независимо от того, когда он был создан, изменен или добавлен. В следующем коде мы создаем конструктор Foo , добавляем свойство x к prototype , а затем создаем экземпляр Foo() именем FooInstance . Далее мы записываем значение x . Затем мы обновляем значение прототипа x и регистрируем его снова, чтобы обнаружить, что наш экземпляр имеет доступ к последнему значению, найденному в объекте prototype .

Образец: sample127.html

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
<!DOCTYPE html><html lang=»en»><body><script>
 
    var Foo = function Foo() { };
 
    Foo.prototype.x = 1;
 
    var FooInstance = new Foo();
 
    console.log(FooInstance.x);
 
    Foo.prototype.x = 2;
 
    console.log(FooInstance.x);
 
</script></body></html>

Учитывая, как работает цепочка поиска, такое поведение не должно вызывать удивления. Если вам интересно, это работает одинаково, независимо от того, используете ли вы объект- prototype по умолчанию или переопределяете его своим. В следующем примере я заменяю объект- prototype по умолчанию, чтобы продемонстрировать этот факт.

Образец: sample128.html

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
<!DOCTYPE html><html lang=»en»><body><script>
 
    var Foo = function Foo() { };
 
    Foo.prototype = { x: 1 };
 
    var FooInstance = new Foo();
 
    console.log(FooInstance.x);
 
    Foo.prototype.x = 2;
 
    console.log(FooInstance.x);
 
</script></body></html>

Вы можете подумать, что вы можете полностью заменить свойство prototype в любое время и что все экземпляры будут обновлены, но это не правильно. Когда вы создаете экземпляр, этот экземпляр будет привязан к prototype который был создан во время создания экземпляра. Предоставление нового объекта в качестве свойства prototype не обновляет связь между уже созданными экземплярами и новым prototype .

Но помните, как я уже говорил ранее, вы можете обновить или добавить первоначально созданный объект- prototype и эти значения остаются подключенными к первому экземпляру (-ам).

Образец: sample129.html

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!DOCTYPE html><html lang=»en»><body><script>
 
    var Foo = function Foo() { };
 
    Foo.prototype.x = 1;
 
    var FooInstance = new Foo();
 
    console.log(FooInstance.x);
 
    // Now let’s replace/override the prototype object with a new Object() object.
    Foo.prototype = { x: 2 };
 
    console.log(FooInstance.x);
    /* FooInstance still references the same state of the prototype object that was there when it was instantiated.
 
    // Create a new instance of Foo()
    var NewFooInstance = new Foo();
 
    // The new instance is now tied to the new prototype object value ({x:2};).
    console.log(NewFooInstance.x);
 
</script></body></html>

Ключевая идея, которую стоит здесь отбросить, состоит в том, что прототип объекта не следует заменять новым объектом, как только вы начнете создавать экземпляры. Это приведет к тому, что экземпляры будут иметь ссылку на разные прототипы.

Надеемся, что в этот момент в статье описывается, как JavaScript сам использует свойство prototype для наследования ( Array.prototype ). Этот же шаблон можно использовать при создании не собственных пользовательских функций конструктора. В следующем примере мы берем классический объект Person и имитируем шаблон, который JavaScript использует для наследования.

Образец: sample130.html

01
02
03
04
05
06
07
08
09
10
11
12
13
14
<!DOCTYPE html><html lang=»en»><body><script>
 
    var Person = function () { };
 
    // All Person instances inherit the legs, arms, and countLimbs properties.
    Person.prototype.legs = 2;
    Person.prototype.arms = 2;
    Person.prototype.countLimbs = function () { return this.legs + this.arms;
 
    var chuck = new Person();
 
    console.log(chuck.countLimbs());
 
</script></body></html>

В этом коде создается функция конструктора Person() . Затем мы добавляем свойства в свойство prototype Person() , которые могут наследоваться всеми экземплярами. Очевидно, что вы можете использовать цепочку прототипов в своем коде так же, как JavaScript использует ее для наследования нативных объектов.

В качестве хорошего примера того, как вы можете использовать это, вы можете создать функцию конструктора, экземпляры которой наследуют свойства legs и arms если они не предоставлены в качестве параметров. В следующем примере, если конструктору Person() передаются параметры, параметры используются в качестве свойств экземпляра, но если один или несколько параметров не предоставлены, возникает запасной вариант. Эти свойства экземпляра затемняют или маскируют унаследованные свойства, давая вам лучшее из обоих миров.

Образец: sample131.html

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
<!DOCTYPE html><html lang=»en»><body><script>
 
    var Person = function (legs, arms) {
        // Shadow prototype value.
        if (legs !== undefined) { this.legs = legs;
        if (arms !== undefined) { this.arms = arms;
    };
 
    Person.prototype.legs = 2;
    Person.prototype.arms = 2;
    Person.prototype.countLimbs = function () { return this.legs + this.arms;
 
    var chuck = new Person(0, 0);
 
    console.log(chuck.countLimbs());
 
</script></body></html>

Прототипное наследование было задумано так, чтобы позволить цепочки наследования, которые имитируют шаблоны наследования, встречающиеся в традиционных объектно-ориентированных языках программирования. Чтобы один объект наследовал от другого объекта в JavaScript, все, что вам нужно сделать, — это создать экземпляр объекта, от которого вы хотите наследовать, и назначить его свойству prototype объекта, который выполняет наследование.

В следующем примере кода объекты Chef ( cody ) наследуются от Person() . Это означает, что если свойство не найдено в объекте Chef , оно будет проверяться в прототипе функции, создавшей объекты Person() . Чтобы подключить наследование, все, что вам нужно сделать, это создать экземпляр Person() в качестве значения для Chef.prototype ( Chef.prototype = new Person(); ).

Образец: sample132.html

01
02
03
04
05
06
07
08
09
10
11
12
13
14
<!DOCTYPE html><html lang=»en»><body><script>
 
    var Person = function () { this.bar = ‘bar’ };
    Person.prototype.foo = ‘foo’;
 
    var Chef = function () { this.goo = ‘goo’ };
    Chef.prototype = new Person();
    var cody = new Chef();
 
    console.log(cody.foo);
    console.log(cody.goo);
    console.log(cody.bar);
 
</script></body></html>

Все, что мы сделали в этом примере, это использовали систему, которая уже была на месте с нативными объектами. Учтите, что Person() мало чем отличается от значения Object() по умолчанию для свойств прототипа. Другими словами, это именно то, что происходит, когда свойство prototype, содержащее пустое значение Object() умолчанию, ищет прототип созданной функции конструктора ( Object.prototype ) для унаследованных свойств.