[[Инженер JavaScript — 4]]

Какво представлява връзката [[Prototype]]?

Всеки обект в JavaScript има свойство [[Prototype]] или __proto__ в него.
Целта на свойството е да свърже обекта obj с друг обект xyz (obj.[[Prototype]] === xyz), така че че когато дадено свойство не е намерено в obj, то се търси в обекта [[Prototype]] на obj, Тъй като obj.[[Prototype]] също е обект, възможно е той да може да се свърже с друг обект, използвайки негов [[Прототип]]

Следователно това, което наистина се случва, е, че имаме верига от обекти, всички от които са свързани чрез [[Prototype]] връзка. когато свойство не бъде намерено в поискания обект, JavaScript ще продължи да търси във веригата [[Prototype]].

Нека си поиграем с веригата [[Prototype]], за да разберем основите на това как работи достъпът до свойствата на обекта, как работи алгоритъмът [[Get]] и [[Set]].

const firstObj = {
    a: 100
};

const secondObj = {
    b: 200
};

const thirdObj = {
    c: 300
};

Object.setPrototypeOf(thirdObj, null);
Object.setPrototypeOf(secondObj, thirdObj);
Object.setPrototypeOf(firstObj, secondObj);

console.log("Chain Set Up Manually");

Когато веригата е настроена, изглежда като

console.log(firstObj.a);    // 100
console.log(firstObj.b);    // 200
console.log(firstObj.c);    // 300

console.log(secondObj.a);    // undefined
console.log(secondObj.b);    // 200
console.log(secondObj.c);    // 300

console.log(thirdObj.a);    // undefined
console.log(thirdObj.b);    // undefined
console.log(thirdObj.c);    // 300
property a is directly available on firstObj
property b searched on firstObj, not found, continue search in [[Prototype]]
property b searched on secondObj, found with a value 200
... so on.
The takeaway from the example is
expression like firstObj.b does not simply mean property b of firstObj, in fact it is determined with [[Get]] algorithm in JavaScript which is based on chain of objects connected from one object to another through [[Prototype]] link.

Тъй като JavaScript обектите са динамична колекция от свойства, ни е позволено да добавяме/изтриваме/променяме/дефинираме свойства на обекта във всеки един момент.

Нека демонстрираме какво се случва, когато се опитаме да напишем свойство на JavaScript обекти, отново ще обсъдим подробно идеята за тези алгоритми.

const firstObj = {
    a: 100
};

const secondObj = {
    b: 200
};

const thirdObj = {
    c: 300
};

Object.setPrototypeOf(thirdObj, null);
Object.setPrototypeOf(secondObj, thirdObj);
Object.setPrototypeOf(firstObj, secondObj);

console.log("Chain Set Up Manually");

console.log(firstObj.b);  // (*)
firstObj.b = 500;
console.log(firstObj);
console.log(secondObj);

console.log(secondObj.c);
secondObj.c = 330;
console.log(secondObj);
console.log(thirdObj);

// firstObj.b tries to lookup b in firstObj, not found
// then b is looked up in secondObj, return 200
//firstObj.b = 500; 
// should JavaScript modify the value of property b present in chain,
// or should it add a property b to firstObj,
// JavaScript in the above code, added a property to firstObj,
// it did not change the value of the property present in the chain.
// [[Set]] algorithm is followed in instructions like firstObj.b = 500;

След като въведохме [[Get]] и [[Set]] до известна степен, нека сега да разгледаме алгоритмите

[[Get]] алгоритъм:

Input: object obj, property p
Output: the value determined for the property
// how obj.p , obj.fun(...args) is executed in JavaScript?
Let me explain the idea of [[Get]] mechanism,
the property is searched starting from object searchObj = obj
   does searchObj contain the property p, 
   if yes we know the object containing the property 
   otherwise we continue searching in [[Prototype]] of the object being searched. searchObj = searchObj.[[Prototype]]
If we have exhausted searching the entire chain and the property is not found, then we return undefined. [Case 1]
Else
   Let x be the first object which contains the property p in the chain of obj.
  If p is a getter descriptor property,
        call the getter function with this as obj.
         return the return value of getter function [ Case 2]
  If p is a function and is called implicitly,
        execute the function p with this as obj
        return the return value of calling p [ Case 3]
  Otherwise
       return the value of property p [ Case 4]

Разгледайте веригата [[Prototype]] по-долу, за да разберете механизма [[Get]].

a, x, y са свойства на дескриптор на данни, дефинирани на obj
distance е свойство на дескриптор на достъп за получаване, дефинирано на obj.[[Прототип]]
p е свойство на дескриптор на данни, дефинирано на obj.[[Prototype]]
b е свойство на дескриптор на данни, дефинирано на obj.[[Prototype]].[ [Прототип]]
fn е регулярна функция на дескриптор на данни, дефинирана на obj.[[Прототип]].[[Прототип]]

