Переклад не закінчено. Будь ласка, допоможіть перекласти цю статтю з англійської.

JavaScript є дещо незвичним для розробників із досвідом у клас-орієнтованих мовах програмування (таких як Java чи C++), оскільки він є динамічним і не надає реалізації класу на рівні мови (ключове слово class введене в ES2015, але є синтаксичним цукром, тому JavaScript залишається прототип-орієнтованою мовою).

Якщо йдеться про наслідування, то JavaScript має лише одну конструкцію: обʼєкти. Кожен обʼєкт містить у якості прихованої властивості посилання на інший обʼєкт, який називається прототипом даного. Цей прототип у свою чергу має свій прототип, і так далі, поки не досягаємо обʼєкта, прототипом якого є null. За визначенням, null не має прототипу і слугує фінальним значенням у цьому ланцюжку прототипів.

Майже всі обʼєкти в JavaScript є екземплярами Object, який стоїть на вершині ланцюжка прототипів.

Прототипна модель наслідування нерідко розглядається як одна зі слабкостей JavaScript, але фактично вона є потужнішою за класичну. Класичну модель, наприклад, досить нескладно побудувати на основі прототипної.

 

Наслідування за допомогою ланцюжка прототипів

Наслідування властивостей

Обʼєкти в JavaScript є динамічними "наборами" властивостей (будемо називати їх власними властивостями). Обʼєкти в JavaScript містять посилання на свій обʼєкт-прототип. При зверненні до будь-якої властивості обʼєкта пошук цієї властивості відбувається не лише всередині обʼєкта, але і в його прототипі, і у прототипі прототипа, і так далі, поки властивість із заданим імʼям не буде знайдена, або не буде досягнуто кінця ланцюжка прототипів.

Відповідно до стандарту ECMAScript, запис someObject.[[Prototype]] використовується для вказання прототипа someObject. Починаючи з ECMAScript 2015, доступ до [[Prototype]] отримують за допомогою Object.getPrototypeOf() та Object.setPrototypeOf(). Це еквівалентно властивості JavaScript __proto__, яка не відповідає стандарту, але де-факто імплементована у більшості браузерів.

Ви не повинні плутати це з func.prototype, властивістю функції, яка натомість визначає, що [[Prototype]] має бути присвоєний усім екземплярам обʼєктів, створених цією функцією як конструктором. Властивість Object.prototype представляє прототип Object.

Ось що відбувається при спробі звернення до властивості:

// За допомогою функції f створимо обʼєкт o із власними властивостями a і b:
let f = function () {
   this.a = 1;
   this.b = 2;
}
let o = new f(); // {a: 1, b: 2}

// додаємо властивості у прототип фунції f
 f.prototype.b = 3;
 f.prototype.c = 4;

// не присвоюйте значення f.prototype = {b:3,c:4}; це зруйнує ланцюжок прототипів
// o.[[Prototype]] має властивості b та c.
// o.[[Prototype]].[[Prototype]] це Object.prototype.
// Нарешті, o.[[Prototype]].[[Prototype]].[[Prototype]] це null.
// Це кінець ланцюжка прототипів, оскільки null,
// за визначенням, не має [[Prototype]].
// Отже, повністю ланцюжок прототипів виглядатиме так:
// {a: 1, b: 2} ---> {b: 3, c: 4} ---> Object.prototype ---> null

console.log(o.a); // 1
// Чи є 'a' власною влативістю для o? Так, і її значення 1.

console.log(o.b); // 2
// Чи є 'b' власною властивістю для o? Так, і її значення 2.
// Прототип також має властивість 'b', але вона не відвідується. 
// Це називається "затінюванням властивості".

console.log(o.c); // 4
// Чи є 'c' власною властивістю для o? Ні, перевіряємо його прототип.
// Чи є 'c' власною властивістю для o.[[Prototype]]? Так, і її значення 4.

console.log(o.d); // undefined
// Чи є 'd' власною властивістю для o? Ні, перевіряємо його прототип.
// Чи є 'd' власною властивістю для o.[[Prototype]]? Ні, перевіряємо його прототип.
// o.[[Prototype]].[[Prototype]] це null, закінчуємо пошук,
// властивості не знайдено, повертаємо undefined.

