Ответ 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, на других языках вы можете не использовать частных членов, поскольку вы доверяете другим, чтобы знать, что они делают, и дают им возможность делать то, что они хотят (с учетом рисков).
Если вы все еще настаиваете на рядовых, то может помочь следующий шаблон. Он не реализует частные, но реализует защищенные.