原型继承-撰写[重复]
- 2024-11-02 21:00:00
- admin 原创
- 33
问题描述:
所以我有这两个例子,来自 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
两只仓鼠的 相等,换句话说,它们的胃是相同的。
从这里我理解,当编写并添加一个不存在的新属性时,解释器将沿着原型链向上查找,直到找到该属性,然后更改它。
但在示例 1 中,发生了其他事情:
我们运行rabbit.eat
,这会更改rabbit.full
。full
属性无处可寻,因此它应该沿着原型链向上移动(到对象??),好吧,我不确定这里发生了什么。 在此示例中,full
的属性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 创建实例时,必须使用 new 关键字:
var bob = new Person("Bob");console.log(bob.name);//=Bob
var ben = new Person("Ben");console.log(ben.name);//=Ben
属性/成员name
是特定于实例的,对于 bob 和 ben 来说,它是不同的
该成员walk
是 Person.prototype 的一部分,并由所有实例共享。bob 和 ben 是 Person 的实例,因此它们共享 walk 成员(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
。 的值this
将是调用对象;现在我们假设它是当前实例,因此bob.walk()
“this”将是 bob。(稍后将详细介绍“this”和调用对象)。
如果本在等红灯,而鲍勃在等绿灯;那么您将对本和鲍勃都调用 walk(),显然本和鲍勃会发生不同的事情。
当我们执行类似 的操作时,会发生成员隐藏ben.walk=22
,即使 bob 和 ben 共享对 ben.walk 的walk
赋值22 ,也不会影响 bob.walk。这是因为该语句将创建一个walk
直接在 ben 上调用的成员,并为其赋值 22。将有 2 个不同的 walk 成员:ben.walk 和 Person.prototype.walk。
当请求 bob.walk 时,您将获得 Person.prototype.walk 函数,因为walk
在 bob 上找不到。但是,请求 ben.walk 将获得值 22,因为成员 walk 已在 ben 上创建,并且由于 JavaScript 在 ben 上找到了 walk,因此它不会在 Person.prototype 中查找。
当使用带有 2 个参数的 Object.create 时,Object.defineProperty 或 Object.defineProperties 阴影的工作方式略有不同。有关更多信息,请点击此处。
有关原型的更多信息
一个对象可以通过使用原型从另一个对象继承。您可以使用 将任何对象的原型设置为任何其他对象Object.create
。在构造函数介绍中,我们已经看到,如果在对象上找不到成员,则 JavaScript 将在原型链中查找它。
在前面的部分中,我们已经看到,来自实例原型(ben.walk)的成员的重新分配将掩盖该成员(在 ben 上创建 walk 而不是更改 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 共享 person 的成员。只有一个 person,它被设置为 bob 和 ben 的原型(person 用作原型链中的第一个对象,用于查找实例中不存在的请求成员)。上面的代码的问题是 bob 和 ben 应该有自己的food
成员。这就是构造函数的作用所在。它用于创建特定于实例的成员。您还可以将参数传递给它以设置这些特定于实例的成员的值。
下面的代码展示了实现构造函数的另一种方法,语法不同,但思想是相同的:
定义一个具有对于许多实例相同的成员的对象(人是鲍勃和本的蓝图,可以是吉利、玛丽、克莱尔……)
定义对于实例(bob 和 ben)应唯一的实例特定成员。
创建一个运行步骤2中代码的实例。
使用构造函数,您将在下面的代码中的步骤 2 中设置原型,我们在步骤 3 中设置原型。
在此代码中,我从原型中删除了 name 以及 food,因为无论如何,在创建实例时,您很可能会立即将其隐藏。Name 现在是实例特定成员,其默认值设置在构造函数中。由于 food 成员也从原型移动到了实例特定成员,因此在将 food 添加到 ben 时不会影响 bob.food。
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'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 时,food 成员将被遮蔽,因为 Hamster 代码会随之运行Hamster.apply(this,arguments);
,this.food = []
但任何 Hamster 成员仍将是 RussionMini.prototype 的成员。
另一个原因可能是,要创建 Hamster,需要对传递的参数进行大量复杂的计算,而这些参数可能尚未可用,同样,您可以传递虚拟参数,但这可能会不必要地使您的代码复杂化。
扩展和覆盖父函数
有时children
需要扩展parent
功能。
您希望“子代”(=RussionMini)做一些额外的事情。当 RussionMini 可以调用 Hamster 代码来做一些事情,然后做一些额外的事情时,您无需将 Hamster 代码复制并粘贴到 RussionMini。
在以下示例中,我们假设 Hamster 每小时可以跑 3 公里,而 Russion mini 的跑速只有其一半。我们可以在 RussionMini 中硬编码 3/2,但如果此值发生变化,则代码中有多个地方需要更改。以下是我们使用 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.
super
缺点是您需要对 Hamster.prototype 进行硬编码。可能存在一些模式,可为您提供与 Java 中相同的优势。
我见过的大多数模式在继承级别超过 2 级(子级 => 父级 => 祖级)时会中断,或者通过闭包实现超级继承会使用更多的资源。
要覆盖 Parent (=Hamster) 方法,您可以执行相同的操作,但不要执行 Hamster.prototype.parentMethod.call(this,....
this.构造函数
构造函数属性由 JavaScript 包含在原型中,您可以更改它,但它应该指向构造函数。因此Hamster.prototype.constructor
应该指向 Hamster。
如果设置了继承的原型部分后,您应该让它再次指向正确的函数。
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,而是 Cat 可以移动。在基于类的语言中,Cat 必须实现 Movable。在 JavaScript 中,我们可以定义 Movable 并在此处定义实现,Cat 可以覆盖、扩展它或使用它的默认实现。
对于 Movable,我们有实例特定的成员(如location
)。我们也有非实例特定的成员(如函数 move())。实例特定的成员将在创建实例时通过调用 mxIns(由 mixin 辅助函数添加)来设置。原型成员将使用 mixin 辅助函数从 Movable.prototype 逐一复制到 Cat.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(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 变量
在所有示例代码中,您将看到this
引用当前实例。
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,您可以传递闭包而不是直接传递函数:
setTimeout(function(){someObject.aFuncton();},100);
somebutton.onclick = function(){someObject.aFunction();};
我喜欢定义返回原型上闭包函数的函数,以便对闭包范围内包含的变量进行精细控制。
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 ( 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 没有 private 修饰符。
我同意以下内容:http://blog.millermedeiros.com/a-case-against-private-variables-and-functions-in-javascript/,但我个人没有使用过它们。
_aPrivate
您可以通过命名成员或将所有私有变量放在名为 的对象变量中来向其他程序员表明某个成员是私有的_
。
您可以通过闭包实现私有成员,但实例特定的私有成员只能由原型上没有的函数访问。
不将私有函数作为闭包来实现会泄露实现,并使您或扩展您代码的用户能够使用不属于您的公共 API 的成员。这既有好处也有坏处。
它的优点在于它能让你和其他人轻松地模拟某些成员以进行测试。它让其他人有机会轻松改进(修补)你的代码,但缺点在于它无法保证你的代码的下一个版本具有相同的实现和/或私有成员。
通过使用闭包,您不会给其他人选择权,而通过使用命名约定和文档,您可以给其他人选择权。这并非 JavaScript 所特有的,在其他语言中,您可以决定不使用私有成员,因为您相信其他人知道他们在做什么,并让他们选择按照自己的意愿行事(存在风险)。
如果你仍然坚持使用 private,那么下面的模式可能会有所帮助。它没有实现 private,但实现了 protected。
解决方案 2:
原型不会为对象的每个实例进行实例化。
Hamster.prototype.food = []
Hamster 的每个实例都将共享该数组
如果您需要(在本例中确实需要)为每只 Hamster 单独设置食物集合实例,则需要在实例上创建属性。例如:
function Hamster() {
this.food = [];
}
回答关于示例 1 的问题,如果在原型链中的任何地方都找不到该属性,它会在目标对象上创建该属性。
- 2024年20款好用的项目管理软件推荐,项目管理提效的20个工具和技巧
- 2024年开源项目管理软件有哪些?推荐5款好用的项目管理工具
- 项目管理软件有哪些?推荐7款超好用的项目管理工具
- 项目管理软件哪个最好用?盘点推荐5款好用的项目管理工具
- 项目管理软件有哪些最好用?推荐6款好用的项目管理工具
- 项目管理软件有哪些,盘点推荐国内外超好用的7款项目管理工具
- 2024项目管理软件排行榜(10类常用的项目管理工具全推荐)
- 项目管理软件排行榜:2024年项目经理必备5款开源项目管理软件汇总
- 2024年常用的项目管理软件有哪些?推荐这10款国内外好用的项目管理工具
- 项目管理必备:盘点2024年13款好用的项目管理软件