Prototypical inheritance - writing up
所以我有这两个例子,来自javascript.info:
例1:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | var animal = { eat: function() { alert("I'm full" ) this.full = true } } var rabbit = { jump: function() { /* something */ } } rabbit.__proto__ = animal rabbit.eat() |
例2:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 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开始:当代码到达
据我所知,在编写和添加一个不存在的新属性时,解释器将上升到原型链,直到找到属性,然后更改它。
但是在示例1中还发生了其他事情:
我们运行
我很困惑,也看不出为什么会这样。
构造函数介绍
您可以使用函数作为构造函数来创建对象,如果构造函数名为Person,则使用该构造函数创建的对象是Person的实例。
1 2 3 4 5 6 7 | var Person = function(name){ this.name = name; }; Person.prototype.walk=function(){ this.step().step().step(); }; var bob = new Person("Bob"); |
Person是构造函数。使用Person创建实例时,必须使用new关键字:
1 2 | var bob = new Person("Bob");console.log(bob.name);//=Bob var ben = new Person("Ben");console.log(ben.name);//=Ben |
属性/成员
成员
1 | bob.walk();ben.walk(); |
因为在bob上找不到walk(),所以JavaScript会在Person.prototype中查找它,因为这是bob的构造函数。如果在那里找不到它,它将在Object.prototype上查找。这被称为原型链。继承的原型部分是通过延长这个链来完成的;例如bob => Employee.prototype => Person.prototype => Object.prototype(稍后继承更多内容)。
尽管bob,ben和所有其他创建的Person实例共享walk,但每个实例的函数行为都不同,因为在walk函数中它使用
如果本正在等待红灯而且鲍勃处于绿灯状态;然后你会在ben和bob上调用walk(),显然ben和bob会发生不同的事情。
当我们做像
当请求bob.walk时,你将得到Person.prototype.walk函数,因为在bob上找不到
当使用带有2个参数的Object.create时,Object.defineProperty或Object.defineProperties阴影的工作方式有点不同。关于这里的更多信息。
更多关于原型
对象可以通过使用原型从另一个对象继承。您可以使用
在前面的部分中,我们已经看到重新分配来自实例原型(ben.walk)的成员将影响该成员(在ben上创建walk而不是更改Person.prototype.walk)。
如果我们不重新分配但改变成员怎么办?变异是(例如)改变Object的子属性或调用将改变对象值的函数。例如:
1 2 3 4 | var o = []; var a = o; a.push(11);//mutate a, this will change o a[1]=22;//mutate a, this will change o |
以下代码通过变更成员来演示原型成员和实例成员之间的区别。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 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应该有自己的
下一个代码显示了实现构造函数的另一种方法,语法不同但想法是一样的:
使用构造函数,您将在以下代码中的步骤2中设置原型,我们在步骤3中设置原型。
在这段代码中,我已经从原型和食物中删除了名称,因为无论如何,在创建实例时,您很可能会立即隐藏它。 Name现在是一个特定于实例的成员,在构造函数中设置了默认值。 Becaus食品成员也从原型转移到实例特定成员,在向Ben添加食物时不会影响bob.food。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 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);//=[] |
您可能会遇到类似的模式,这些模式更强大,可以帮助创建对象和定义对象。
遗产
以下代码显示了如何继承。这些任务与之前的代码基本相同,但需要额外的一些
使用模式有些人会称之为"经典继承"。如果您对语法感到困惑,我会很乐意解释更多或提供不同的模式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | 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设置继承的原型部分
这是关于Object.create的文档,它基本上返回第二个参数(在polyfil中不支持),第一个参数作为返回对象的原型。
如果没有给出第二个参数,它将返回一个带有第一个参数的空对象,用作返回对象的原型(在返回对象的原型链中使用的第一个对象)。
有些人会将RussionMini的原型设置为Hamster的一个实例(RussionMini.prototype = new Hamster())。这是不可取的,因为即使它完成相同(RussionMini.prototype的原型是Hamster.prototype),它也将Hamster实例成员设置为RussionMini.prototype的成员。所以RussionMini.prototype.food将存在,但是是一个共享成员(请记住"更多关于原型"的bob和ben?)。在创建RussionMini时,食物成员将被遮蔽,因为Hamster代码以
另一个原因可能是创建一个Hamster需要对传递的参数进行许多复杂的计算,这些参数可能还没有,再次你可以传入伪参数,但它可能会不必要地使你的代码复杂化。
扩展和覆盖父函数
有时
你希望'child'(= RussionMini)做一些额外的事情。当RussionMini可以调用Hamster代码执行某些操作然后执行额外操作时,您无需将Hamster代码复制并粘贴到RussionMini。
在下面的例子中,我们假设一只仓鼠可以每小时跑3公里,但Russion迷你只能跑一半。我们可以在RussionMini中硬编码3/2,但如果要更改此值,我们在代码中需要更改多个位置。以下是我们如何使用Hamster.prototype获取父(仓鼠)速度。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | 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。可能有一些模式可以像Java一样为您提供
我看到的大多数模式要么在继承级别超过2级时出现问题(Child => Parent => GrandParent),要么通过实现超级闭包来使用更多资源。
要覆盖Parent(= Hamster)方法,您可以执行相同的操作,但不要执行Hamster.prototype.parentMethod.call(this,....
this.constructor
构造函数属性由JavaScript包含在原型中,您可以更改它,但它应该指向构造函数。所以
如果在设置继承的原型部分后,您应该再次指向正确的功能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | 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 |
混合ins的"多重继承"
有些事情最好不要继承,如果猫可以移动,然后猫不应该继承Movable。猫不是可移动的,而是猫可以移动。在基于类的语言中,Cat必须实现Movable。在JavaScript中我们可以在这里定义Movable并定义实现,Cat可以覆盖,扩展它或者我们它的默认实现。
对于Movable,我们有特定于实例的成员(如
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | 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变量实际上是指调用对象,它指的是函数之前的对象。
澄清一下,看下面的代码:
1 | theInvokingObject.thefunction(); |
通常在附加事件侦听器,回调或超时和间隔时,这将引用错误对象的实例。在接下来的两行代码中,我们
1 2 | setTimeout(someObject.aFuncton,100);//this in aFunction is window somebutton.onclick = someObject.aFunction;//this in aFunction is somebutton |
要在上述情况下使
1 2 | setTimeout(function(){someObject.aFuncton();},100); somebutton.onclick = function(){someObject.aFunction();}; |
我喜欢定义返回原型闭包函数的函数,以便对闭包范围中包含的变量进行精细控制。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | 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调用Parent(
我通常将一个对象传递给一个函数并让该函数改变它需要的任何东西(设置默认值),然后该函数将它传递给另一个将执行相同操作的函数,依此类推。这是一个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | //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 |
在许多情况下,这种在函数链中传递参数的方法很有用。当您正在处理可以计算某些内容的代码时,您希望重新计算某种货币的总和,您可能需要更改许多函数来传递货币的值。你可以扩大货币价值范围(甚至是
通过传递一个对象,只要它在函数链中可用,就可以将货币添加到
私有变量
JavaScript没有私有修饰符。
我同意以下内容:http://blog.millermedeiros.com/a-case-against-private-variables-and-functions-in-javascript/并且个人尚未使用它们。
您可以通过命名
您可以通过闭包实现私有成员,但实例特定的私有成员只能由不在原型上的函数访问。
不实现privates作为闭包会泄漏实现,并使您或用户能够扩展您的代码以使用不属于您的公共API的成员。这可能既好又坏。
它很好,因为它可以让你和其他人模拟某些成员进行轻松测试。它为其他人提供了轻松改进(修补)代码的机会,但这也很糟糕,因为无法保证下一版本的代码具有相同的实现和/或私有成员。
通过使用闭包,您不会给其他人一个选择,并使用命名约定和您所做的文档。这不是特定于JavaScript的,在其他语言中,您可以决定不使用私有成员,因为您相信其他人知道他们正在做什么并让他们选择按照自己的意愿行事(涉及风险)。
如果你仍然坚持私有,那么以下模式可能有所帮助。它不实现私有,但实现受保护。
好。
原型未针对对象的每个实例进行实例化。
1 | Hamster.prototype.food = [] |
Hamster的每个实例都将共享该数组
如果您需要(在这种情况下),每个Hamster的食物集合的单独实例,您需要在实例上创建属性。 例如:
1 2 3 | function Hamster() { this.food = []; } |
要回答关于示例1的问题,如果它没有在原型链中的任何位置找到该属性,则会在目标对象上创建该属性。