原型继承 - 写作 [英] Prototypical inheritance - writing up

查看:107
本文介绍了原型继承 - 写作的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

所以我有这两个例子,来自javascript.info:



示例1:

  var animal = {
eat:function(){
alert(我满了)
this.full = true
}
}

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

rabbit .__ proto__ = animal

rabbit.eat()

示例2:

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

//创建两只快速而懒惰的仓鼠,然后喂第一只
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 亲perty在 speedy 中,所以它爬上原型并在那里进行更改。这就是为什么 food.length 对于两只仓鼠来说都是相同的,换句话说,他们有相同的胃。


从此我明白,当编写并添加一个不存在的新属性,解释器将上升到原型链,直到找到属性,然后更改它。



但是在示例中1其他事情发生:

我们运行 rabbit.eat ,这会改变 rabbit.full 完全属性无处可寻,所以应该将原型链上升到(对象??),好吧,我不知道这里发生了什么。在此示例中,创建并更改了 rabbit 的属性完整,而在第一个示例中,它上升了原型链因为它找不到属性。



我很困惑,看不清楚为什么会这样。

解决方案

构造函数功能介绍



如果构造函数是,则可以使用函数作为构造函数来创建对象命名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

属性/成员名称是特定于实例的,对于bob和ben来说是不同的



成员 walk 是Person.prototype的一部分,并且对所有实例共享bob和ben是Person的实例,所以他们分享步行成员(bob.walk === ben.walk)。

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

因为在bob上找不到walk(),所以JavaScript会在Person.prototype中找到它因为这是bob的构造函数。如果在那里找不到它,它将在Object.prototype上查找。这被称为原型链。继承的原型部分是通过延长这个链来完成的;例如bob => Employee.prototype => Person.prototype => Object.prototype(稍后继承更多)。



即使bob,ben和所有其他创建的Person实例共享walk每个实例的函数行为都不同,因为在walk函数中它使用 this 这个的值将是调用对象;现在让我们说这是当前的实例,所以对于 bob.walk()这个将是bob。 (更多关于this和稍后调用的对象)。



如果ben等待红灯而且bob处于绿灯状态;然后你会在ben和bob上调用walk(),显然ben和bob会发生不同的事情。



当我们做<$ c $之类的事情时会发生阴影成员c> ben.walk = 22 ,即使bob和ben分享步行 22的作业到ben.walk不会影响bob.walk。这是因为该语句将直接在ben上创建一个名为 walk 的成员,并为其赋值22.将有2个不同的walk成员:ben.walk和Person.prototype .walk。



当你要求bob.walk时,你将获得Person.prototype.walk函数,因为 walk 在bob上找不到。要求ben.walk然后会得到值22,因为成员步行已经在ben上创建,因为JavaScript发现在Ben上行走它不会在Person.prototype中查找。



当使用带有2个参数的Object.create时,Object.defineProperty或Object.defineProperties阴影效果有点不同。更多信息此处



有关原型的更多信息



对象可以通过使用原型从另一个对象继承。您可以使用 Object.create 设置任何其他对象的任何对象的原型。在构造函数函数介绍中我们已经看到,如果在对象上找不到成员,那么JavaScript将在prototpe链中查找它。



在上一部分我们已经看到重新分配来自实例原型(ben.walk)的成员将影响该成员(在ben上创建walk而不是更改Person .prototype.walk)。



如果我们不重新分配但改变成员怎么办? Mutating是(例如)更改Object的子属性或调用将更改对象值的函数。例如:

  var o = []; 
var a = o;
a.push(11); //变异a,这将改变o
a [1] = 22; //变异a,这将改变o

