Прототипическое наследование - запись

Итак, у меня есть эти 2 примера, из javascript.info:

Пример 1:

var animal = {
  eat: function() {
    alert( "I'm full" )
    this.full = true
  }
}

var rabbit = {
  jump: function() { /* something */ }
}

rabbit.__proto__ = animal 

rabbit.eat() 

Пример 2:

function Hamster() {  }
Hamster.prototype = {
  food: [],
  found: function(something) {
    this.food.push(something)
  }
}

// Create two speedy and lazy hamsters, then feed the first one
speedy = new Hamster()
lazy = new Hamster()

speedy.found("apple")
speedy.found("orange")

alert(speedy.food.length) // 2
alert(lazy.food.length) // 2 (!??)

Начните с примера 2: когда код достигнет speedy.found, он не найдет свойство found в speedy, и поэтому он поднимается до прототипа и меняет его. Вот почему food.length равно для обоих хомяков, другими словами, у них один желудок. Из этого я понимаю, что при написании и добавлении нового свойства, которого не существует, интерпретатор будет продвигаться по цепочке прототипов до тех пор, пока не найдет свойство, а THEN изменит его.

НО в примере 1 произойдет что-то еще:
мы запускаем rabbit.eat, который меняет rabbit.full. full свойство нигде не найдено, поэтому оно должно подниматься вверх по цепочке прототипов до (к объекту?), и хорошо, я не уверен, что здесь происходит. В этом примере свойство full of rabbit создано и изменено, в то время как в первом примере оно идет вверх по цепочке прототипов, потому что не может найти свойство.

Я смущен и не понимаю, почему это происходит.

Ответы

Ответ 1

Введение функции конструктора

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

var Person = function(name){
  this.name = name;
};
Person.prototype.walk=function(){
  this.step().step().step();
};
var bob = new Person("Bob");

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

var bob = new Person("Bob");console.log(bob.name);//=Bob
var ben = new Person("Ben");console.log(ben.name);//=Ben

Свойство/member name является специфичным для экземпляра, оно отличается для bob и ben

Член walk является частью Person.prototype и является общим для всех экземпляров. bob и ben являются экземплярами Person, поэтому они делят элемент walk (bob.walk === ben.walk).

bob.walk();ben.walk();

Поскольку функция walk() не может быть найдена на bob непосредственно, JavaScript будет искать ее в Person.prototype, поскольку это конструктор bob. Если он не может быть найден там, он будет искать Object.prototype. Это называется цепочкой прототипов. Прообразная часть наследования выполняется путем удлинения этой цепи; например bob = > Employee.prototype = > Person.prototype = > Object.prototype(подробнее о наследовании позже).

Несмотря на то, что bob, ben и все другие созданные экземпляры Person совместно используют функцию, она будет вести себя по-разному на один экземпляр, потому что в функции walk она использует this. Значение this будет вызывающим объектом; теперь скажем, что текущий экземпляр, поэтому для bob.walk() "this" будет bob. (подробнее о "this" и вызывающем объекте позже).

Если Бен ожидал красный свет, а боб был на зеленом свете; то вы будете вызывать walk() на обоих ben и bob, очевидно, что что-то другое произойдет с ben и bob.

Элементы Shadowing происходят, когда мы делаем что-то вроде ben.walk=22, хотя bob и ben share walk присвоение 22 to ben.walk не влияет на bob.walk. Это связано с тем, что этот оператор создаст элемент с именем walk on ben напрямую и назначит ему значение 22. Будет два разных элемента walk: ben.walk и Person.prototype.walk.

При запросе bob.walk вы получите функцию Person.prototype.walk, потому что walk не может быть найден на bob. Если вы запросите ben.walk, вы получите значение 22, потому что прогулка участника была создана на ben, и поскольку JavaScript нашел прогулку по ben, он не будет выглядеть в Person.prototype.

При использовании Object.create с двумя аргументами затенение Object.defineProperty или Object.defineProperties немного отличается. Подробнее об этом здесь.

