构造函数介绍
您可以使用函数作为构造函数来创建对象,如果构造函数名为 Person,则使用该构造函数创建的对象是 Person 的实例。
var Person = function(name){
this.name = name;
};
Person.prototype.walk=function(){
this.step().step().step();
};
var bob = new Person("Bob");
人是构造函数。使用 Person 创建实例时,必须使用 new 关键字:
var bob = new Person("Bob");console.log(bob.name);//=Bob
var ben = new Person("Ben");console.log(ben.name);//=Ben
属性/成员是特定于实例的,对于 bob 和 ben 是不同的name
该成员是Person.prototype的一部分,并且对于所有实例,bob和ben都是Person的实例共享,因此它们共享walk成员(bob.walk===ben.walk)。walk
bob.walk();ben.walk();
因为 walk() 不能直接在 bob 上找到,JavaScript 会在 Person.prototype 中查找它,因为这是 bob 的构造函数。如果在那里找不到它,它会在Object.prototype上查看。这称为原型链。继承的原型部分是通过延长这个链来完成的;例如,bob => Employee.prototype => Person.prototype => Object.prototype(稍后将详细介绍继承)。
即使 bob、ben 和所有其他创建的 Person 实例共享 walk,该函数在每个实例的行为也会有所不同,因为在 walk 函数中它使用 。的值将是调用对象;现在,假设它是当前实例,因此对于“this”将是bob。(稍后将详细介绍“this”和调用对象)。this
this
bob.walk()
如果本在等待红灯,而鲍勃在绿灯;然后你会在ben和bob上调用walk(),显然ben和bob会发生不同的事情。
当我们做类似的事情时,就会发生影子成员,即使 bob 和 ben 共享 22 到 ben.walk 的分配也不会影响 bob.walk。这是因为该语句将创建一个直接调用 ben 的成员,并为其分配值 22。将有2个不同的步行成员:ben.walk和Person.prototype.walk。ben.walk=22
walk
walk
当要求bob.walk时,你会得到Person.prototype.walk函数,因为在bob上找不到。但是,询问ben.walk会得到值22,因为成员walk是在ben上创建的,并且由于JavaScript在ben上找到了walk,因此它不会在Person.prototype中查找。walk
当使用具有 2 个参数的 Object.create 时,Object.defineProperty 或 Object.defineProperties 阴影的工作方式略有不同。有关这方面的更多信息 请点击此处。.
更多关于原型
一个对象可以通过使用原型从另一个对象继承。您可以使用 设置任何对象的原型和任何其他对象。在构造函数介绍中,我们已经看到,如果在对象上找不到成员,那么JavaScript将在原型链中寻找它。Object.create
在上一部分中,我们已经看到,重新分配来自实例原型 (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 从个人共享成员。只有一个人,它被设置为 bob 和 ben 的原型(person 被用作原型链中的第一个对象,用于查找实例上不存在的请求成员)。上述代码的问题在于 bob 和 ben 应该有自己的成员。这就是构造函数的用武之地。它用于创建特定于实例的成员。您还可以向其传递参数以设置这些特定于实例的成员的值。food
接下来的代码显示了实现构造函数的另一种方法,语法不同,但想法是相同的:
- 定义一个对象,该对象的成员在很多情况下都是相同的(person是bob和ben的蓝图,可以是jilly,marie,clair...)
- 定义对于实例(bob 和 ben)应该是唯一的特定于实例的成员。
- 创建一个运行步骤 2 中代码的实例。
使用构造函数,您将在以下代码的步骤 2 中设置原型,我们在步骤 3 中设置原型。
在此代码中,我从原型和食物中删除了名称,因为无论如何,在创建实例时,您几乎都会立即隐藏它。Name 现在是实例特定的成员,在构造函数中设置了默认值。Becaus 食品成员也从原型移动到实例特定成员,在向 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时,食物成员将被隐藏,因为仓鼠代码是随之运行的,但任何仓鼠成员仍将是RussionMini.prototype的成员。Hamster.apply(this,arguments);
this.food = []
另一个原因可能是,要创建仓鼠,需要在可能尚不可用的传递参数上进行大量复杂的计算,同样,您可以传入虚拟参数,但它可能会不必要地使您的代码复杂化。
扩展和重写父函数
有时需要扩展功能。children
parent
你希望“孩子”(=RussionMini)做一些额外的事情。当RussionMini可以调用仓鼠代码来做某事,然后做一些额外的事情时,你不需要将仓鼠代码复制并粘贴到RussionMini。
在下面的示例中,我们假设仓鼠可以以每小时3公里的速度奔跑,但Russion mini只能跑一半的速度。我们可以在RussionMini中硬编码3/2,但是如果这个值要改变,我们在代码中有多个地方需要改变。以下是我们使用Hamster.prototype获得父级(仓鼠)速度的方法。
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.
缺点是你硬编码仓鼠原型。可能有一些模式可以给你带来Java中的优势。super
我见过的大多数模式要么在继承级别超过2个级别时(子级=>父级=>父级)时中断,要么通过闭包实现超级来使用更多资源。
要重写 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
“多重继承”与混音
有些东西最好不要被继承,如果一只猫可以移动,那么一只猫不应该从Movable继承。猫不是可移动的,而是猫可以移动的。在基于类的语言中,Cat 必须实现 Movable。在JavaScript中,我们可以在这里定义Movable并定义实现,Cat可以覆盖,扩展它或我们默认实现。
对于 Movable,我们有特定于实例的成员(如 )。而且我们有一些不是特定于实例的成员(如函数 move())。在创建实例时,将通过调用 mxIns(由 mixin 帮助程序函数添加)来设置特定于实例的成员。原型成员将使用 mixin 助手函数从 Movable.prototype 逐个复制到 Cat.prototype 上。location
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 变量实际上指的是调用对象,它指的是函数之前的对象。
若要清楚起见,请参阅以下代码:
theInvokingObject.thefunction();
这引用错误对象的情况通常是在附加事件侦听器,回调或超时和间隔时。在接下来的 2 行代码中,我们函数,我们不调用它。传递函数 is: 并调用它是: 。该值不是指声明函数的对象,而是引用它所声明的对象。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
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 以相同的顺序使用与 RussionMini 相同的参数。对于调用其他函数的函数,我通常使用另一种方式来传递参数。Hamster.apply(this,arguments);
我通常将一个对象传递给一个函数,并让该函数改变它需要的任何内容(设置默认值),然后该函数将它传递给另一个将执行相同操作的函数,依此类推。下面是一个示例:
//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
_
您可以通过闭包实现私有成员,但特定于实例的私有成员只能由不在原型上的函数访问。
不将私有实现为闭包会泄漏实现,并使你或扩展代码的用户能够使用不属于公共 API 的成员。这既可以是好的,也可以是坏的。
这很好,因为它使你和其他人能够轻松模拟某些成员进行测试。它让其他人有机会轻松改进(修补)您的代码,但这也是不好的,因为不能保证您的代码的下一个版本具有相同的实现和/或私有成员。
通过使用闭包,您不会给其他人一个选择,并且通过将命名约定用于您所做的文档。这并不是JavaScript所特有的,在其他语言中,你可以决定不使用私有成员,因为你相信其他人知道他们在做什么,并让他们选择按照自己的意愿去做(涉及风险)。
如果您仍然坚持使用私有,那么以下模式可能会有所帮助。它不实现私有,但实现受保护的。