// setting the chain as per the image
const thirdObj = Object.create(null);
thirdObj.b = 1000;
thirdObj.fn = function () {
    console.log("this is ",this);
    console.log(this.a);        // [[Get]] used
    console.log(this.x);        // [[Get]] used
    console.log(this.y);        // [[Get]] used
    console.log(this.distance); // [[Get]] used
    console.log(this.p);        // [[Get]] used
    console.log(this.b);        // [[Get]] used
    
};

const secondObj = Object.create(thirdObj);
secondObj.p = 5000;
Object.defineProperty(secondObj, 'distance', {
    get: function () {
        console.log("Getter called with this", this);
        // realise that this.x below is again searched using [[Get]],
        // starting from this
        return Math.sqrt(this.x * this.x + this.y * this.y);    
    },
    enumerable: true,
    configurable: true
});

const obj = Object.create(secondObj);
obj.a = 20;
obj.x = 500;
obj.y = 1000;


// setup done
// Play with [[Get]]
console.log("Not found in the chain",obj.z);
console.log("found data descriptor on obj ", obj.a);
console.log("found data descriptor on obj ",obj.x);
console.log("found data descriptor on obj ",obj.y);
console.log("found data descriptor on obj.[[Prototype]] ",obj.p);
console.log("Called getter descriptor in chain ", obj.distance); // this is obj, implicit binding

// so overall, 
// [[Get]] used for searching the property 
//and implicit binding to determine this


console.log("found data descriptor on obj.[[Prototype]].[[Prototype]] ", obj.b);
console.log("Calling a function using [[Get]]");
obj.fn();

// Spend enough time on the algorithm and 
// notice carefully that every object access to read a value
// uses [[Get]] mechanism.

Алгоритъмът [[Set]]:

Нека обсъдим идеята зад алгоритъма [[Set]]
Инструкции във формата obj.x = 500; се изпълняват с помощта на алгоритъма [[Set]].

[[Set]] алгоритъм:

// how obj.p = val is performed in JavaScript
Inputs: object obj, property p, value val
Output: setting the property using [[Set]]
Let me explain the idea of [[Set]] mechanism,
searchObj = obj
First the property p is searched on searchObj, 
if found we know the first occurence of p in prototype chain
else we search in the next object in chain, searchObj = searchObj.[[Prototype]]
if no occurence found in the chain,
 add a data descriptor property p with value val on obj. [ Case 1]
else:
  Let x be the object containing first occurence of property p in the chain of obj
  if property p is defined with writable: false on object x as the descriptor. then obj will respect the restriction and [[Set]] will fail with error in strict mode, will fail silently in dev mode [ Case 2]
  
else if property p is defined with a setter accessor descriptor regular function
     then the setter function is executed with this as obj [ Case 3]
else the property p is a data descriptor on object x,
    instead of modifying the data descriptor value in chain,
    define a property p with value val on object obj [Called as shadowing]
 [ Case 4]

Note that [Case 1] and [Case 4] end up defining the property value on obj directly, most people expect such behaviour 100% of the time
but it is true only in 2 out of 4 case, so 50% of the time.
Practically though, most of the times, the first occurence in a chain is neither writable: false, nor setter, so the expected behaviour occurs.

Разгледайте алгоритъма [[Set]] с пример:

addX е свойство на дескриптор на данни, дефинирано на thirdObj като обикновена регулярна функция.
fixedVal е свойство на дескриптор на данни, дефинирано на thirdObj с writable:false
Свойствата на дескриптора на данните firstName, lastName са дефинирани във secondObj.
Свойството fullName е дефинирано с двойка дескриптори на достъп, get и set
firstName, lastName свойствата на дескриптора на данни са дефинирани на firstObj.

'use strict';

const thirdObj = Object.create(null);
Object.defineProperty(thirdObj, 'fixedVal', {
   writable: false,
   value: 2222
});

thirdObj.addX = function (xVal) {
    console.log("this is ",this);
    this.x = xVal;
    
};

const secondObj = Object.create(thirdObj);
secondObj.firstName = "Tushar";
secondObj.lastName = "Kashikar";

Object.defineProperty(secondObj, 'fullName', {
    get: function () {
        console.log("Getter called with this ", this);
        return `${this.firstName} ${this.lastName}`; // [[Get]] used for this.firstName, this.lastName
    },
    set: function(val) {
        console.log("Setter called with this ", this);
        const [fName, lName] = val.split(" ");
        this.firstName = fName; // [[Set]] used 
        this.lastName = lName; // [[Set]] used
    },
    enumerable: true,
    configurable: true
});

const obj = Object.create(secondObj);
obj.firstName = "Prince"; // [[Set]] algorithm, shadowing
obj.lastName = "Jha"; // [[Set]] algorithm, shadowing

// Play with [[Set]]
console.log(obj.fullName); // [[Get]] used, this is obj
obj.fullName = "Ganesh Mogekar"; // [[Set]] used, this is obj, implicit binding
console.log("obj after setter call",obj);
console.log(obj.fullName);  // [[Get]] used, this is obj, implicit binding