Подробнее о прототипе

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

В предыдущей части мы увидели, что повторное назначение элементов, которые исходят из прототипа экземпляра (ben.walk), будет теневым элементом (создайте прогулку по ben, а не сменив Person.prototype.walk).

Что делать, если мы не назначаем, а мутируем элемент? Мутация - это (например) изменение вспомогательных свойств объекта или вызывающих функций, которые изменят значение объекта. Например:

var o = [];
var a = o;
a.push(11);//mutate a, this will change o
a[1]=22;//mutate a, this will change o

Следующий код демонстрирует разницу между членами прототипа и членами экземпляра, изменяя члены.

var person = {
  name:"default",//immutable so can be used as default
  sayName:function(){
    console.log("Hello, I am "+this.name);
  },
  food:[]//not immutable, should be instance specific
         //  not suitable as prototype member
};
var ben = Object.create(person);
ben.name = "Ben";
var bob = Object.create(person);
console.log(bob.name);//=default, setting ben.name shadowed the member
                      //  so bob.name is actually person.name
ben.food.push("Hamburger");
console.log(bob.food);//=["Hamburger"], mutating a shared member on the
// prototype affects all instances as it changes person.food
console.log(person.food);//=["Hamburger"]

В приведенном выше коде показано, что ben и bob разделяют членов от человека. Существует только один человек, он задан как прототип bob и ben (человек используется в качестве первого объекта в цепочке прототипов для поиска запрошенных членов, которых нет в экземпляре). Проблема с приведенным выше кодом заключается в том, что bob и ben должны иметь свой собственный член food. Здесь используется функция конструктора. Она используется для создания конкретных экземпляров экземпляра. Вы также можете передать ему аргументы, чтобы установить значения этих конкретных экземпляров.

Следующий код показывает другой способ реализации функции конструктора, синтаксис отличается, но идея одинаков:

  • Определите объект, у которого есть члены, которые будут одинаковыми для многих экземпляров (человек является планом для bob и ben и может быть для jilly, marie, clair...)
  • Определить конкретные экземпляры экземпляра, которые должны быть уникальными для экземпляров (bob и ben).
  • Создайте экземпляр, запускающий код на шаге 2.

С помощью функций конструктора вы установите прототип на шаге 2 в следующем коде, который мы установили на этапе 3.

В этом коде я удалил имя из прототипа, а также пищу, потому что вы, скорее всего, собираетесь его почти сразу же при создании экземпляра создать. Имя теперь является конкретным экземпляром элемента со значением по умолчанию, установленным в функции конструктора. Becaus, член пищи также перемещается из прототипа в конкретный конкретный член, он не будет влиять на bob.food при добавлении пищи в ben.

var person = {
  sayName:function(){
    console.log("Hello, I am "+this.name);
  },
  //need to run the constructor function when creating
  //  an instance to make sure the instance has
  //  instance specific members
  constructor:function(name){
    this.name = name || "default";
    this.food = [];
    return this;
  }
};
var ben = Object.create(person).constructor("Ben");
var bob = Object.create(person).constructor("Bob");
console.log(bob.name);//="Bob"
ben.food.push("Hamburger");
console.log(bob.food);//=[]

Вы можете столкнуться с похожими шаблонами, которые более надежны, чтобы помочь в создании объекта и определении объекта.

Наследование

Следующий код показывает, как наследовать. Задачи в основном такие же, как и в коде, с небольшим дополнительным

  • Определить конкретные элементы объекта (функции Hamster и RussionMini).
  • Задайте прототип части наследования (RussionMini.prototype = Object.create(Hamster.prototype))
  • Определите членов, которые могут быть разделены между экземплярами. (Hamster.prototype и RussionMini.prototype)
  • Создайте экземпляр, запускающий код на шаге 1, и для объектов, которые наследуют, запустили ли они также родительский код (Hamster.apply(this, arguments);)

С помощью шаблона некоторые вызовут "классическое наследование". Если вы смущены синтаксисом, я буду рад объяснить больше или предоставить разные шаблоны.