以下代码通过改变成员来演示原型成员和实例成员之间的区别。

  var person = {
name:default,// immutable所以可以用作默认值
sayName:function(){
console.log(你好,我是+ this.name;
},
food:[] //不可变,应该是特定于实例的
//不适合作为原型成员
};
var ben = Object.create(person);
ben.name =Ben;
var bob = Object.create(person);
console.log(bob.name); // =默认,设置ben.name阴影成员
//所以bob.name实际上是person.name
ben.food.push( 汉堡包);
console.log(bob.food); // = [Hamburger],改变
//原型上的共享成员会影响所有实例,因为它会更改person.food
console。 log(person.food); // = [Hamburger]

上面的代码显示ben和鲍勃分享会员。只有一个人,它被设置为bob和ben的原型(人被用作原型链中的第一个对象,用于查找实例上不存在的请求成员)。上面代码的问题是bob和ben应该有自己的 food 成员。这是构造函数的用武之地。它用于创建特定于实例的成员。你也可以传递参数来设置这些实例特定成员的值。



下一个代码显示了实现构造函数的另一种方法,语法不同但想法是相同:


  1. 定义一个对象,其成员对于许多实例都是相同的(人是bob和ben的蓝图,可以是对于jilly,marie,clair ...)

  2. 定义实例特定成员,这些成员对于实例(bob和ben)应该是唯一的。

  3. 创建一个在第2步中运行代码的实例。

使用构造函数,您将在以下代码中的步骤2中设置原型,我们设置了步骤3中的原型。



在此代码中,我已经从原型和食物中删除了名称,因为无论如何,在创建实例时,您很可能会立即将其隐藏起来。 Name现在是一个特定于实例的成员,在构造函数中设置了默认值。 Becaus食品成员也从原型转移到实例特定成员,在向Ben添加食物时不会影响bob.food。

  var person = {
sayName:function(){
console.log(你好,我是+ this.name);
},
//在创建
//实例时需要运行构造函数以确保实例具有
//实例特定成员
构造函数:function (姓名){
this.name = name || 默认;
this.food = [];
返回此;
}
};
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); // = []

你可能会来跨越类似的模式,这些模式更强大,可以帮助创建对象和定义对象。



继承



以下代码显示了如何继承。这些任务与之前的代码基本相同,只需要额外的一些


  1. 定义对象的特定于实例的成员(函数Hamster和RussionMini)。

  2. 设置继承的原型部分(RussionMini.prototype = Object.create(Hamster.prototype))

  3. 定义可在实例之间共享的成员。(Hamster.prototype和RussionMini.prototype)

  4. 在步骤1中创建一个运行代码的实例,对于继承它们的对象也运行父代码(Hamster.apply(this,参数);)

使用模式可以称之为经典继承。如果您对语法感到困惑,我会很乐意解释更多或提供不同的模式。

  function Hamster(){
this.food = [];
}
函数RussionMini(){
//Hamster.apply(this,arguments)执行Hamster体中代码
//的每一行,其中this的值是
//要创建的RussionMini(一次用于迷你,一次用于贝蒂)
Hamster.apply(this,arguments);
}
//设置RussionMini的原型
RussionMini.prototype = Object.create(Hamster.prototype);
//设置内置成员,称为构造函数,将
//指向右边的函数(上一行指向Hamster)
RussionMini.prototype.constructor = RussionMini;
mini = new RussionMini();
//this.food(实例特定于迷你)
//来自使用Hamster.apply运行Hamster代码
//(this,arguments);
mini.food.push(迷你食物);
//添加特定于Hamster的行为仍然是
//由RussionMini继承//因为RussionMini.prototype的原型
//是Hamster.prototype
Hamster.prototype.runWheel = function (){console.log(我正在跑步)};
mini.runWheel(); // =我正在运行

对象.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)运行; 反过来运行这个。 food = [] 但任何Hamster成员仍然是RussionMini.prototype的成员。



另一个原因可能是创建一个仓鼠很多对于可能尚未提供的传递参数,需要进行复杂的计算,同样可以传递伪参数,但可能会使代码复杂化。



扩展并覆盖父函数



有时需要扩展 parent 函数。



您希望'child'(= RussionMini)做额外的事情。当RussionMini可以调用Hamster代码执行某些操作然后执行额外操作时,您无需将Hamster代码复制并粘贴到RussionMini。



在以下示例中,我们假设仓鼠可以每小时跑3公里,但Russion迷你只能跑一半。我们可以在RussionMini中硬编码3/2,但如果要更改此值,我们在代码中有多个位置需要更改。以下是我们如何使用Hamster.prototype获取父(仓鼠)速度。

  var Hamster = function(name){
if(name === undefined){
throw new Error(Name not not undefined);
}
this.name = name;
}
Hamster.prototype.getSpeed = function(){
return 3;
}
Hamster.prototype.run = function(){
// Russionmini不需要将此函数实现为
//它将完成与它的完全相同仓鼠
//但是Russionmini确实需要实现getSpeed,因为
//将不会像Hamster一样返回(参见后面的代码)
return我正在运行+
this.getSpeed()+km一小时。;
}

var RussionMini = function(name){
Hamster.apply(this,arguments);
}
//在设置RussionMini原型之前调用它
RussionMini.prototype = Object.create(Hamster.prototype);
RussionMini.prototype.constructor = RussionMini;

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