Посилання на код

Присвоєння значення властивості обʼєкта створює власну властивість. Єдиним винятком є той випадок, коли властивість успадковується разом із геттером або сеттером.

Успадкування "методів"

JavaScript не має "методів" у тому вигляді, як у клас-орієнтованих мовах. У JavaScript будь-яку функцію можна додати до обʼєкта в якості властивості. Успадкована фукція поводиться так само як і будь-яка інша властивість, включаючи затінювання властивостей, як було показано вище (в даному випадку це є певною формою заміщення методу).

Коли виконується успадкована функція, значення this вказує на обʼєкт, що її наслідує, а не на обʼєкт-прототип, у якому функція є власною властивістю.

var o = {
  a: 2,
  m: function() {
    return this.a + 1;
  }
};

console.log(o.m()); // 3
// Коли тут викликається o.m, 'this' вказує на o

var p = Object.create(o);
// p є обʼєктом, що наслідує від o

p.a = 4; // створюється власна властивість 'a' для p
console.log(p.m()); // 5
// коли викликається p.m, 'this' вказує на p.
// Тобто коли p наслідує функцію m обʼєкта o, 
// 'this.a' означає p.a, власну властивість 'a' обʼєкта p

Різні способи створення обʼєктів та результуючого ланцюжка прототипів

Обʼєкти, що створені за допомогою синтаксичних конструкцій

var o = {a: 1};

// The newly created object o has Object.prototype as its [[Prototype]]
// o has no own property named 'hasOwnProperty'
// hasOwnProperty is an own property of Object.prototype. 
// So o inherits hasOwnProperty from Object.prototype
// Object.prototype has null as its prototype.
// o ---> Object.prototype ---> null

var b = ['yo', 'whadup', '?'];

// Arrays inherit from Array.prototype 
// (which has methods indexOf, forEach, etc.)
// The prototype chain looks like:
// b ---> Array.prototype ---> Object.prototype ---> null

function f() {
  return 2;
}

// Functions inherit from Function.prototype 
// (which has methods call, bind, etc.)
// f ---> Function.prototype ---> Object.prototype ---> null

За допомогою конструктора

Конструктор в JavaScript це "просто" функція, яка може бути викликана з новим оператором.

function Graph() {
  this.vertices = [];
  this.edges = [];
}

Graph.prototype = {
  addVertex: function(v) {
    this.vertices.push(v);
  }
};

var g = new Graph();
// g is an object with own properties 'vertices' and 'edges'.
// g.[[Prototype]] is the value of Graph.prototype when new Graph() is executed.

За допомогою Object.create

У ECMAScript 5 введений новий метод: Object.create(). Виклик цього методу створює новий об'єкт. Прототип цього об'єкту це перший аргумент функції:

var a = {a: 1}; 
// a ---> Object.prototype ---> null

var b = Object.create(a);
// b ---> a ---> Object.prototype ---> null
console.log(b.a); // 1 (inherited)

var c = Object.create(b);
// c ---> b ---> a ---> Object.prototype ---> null

var d = Object.create(null);
// d ---> null
console.log(d.hasOwnProperty); 
// undefined, because d doesn't inherit from Object.prototype

With the class keyword

ECMAScript 2015 introduced a new set of keywords implementing classes. Although these constructs look like those familiar to developers of class-based languages, they are not the same. JavaScript remains prototype-based. The new keywords include class, constructor, static, extends, and super.

'use strict';

class Polygon {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
}

class Square extends Polygon {
  constructor(sideLength) {
    super(sideLength, sideLength);
  }
  get area() {
    return this.height * this.width;
  }
  set sideLength(newLength) {
    this.height = newLength;
    this.width = newLength;
  }
}

var square = new Square(2);

Продуктивність

The lookup time for properties that are high up on the prototype chain can have a negative impact on performance, and this may be significant in code where performance is critical. Additionally, trying to access nonexistent properties will always traverse the full prototype chain.

Also, when iterating over the properties of an object, every enumerable property that is on the prototype chain will be enumerated.

To check whether an object has a property defined on itself and not somewhere on its prototype chain, it is necessary to use the hasOwnProperty method which all objects inherit from Object.prototype.