function Hamster(){
 this.food=[];
}
function RussionMini(){
  //Hamster.apply(this,arguments) executes every line of code
  //in the Hamster body where the value of "this" is
  //the to be created RussionMini (once for mini and once for betty)
  Hamster.apply(this,arguments);
}
//setting RussionMini prototype
RussionMini.prototype=Object.create(Hamster.prototype);
//setting the built in member called constructor to point
// to the right function (previous line has it point to Hamster)
RussionMini.prototype.constructor=RussionMini;
mini=new RussionMini();
//this.food (instance specic to mini)
//  comes from running the Hamster code
//  with Hamster.apply(this,arguments);
mini.food.push("mini food");
//adding behavior specific to Hamster that will still be
//  inherited by RussionMini because RussionMini.prototype prototype
//  is Hamster.prototype
Hamster.prototype.runWheel=function(){console.log("I'm running")};
mini.runWheel();//=I'm running

Object.create для установки прототипа части наследования

Вот документация о Object.create, она в основном возвращает второй аргумент (не поддерживается в polyfil) с первым аргументом как прототип возвращенного объекта.

Если второй аргумент не задан, он возвращает пустой объект с первым аргументом, который будет использоваться в качестве прототипа возвращаемого объекта (первый объект, который будет использоваться в цепочке прототипов возвращаемого объекта).

Некоторые установили прототип RussionMini на экземпляр Hamster (RussionMini.prototype = new Hamster()). Это нежелательно, поскольку, несмотря на то, что он выполняет то же самое (прототип RussionMini.prototype - Hamster.prototype), он также устанавливает членов экземпляра Hamster в качестве членов RussionMini.prototype. Таким образом, RussionMini.prototype.food будет существовать, но является общим членом (помните bob и ben в разделе "Больше о прототипе"?). Элемент питания будет затенен при создании RussionMini, поскольку код Hamster запускается с Hamster.apply(this,arguments);, который, в свою очередь, запускает this.food = [], но все члены Hamster все еще будут членами RussionMini.prototype.

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

Расширение и переопределение родительских функций

Иногда children необходимо расширить функции parent.

Вы хотите, чтобы "ребенок" (= RussionMini) сделал что-то дополнительное. Когда RussionMini может вызвать код Hamster, чтобы что-то сделать, а затем сделать что-то дополнительное, вам не нужно копировать и вставлять код Hamster в RussionMini.

В следующем примере мы предполагаем, что Хомяк может работать 3 км в час, но российский мини может работать только наполовину быстрее. Мы можем жестко кодировать 3/2 в RussionMini, но если это значение изменилось, у нас есть несколько мест в коде, где он нуждается в изменении. Вот как мы используем Hamster.prototype для получения родительской (Hamster) скорости.

var Hamster = function(name){
 if(name===undefined){
   throw new Error("Name cannot be undefined");
 }
 this.name=name;
}
Hamster.prototype.getSpeed=function(){
  return 3;
}
Hamster.prototype.run=function(){
  //Russionmini does not need to implement this function as
  //it will do exactly the same as it does for Hamster
  //But Russionmini does need to implement getSpeed as it
  //won't return the same as Hamster (see later in the code) 
  return "I am running at " + 
    this.getSpeed() + "km an hour.";
}

var RussionMini=function(name){
  Hamster.apply(this,arguments);
}
//call this before setting RussionMini prototypes
RussionMini.prototype = Object.create(Hamster.prototype);
RussionMini.prototype.constructor=RussionMini;

RussionMini.prototype.getSpeed=function(){
  return Hamster.prototype
    .getSpeed.call(this)/2;
}    

var betty=new RussionMini("Betty");
console.log(betty.run());//=I am running at 1.5km an hour.

Недостаток заключается в том, что вы производите жесткий код Hamster.prototype. Могут быть шаблоны, которые предоставят вам преимущество super, как в Java.