var betty = new RussionMini(Betty);
console.log(betty.run()); // =我以每小时1.5公里的速度运行。

缺点是你硬编码Hamster.prototype。可能有一些模式可以像Java一样为您提供 super 的优势。



我看到的大部分模式要么在继承级别超过2级时出现问题(Child => Parent => GrandParent),要么通过实现super来使用更多资源通过关闭



要覆盖Parent(= Hamster)方法,您可以执行相同操作,但不要执行Hamster.prototype.parentMethod.call(this,....



this.constructor



构造函数属性由JavaScript包含在原型中,您可以更改它但它应该指向构造函数。所以 Hamster.prototype.constructor 应该指向Hamster。



如果在设置继承的原型部分之后你应该让它再次指向正确的功能。

  var Hamster = function(){}; 
var RussionMinni = function(){
//重新使用父构造函数(我知道那里没有)
Hamster.apply(this,arguments);
};
RussionMinni.prototype = Object.create(Hamster.prototype);
console.log(RussionMinni.prototype.constructor === Hamster); // = true
RussionMinni.prototype.haveBaby = function(){
return this this.constructor();
};
var betty = new RussionMinni();
var littleBetty = betty.haveBaby();
console.log(littleBetty instanceof RussionMinni); // false
console.log(littleBetty instanceof Hamster); // true
//修复构造函数
RussionMinni.prototype.constructor = RussionMinni;
//现在再次生孩子
var littleBetty = betty.haveBaby();
console.log(littleBetty instanceof RussionMinni); // true
console.log(littleBetty instanceof Hamster); // true

多重继承与混合输入



有些事情最好不要继承,如果猫可以移动然后猫不应该继承Movable。猫不是可移动的,而是猫可以移动。在基于类的语言中,Cat必须实现Movable。在JavaScript中我们可以在这里定义Movable并定义实现,Cat可以覆盖,扩展它或者我们的默认实现。



对于Movable,我们有特定于实例的成员(如位置)。我们的成员不是特定于实例的(如函数move())。在创建实例时,将通过调用mxIns(由mixin辅助函数添加)来设置特定于实例的成员。使用mixin辅助函数从Movable.prototype中逐个复制原型成员。

  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(构造函数,混合){
var thing
,cProto = constructor.prototype
,mProto = mix.prototype;
//无扩展,如果多个原型
//具有相同名称的成员,则使用
//最后
为(mProto中的东西){
if (Object.hasOwnProperty.call(mProto,thing)){
cProto [thing] = mProto [thing];
}
}
//实例初始值
cProto.mixIns = cProto.mixIns || [];
cProto.mixIns.push(mix);
};
var Movable = function(args){
args = args || {};
//演示如何使用truthy设置默认值
//不检查validaty
this.location = args.location;
this.isStuck =(args.isStuck === true); //默认为false
this.canMove =(args.canMove!== false); //默认为true
//速度默认为4
this.speed =(args.speed === 0)?0:(args.speed || 4);
};
Movable.prototype.move = function(){
console.log('我正在移动,默认实现。');
};
var Animal = function(args){
args = args || {};
this.name = args.name || 事情;
};
var Cat = function(args){
var i,len;
Animal.call(args);
//如果一个对象可以让其他人混合在
//那么这需要初始化
//实例成员
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();

以上是一个简单的实现,用最后混合的任何混合替换相同的命名函数。 / p>

此变量



在所有示例代码中,您将看到 this 引用当前实例。



此变量实际上是指调用对象,它指的是来自的对象在函数之前。



澄清看下面的代码:

  theInvokingObject.thefunction(); 

这会引用错误对象的实例通常是在附加事件监听器,回调或超时时间隔。在接下来的两行代码中,我们传递函数,我们不会调用它。传递函数是: someObject.aFunction 并且调用它是: someObject.aFunction()这个值不是引用函数声明的对象,而是引用调用它的对象

  setTimeout(someObject.aFuncton,100); //这在aFunction中是窗口
somebutton.onclick = someObject.aFunction; //这个在aFunction是somebutton

要使这个在上述情况下,请参考someObject,您可以传递关闭而不是直接函数:

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

我喜欢定义为闭包可以很好地控制关闭范围。

  var Hamster = function(name){
var largeVariable = new Array(100000).join(Hello World);
//如果我做
// setInterval(function(){this.checkSleep();},100);
//然后largeVariable将在闭包范围内以及
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); //名为Betty的仓鼠实例
hamsterInstance.checkSleep();
};
}
};
Hamster.prototype.checkSleep = function(){
//假设这是Hamster实例
};