hasOwnProperty is the only thing in JavaScript which deals with properties and does not traverse the prototype chain.

Note: It is not enough to check whether a property is undefined. The property might very well exist, but its value just happens to be set to undefined.

Bad practice: Extension of native prototypes

One mis-feature that is often used is to extend Object.prototype or one of the other built-in prototypes.

This technique is called monkey patching and breaks encapsulation. While used by popular frameworks such as Prototype.js, there is still no good reason for cluttering built-in types with additional non-standard functionality.

The only good reason for extending a built-in prototype is to backport the features of newer JavaScript engines, like Array.forEach.

Приклад 

B shall inherit from A:

function A(a) {
  this.varA = a;
}

// What is the purpose of including varA in the prototype when A.prototype.varA will always be shadowed by
// this.varA, given the definition of function A above?
A.prototype = {
  varA: null,  // Shouldn't we strike varA from the prototype as doing nothing?
      // perhaps intended as an optimization to allocate space in hidden classes?
      // https://developers.google.com/speed/articles/optimizing-javascript#Initializing instance variables
      // would be valid if varA wasn't being initialized uniquely for each instance
  doSomething: function() {
    // ...
  }
};

function B(a, b) {
  A.call(this, a);
  this.varB = b;
}
B.prototype = Object.create(A.prototype, {
  varB: {
    value: null, 
    enumerable: true, 
    configurable: true, 
    writable: true 
  },
  doSomething: { 
    value: function() { // override
      A.prototype.doSomething.apply(this, arguments); // call super
      // ...
    },
    enumerable: true,
    configurable: true, 
    writable: true
  }
});
B.prototype.constructor = B;

var b = new B();
b.doSomething();

The important parts are:

  • Types are defined in .prototype.
  • You use Object.create() to inherit.

prototype та Object.getPrototypeOf

JavaScript is a bit confusing for developers coming from Java or C++, as it's all dynamic, all runtime, and it has no classes at all. It's all just instances (objects). Even the "classes" we simulate are just a function object.

You probably already noticed that our function A has a special property called prototype. This special property works with the JavaScript new operator. The reference to the prototype object is copied to the internal [[Prototype]] property of the new instance. For example, when you do var a1 = new A(), JavaScript (after creating the object in memory and before running function A() with this defined to it) sets a1.[[Prototype]] = A.prototype. When you then access properties of the instance, JavaScript first checks whether they exist on that object directly, and if not, it looks in [[Prototype]]. This means that all the stuff you define in prototype is effectively shared by all instances, and you can even later change parts of prototype and have the changes appear in all existing instances, if you wanted to.

If, in the example above, you do var a1 = new A(); var a2 = new A(); then a1.doSomething would actually refer to Object.getPrototypeOf(a1).doSomething, which is the same as the A.prototype.doSomething you defined, i.e. Object.getPrototypeOf(a1).doSomething == Object.getPrototypeOf(a2).doSomething == A.prototype.doSomething.

In short, prototype is for types, while Object.getPrototypeOf() is the same for instances.

[[Prototype]] is looked at recursively, i.e. a1.doSomething, Object.getPrototypeOf(a1).doSomething, Object.getPrototypeOf(Object.getPrototypeOf(a1)).doSomething etc., until it's found or Object.getPrototypeOf returns null.

So, when you call

var o = new Foo();

JavaScript actually just does

var o = new Object();
o.[[Prototype]] = Foo.prototype;
Foo.call(o);

(or something like that) and when you later do

o.someProp;

it checks whether o has a property someProp. If not, it checks Object.getPrototypeOf(o).someProp, and if that doesn't exist it checks Object.getPrototypeOf(Object.getPrototypeOf(o)).someProp, and so on.

Висновки

It is essential to understand the prototypal inheritance model before writing complex code that makes use of it. Also, be aware of the length of the prototype chains in your code and break them up if necessary to avoid possible performance problems. Further, the native prototypes should never be extended unless it is for the sake of compatibility with newer JavaScript features.

Мітки документа й учасники

Зробили внесок у цю сторінку: baronkoko, ihor91, IgorRusso, gumanista, Syarol, AndriySand, vlisivka
Востаннє оновлена: baronkoko,