obj.addX(7000); // called with [[Get]] and implicit this binding
console.log("Object obj after addX call ", obj);
console.log("property added through [[Set]] since not found on chain ", obj.x);

console.log("Second Obj ",secondObj);
console.log(secondObj.fullName); // [[Get]], this is secondObj
secondObj.fullName = "Aditya Yadav"; // [[Set]], this is secondObj
console.log("After calling setter on secondObj ", secondObj); 
obj.fixedVal = 20; // throws an Error since writable: false

//Spend enough time on the algorithm 
// and notice carefully that every object access to 
// write a value uses [[Set]] mechanism.

Обобщение на прототипното наследяване:

Начинът, по който даден обект се свързва към верига и изпълнява операциите за четене и запис ([[Get]] и [[Set]] механизми), използвайки веригата от обекти, които са свързани чрез връзката [[Prototype]], е известен като прототипно наследство. Този механизъм за наследяване е напълно различен от механизма за наследяване, базиран на клас, който може да сте виждали в OOP.

Механизмът за прототипно наследяване като цяло има много интуитивно значение, че всички обекти са от един и същи тип (тип „обект“ в JavaScript) и могат директно да наследяват от всеки друг обект, от който искат да наследяват. Така че самият обект може да действа като прототип ( [[Prototype]] в терминологията на JavaScript) за всеки друг обект, докато самият той може да се променя динамично и може също да наследява от други обекти.

Пример за динамична природа на обектите в JavaScript и нейните последици:

Също така осъзнайте, че всеки един обект, независимо към кой обект е свързан, динамично изпълнява алгоритмите [[Get]] и [[Set]] всеки път, когато обектът е достъпен и по този начин може да бъде динамично променяни чрез добавяне на свойства, изтриване на свойства, актуализиране на свойства ..и т.н.

const obj = {
  a: 500
};

const secondObj = {
  b: 2000
};

Object.setPrototypeOf(obj, secondObj);

// obj --> secondObj --> Object.prototype --> null

console.log("Through [[Get]] obj.x ", obj.x); // undefined // ...(a)
console.log("Through [[Get]] obj.b ", obj.b); // 2000
secondObj.x = 500; 

// [[Set]] algorithm will end up adding a new property to secondObj
console.log("Now secondObj ", secondObj);
console.log("Through [[Get]] obj.x ", obj.x); // 500 // ...(b)

Фактът, че изходът на ред (a) и ред (b) е различен, потвърждава, че всяка промяна във secondObj наистина ще бъде отразена, когато следващия път obj се опита да използва алгоритъма [[Get]] или алгоритъма [[Set]]. По този начин JavaScript обектите са напълно динамични.

Връзката на наследяване между обектите също е динамична и може да бъде променена само чрез промяна на [[Prototype]] на обекта.

Клопка с алгоритъма [[Get]] и [[Set]]:

Лесно е да се разберат погрешно идеите за това как работи достъпът до свойствата на обекта в JavaScript. Например, ако смятате, че редът от код по-долу е просто самозадаване и следователно ефективно не прави нищо, вашето предположение е погрешно.

obj.x = obj.x;

Горният ред първо изпълнява алгоритъма [[Get]] върху обекта obj по отношение на името на свойството x. Върнатата стойност на алгоритъма [[Get]] ще се използва за извикване на алгоритъма [[Set]] на обекта obj за името на свойството x.
С други думи, горният ред код може да се мисли за както следва

const value = obj.x;
obj.x = value;

Причината, поради която инструкцията не е просто самоприсвояване, е, че обектите на JavaScript трябва да спазват прототипния модел на наследяване и следователно свойството x може да не е собствено свойство на обекта obj, но може да бъде наследено свойство.

const secondObj = {
   x: 500
}

const firstObj = {};

Object.setPrototypeOf(firstObj, secondObj);

console.log(firstObj.x); // [[Get]], inherited data descriptor property from secondObj

console.log("firstObj before ", firstObj);
console.log("secondObj before ", secondObj);

firstObj.x = firstObj.x;

console.log("firstObj after ", firstObj);
console.log("secondObj after ", secondObj);

В горния пример firstObj наследява чрез своя [[Prototype]] от secondObj. Следователно алгоритъмът [[Get]] връща стойността на свойството x на дескриптора на данни на secondObj.
Но когато се изпълни алгоритъмът [[Set]], свойството x вече присъства на secondObj като свойство на дескриптор на данни с writable: true като първото появяване във веригата [[Prototype]] на firstObj, така че ще се появи засенчване и следователно свойство на дескриптор на данни x всъщност ще бъде дефинирано на firstObj.

В обобщение, добра идея е внимателно да осъзнаете, че тези инструкции се изпълняват по отношение на алгоритмите [[Get]] и [[Set]].