Большинство шаблонов, которые я видел, либо сломаются, когда уровень наследования больше двух уровней (Child = > Parent = > GrandParent), либо используйте больше ресурсов, реализуя супер через closures.

Чтобы переопределить метод Parent (= Hamster), вы делаете то же самое, но не делаете Hamster.prototype.parentMethod.call(this,....

this.constructor

Свойство конструктора включено в прототип JavaScript, вы можете его изменить, но он должен указывать на функцию конструктора. Поэтому Hamster.prototype.constructor должен указывать на Хомяка.

Если после установки прототипа части наследования вам нужно снова указать правильную функцию.

var Hamster = function(){};
var RussionMinni=function(){
   // re use Parent constructor (I know there is none there)
   Hamster.apply(this,arguments);
};
RussionMinni.prototype=Object.create(Hamster.prototype);
console.log(RussionMinni.prototype.constructor===Hamster);//=true
RussionMinni.prototype.haveBaby=function(){
  return new this.constructor();
};
var betty=new RussionMinni();
var littleBetty=betty.haveBaby();
console.log(littleBetty instanceof RussionMinni);//false
console.log(littleBetty instanceof Hamster);//true
//fix the constructor
RussionMinni.prototype.constructor=RussionMinni;
//now make a baby again
var littleBetty=betty.haveBaby();
console.log(littleBetty instanceof RussionMinni);//true
console.log(littleBetty instanceof Hamster);//true

"Множественное наследование" со смесями

Некоторые вещи лучше не наследоваться, если Cat может двигаться, а затем Cat не должен наследовать от Movable. Кошка не подвижна, а кошка может двигаться. В языке, основанном на классе, Cat должен будет выполнить Movable. В JavaScript мы можем определить Movable и определить реализацию здесь, Cat может либо переопределить, либо расширить его, либо выполнить его по умолчанию.

Для Movable у нас есть конкретные экземпляры (например, location). И у нас есть члены, которые не являются специфичными для экземпляра (например, функция move()). Конкретные члены экземпляра будут заданы вызовом mxIns (добавлен функцией helin mixin) при создании экземпляра. Элементы Prototype будут копироваться один за другим в Cat.prototype из Movable.prototype, используя вспомогательную функцию mixin.

var Mixin = function Mixin(args){
  if(this.mixIns){
    i=-1;len=this.mixIns.length;
    while(++i<len){
        this.mixIns[i].call(this,args);
      }
  }  
};
Mixin.mix = function(constructor, mix){
  var thing
  ,cProto=constructor.prototype
  ,mProto=mix.prototype;
  //no extending, if multiple prototypes
  // have members with the same name then use
  // the last
  for(thing in mProto){
    if(Object.hasOwnProperty.call(mProto, thing)){
      cProto[thing]=mProto[thing];
    }
  }
  //instance intialisers
  cProto.mixIns = cProto.mixIns || [];
  cProto.mixIns.push(mix);
};
var Movable = function(args){
  args=args || {};
  //demo how to set defaults with truthy
  // not checking validaty
  this.location=args.location;
  this.isStuck = (args.isStuck===true);//defaults to false
  this.canMove = (args.canMove!==false);//defaults to true
  //speed defaults to 4
  this.speed = (args.speed===0)?0:(args.speed || 4);
};
Movable.prototype.move=function(){
  console.log('I am moving, default implementation.');
};
var Animal = function(args){
  args = args || {};
  this.name = args.name || "thing";
};
var Cat = function(args){
  var i,len;
  Animal.call(args);
  //if an object can have others mixed in
  //  then this is needed to initialise 
  //  instance members
  Mixin.call(this,args);
};
Cat.prototype = Object.create(Animal.prototype);
Cat.prototype.constructor = Cat;
Mixin.mix(Cat,Movable);
var poochie = new Cat({
  name:"poochie",
  location: {x:0,y:22}
});
poochie.move();

Вышеприведенная простая реализация, которая заменяет одинаковые именованные функции любым смешиванием в последнем.

Эта переменная

Во всем примере кода вы увидите this, ссылаясь на текущий экземпляр.

Эта переменная на самом деле относится к вызывающему объекту, она относится к объекту, который пришел перед функцией.

Для уточнения см. следующий код:

theInvokingObject.thefunction();

В случаях, когда это относится к неправильному объекту, обычно при подключении прослушивателей событий, обратных вызовов или тайм-аутов и интервалов. В следующих двух строках кода мы pass функцию, мы ее не вызываем. Передача функции: someObject.aFunction и вызов: someObject.aFunction(). Значение this не относится к объекту, объявленному функцией, но к объекту, который invokes он.

setTimeout(someObject.aFuncton,100);//this in aFunction is window
somebutton.onclick = someObject.aFunction;//this in aFunction is somebutton

Чтобы сделать this в приведенных выше случаях ссылкой на someObject, вы можете передать closure вместо функции напрямую:

setTimeout(function(){someObject.aFuncton();},100);
somebutton.onclick = function(){someObject.aFunction();};

Мне нравится определять функции, возвращающие функцию для closures на прототипе, чтобы иметь прекрасный контроль над переменными, которые включены в closure scope.

var Hamster = function(name){
  var largeVariable = new Array(100000).join("Hello World");
  // if I do 
  // setInterval(function(){this.checkSleep();},100);
  // then largeVariable will be in the closure scope as well
  this.name=name
  setInterval(this.closures.checkSleep(this),1000);
};
Hamster.prototype.closures={
  checkSleep:function(hamsterInstance){
    return function(){
      console.log(typeof largeVariable);//undefined
      console.log(hamsterInstance);//instance of Hamster named Betty
      hamsterInstance.checkSleep();
    };
  }
};
Hamster.prototype.checkSleep=function(){
  //do stuff assuming this is the Hamster instance
};

var betty = new Hamster("Betty");

Передача (конструктор) аргументов

Когда Child вызывает родителя (Hamster.apply(this,arguments);), мы предполагаем, что Hamster использует те же аргументы, что и RussionMini в том же порядке. Для функций, которые вызывают другие функции, я обычно использую другой способ передачи аргументов.

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

//helper funciton to throw error
function thowError(message){
  throw new Error(message)
};
var Hamster = function(args){
  //make sure args is something so you get the errors
  //  that make sense to you instead of "args is undefined"
  args = args || {};
  //default value for type:
  this.type = args.type || "default type";
  //name is not optional, very simple truthy check f
  this.name = args.name || thowError("args.name is not optional");
};
var RussionMini = function(args){
  //make sure args is something so you get the errors
  //  that make sense to you instead of "args is undefined"
  args = args || {};
  args.type = "Russion Mini";
  Hamster.call(this,args);
};
var ben = new RussionMini({name:"Ben"});
console.log(ben);// Object { type="Russion Mini", name="Ben"}
var betty = new RussionMini();//Error: args.name is not optional

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

С передачей объекта вы можете добавить валюту в args всякий раз, когда она доступна в цепочке функций, и мутировать/использовать ее всякий раз, когда вам это нужно, без изменения других функций (явно нужно передать ее в вызове функций).

Частные переменные

JavaScript не имеет частного модификатора.

Я согласен со следующим: http://blog.millermedeiros.com/a-case-against-private-variables-and-functions-in-javascript/ и лично не использовал их.

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

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

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

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

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

Если вы все еще настаиваете на рядовых, то может помочь следующий шаблон. Он не реализует частные, но реализует защищенные.

Ответ 2

Прототипы НЕ, созданные для каждого экземпляра объекта.

Hamster.prototype.food = []

Каждый экземпляр Hamster будет делиться этим массивом

Если вам нужно (и вы делаете в этом случае) отдельные экземпляры коллекций продуктов для каждого хомяка, вам нужно создать свойство на экземпляре. Например:

function Hamster() {
  this.food = [];
}

Чтобы ответить на вопрос о примере 1, если он не найдет какое-либо свойство в цепочке прототипов, он создает свойство на целевом объекте.