var betty = new Hamster(Betty);

传递(构造函数)参数



当Child调用Parent( Hamster.apply(this,arguments); )时,我们假设Hamster以相同的顺序使用与RussionMini相同的参数。对于调用其他函数的函数,我通常使用另一种方式来传递参数。



我通常将一个对象传递给一个函数并让该函数改变它需要的任何东西(设置默认值) ,然后该函数将它传递给另一个将执行相同操作的函数,依此类推。下面是一个示例:

  //帮助函数抛出错误
函数thowError(消息){
抛出新错误(消息)
};
var Hamster = function(args){
//确保args是你得到的错误
//对你有意义而不是args未定义
args = args || {};
//类型的默认值:
this.type = args.type || 默认类型;
//名称不是可选的,非常简单的truthy检查f
this.name = args.name || thowError(args.name不是可选的);
};
var RussionMini = function(args){
//确保args是你得到的错误
//对你有意义而不是args未定义
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(); //错误:args.name不是可选的

在许多情况下,这种在函数链中传递参数的方法很有用。当你正在处理能够计算某些东西的代码时,你想要重新计算某种货币的总和,你可能需要改变很多函数来传递货币的价值。你可以扩大货币价值范围(甚至可以扩展到全球范围,如 window.currency ='USD'),但这是解决问题的一种不好的方法。



通过传递一个对象,只要它在函数链中可用,就可以将货币添加到 args ,并在需要时随意改变/使用它而不更改另一个函数(显式必须在函数调用中传递它)。



私有变量



JavaScript没有私有修饰符。



我同意以下内容: http://blog.millermedeiros.com/a-case-against-private-variables-and-functions-in-javascript/ 并且个人没有使用它们。



您可以通过命名成员向其他程序员表明成员是私有的 _aPrivate 或将所有私有变量放在一个名为<的对象变量中code> _ 。



您可以通过闭包但实例特定的私有成员只能由不在原型上的函数访问。



没有实现私有,因为闭包会泄漏实现,使您或用户能够扩展您的代码,以使用不属于您的公共API的成员。这可能既好又坏。



这很好,因为它可以让你和其他人模拟某些成员进行轻松测试。它让其他人有机会轻松改进(修补)您的代码,但这也很糟糕,因为无法保证您的代码的下一版本具有相同的实现和/或私有成员。



通过使用闭包,您不会给别人一个选择,并使用命名约定和您所做的文档。这不是特定于JavaScript的,在其他语言中,您可以决定不使用私有成员,因为您相信其他人知道他们正在做什么,并让他们可以选择按照自己的意愿行事(涉及风险)。



如果您仍然坚持私有,那么以下模式可能有所帮助。它不实现私有,但实现受保护。


So I have these 2 examples, from javascript.info:

Example 1:

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

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

rabbit.__proto__ = animal 

rabbit.eat() 

Example 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 (!??)

Start from Example 2: when the code reaches speedy.found, it finds no found property in speedy, and so it climbs up to the prototype and changes it there. That's why food.length is equal for both hamsters, in other words they have the same stomach.

From this I understand, that when writing up and adding a new property which doesn't exist, the interpreter will go up the prototype chain until it finds the property, and THEN change it.

BUT in Example 1 something else happens:
we run rabbit.eat, which changes rabbit.full. full property is nowhere to be found, so it should go up the prototype chain to (to object??), and well, I'm not sure what happens here. In this example the property full of rabbit is created and changed, while in the first example it goes up the prototype chain because it cannot find the property.

I'm confused and cannot see why this happens.

解决方案

Constructor function introduction

You can use a function as a constructor to create objects, if the constructor function is named Person then the object(s) created with that constructor are instances of Person.

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

Person is the constructor function. When you create an instance using Person you have to use the new keyword:

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

The property/member name is instance specific, it's different for bob and ben

The member walk is part of Person.prototype and is shared for all instances bob and ben are instances of Person so they share the walk member (bob.walk===ben.walk).

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

Because walk() could not be found on bob directly JavaScript will look for it in the Person.prototype as this is the constructor of bob. If it can't be found there it'll look on Object.prototype. This is called the prototype chain. The prototype part of inheritance is done by lengthening this chain; for example bob => Employee.prototype => Person.prototype => Object.prototype (more on inheritance later).

Even though bob, ben and all other created Person instances share walk the function will behave differently per instance because in the walk function it uses this. The value of this will be the invoking object; for now let's say it's the current instance so for bob.walk() "this" will be bob. (more on "this" and the invoking object later).

If ben was waiting for a red light and and bob was at a green light; then you'll invoke walk() on both ben and bob obviously something different would happen to ben and bob.

Shadowing members happens when we do something like ben.walk=22, even though bob and ben share walk the assignment of 22 to ben.walk will not affect bob.walk. This is because that statement will create a member called walk on ben directly and assign it a value of 22. There will be 2 different walk members: ben.walk and Person.prototype.walk.

When asking for bob.walk you'll get the Person.prototype.walk function because walk could not be found on bob. Asking for ben.walk however will get you the value 22 because the member walk has been created on ben and since JavaScript found walk on ben it will not look in the Person.prototype.

When using Object.create with 2 arguments, Object.defineProperty or Object.defineProperties shadowing works a bit different. More info on that here.

More about prototype

An object can inherit from another object through the use of prototype. You can set the prototype of any object with any other object using Object.create. In the constructor function introduction we have seen that if a member can't be found on the object then JavaScript will look in the prototpe chain for it.

In previous part we have seen that re assignment of members that come from an instance's prototype (ben.walk) will shadow that member (create walk on ben rather than changing Person.prototype.walk).

What if we don't re assign but mutate the member? Mutating is (for example) changing sub properties of an Object or invoking functions that will change the object's value. For example:

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

The following code demonstrates the difference between prototype members and instance members by mutating members.

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"]

The code above shows that ben and bob share members from person. There is only one person, it is set as bob's and ben's prototype (person is used as the first object in the prototype chain to look up requested members that don't exist on the instance). The problem with the above code is that bob and ben should have their own food member. This is where the constructor function comes in. It is used to create instance specific members. You could also pass arguments to it to set values of these instance specific members.

The next code shows another way to implement the constructor function, syntax is different but the idea is the same:

  1. Define an object that has members that will be same for many instances (person is a blueprint for bob and ben and can be for jilly, marie, clair ...)
  2. Define instance specific members that should be unique for instances (bob and ben).
  3. Create an instance running the code in step 2.

With constructor functions you'll set the prototype in step 2 in the following code we set the prototype in step 3.

In this code I have removed name from prototype as well as food because you are most likely going to shadow this almost immediately when creating an instance anyway. Name is now an instance specific member with a default value set in the constructor function. Becaus the food member is also moved from prototype to instance specific member it will not affect bob.food when adding food to 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);//=[]

You may come across similar patterns that are more robust to help with object creation and object definition.

Inheritance

The following code shows how to inherit. The tasks are basically the same as in code before with a little extra

  1. Define instance specific members of an object (functions Hamster and RussionMini).
  2. Set the prototype part of inheritance (RussionMini.prototype = Object.create(Hamster.prototype))
  3. Define members that can be shared among instances.(Hamster.prototype and RussionMini.prototype)
  4. Create an instance running the code in step 1 and for objects that inherit have them run the Parent code as well (Hamster.apply(this,arguments);)

Using a pattern some would call "classical inheritance". If you are confused by the syntax I'll be happy to explain more or provide different patterns.

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's 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's food");
//adding behavior specific to Hamster that will still be
//  inherited by RussionMini because RussionMini.prototype's prototype
//  is Hamster.prototype
Hamster.prototype.runWheel=function(){console.log("I'm running")};
mini.runWheel();//=I'm running

Object.create to set prototype part of inheritance

Here is the documentation about Object.create, it basically returns the second argument (not supported in the polyfil) with the first argument as the returned object's prototype.

If no second argument was given it'll return an empty object with first argument to be used as the returned object's prototype (the first object to be used in the returned object's prototype chain).

Some would set the prototype of RussionMini to an instance of Hamster (RussionMini.prototype = new Hamster()). This is not desirable because even though it accomplishes the same (RussionMini.prototype's prototype is Hamster.prototype) it also sets Hamster instance members as members of RussionMini.prototype. So RussionMini.prototype.food will exist but is a shared member (remember bob and ben in "More about prototype"?). The food member will be shadowed when creating a RussionMini because Hamster code is run with Hamster.apply(this,arguments); that in turn runs this.food = [] but any Hamster members will still be members of RussionMini.prototype.

Another reason could be that to create a Hamster a lot of complicated calculations need be done on passed arguments that may be not available yet, again you could pass in dummy arguments but it could unnecessarily complicate your code.

Extending and overriding Parent functions

Sometimes children need to extend parent functions.

You want the 'child' (=RussionMini) to do something extra. When RussionMini can call the Hamster code to do something and then do something extra you don't need to copy and paste Hamster code to RussionMini.

In the following example we assume that a Hamster can run 3km an hour but a Russion mini can only run half as fast. We can hard code 3/2 in RussionMini but if this value were to change we have multiple places in code where it needs changing. Here is how we use Hamster.prototype to get the parent (Hamster) speed.

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.

The disadvantage is that you hard code Hamster.prototype. There may be patterns that will give you the advantage of super as in Java.

Most of the patterns I've seen will either break when inheritance level is more than 2 levels (Child => Parent => GrandParent) or use more resources by implementing super through closures.

To override a Parent (=Hamster) method you do the same but don't do Hamster.prototype.parentMethod.call(this,....

this.constructor

The constructor property is included in the prototype by JavaScript, you can change it but it should point to the constructor function. So Hamster.prototype.constructor should point to Hamster.

If after setting prototype part of inheritance you should have it point to the right function again.

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

"Multiple inheritance" with mix ins

Some things are better not to be inherited, if a Cat can move and then a Cat should not inherit from Movable. A Cat is not a Movable but rather a Cat can move. In a class based language Cat would have to implement Movable. In JavaScript we can define Movable and define implementation here, Cat can either override, extend it or us it's default implementation.

For Movable we have instance specific members (like location). And we have members that are not instance specific (like the function move()). Instance specific members will be set by calling mxIns (added by mixin helper function) when creating an instance. Prototype members will be copied one by one on Cat.prototype from Movable.prototype using the mixin helper function.

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();

The above is a simple implementation that replaces same named functions with whatever mix in is mixed in last.

The this variable

In all the example code you'll see this referring to the current instance.

The this variable actually refers to the invoking object, it refers to the object that came before the function.

To clarify see the following code:

theInvokingObject.thefunction();

The instances where this would refer to the wrong object are usually when attaching event listeners, callbacks or timeouts and intervals. In the next 2 lines of code we pass the function, we don't invoke it. Passing the function is: someObject.aFunction and invoking it is: someObject.aFunction(). The this value does not refer to the object the function was declared on but on the object that invokes it.

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

To make this in the above cases refer to someObject you can pass a closure instead of the function directly:

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

I like to define functions that return a function for closures on the prototype to have fine control over the variables that are included in the 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");

Passing (constructor) arguments

When Child calls a Parent (Hamster.apply(this,arguments);) we assume that Hamster uses the same arguments as RussionMini in the same order. For functions that call other functions I usually use another way to pass arguments.

I usually pass one object to a function and have that function mutate whatever it needs (set defaults), then that function will pass it to another function that will do the same and so on and so on. Here is an example:

//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

This way of passing arguments in a function chain is useful in many cases. When you're working on code that would calculate a total of something and later you'd like to re factor the total of that something to be in a certain currency you may have to change a lot of functions to pass the value for currency. You could up scope a currency value (even to global like window.currency='USD') but that's a bad way to solve it.

With passing an object you could add currency to args whenever it's available in the function chain and mutate/use it whenever you need it without changing the other functions (explicitly have to pass it in the function calls).

Private variables

JavaScript doesn't have a private modifier.

I agree with the following: http://blog.millermedeiros.com/a-case-against-private-variables-and-functions-in-javascript/ and personally have not used them.

You can indicate to other programmers a member is meant to be private by naming it _aPrivate or putting all the private variables in an object variable called _.

You can implement private members through closures but instance specific private members can only be accessed by functions that are not on the prototype.

Not implementing privates as closures would leak implementation and enable you or users extending your code to use members that are not part of your public API. This can be both good and bad.

It's good because it enables you and others to mock certain members for testing easily. It gives others a chance to easily improve (patch) your code but this is also bad because there is no guarantee that a next version of your code has the same implementation and or private members.

By using closures you do not give others a choice and by using the naming convention with documentation you do. This is not specific to JavaScript, in other languages you can decide not to use private members as you trust others to know what they are doing and give them the choice to do as they want (with risks involved).

If you still insist on privates then the following pattern may help. It doesn't implement private though but implements protected.

这篇关于原型继承 - 写作的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