典型的继承——写作

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

例子一:

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


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


rabbit.__proto__ = animal


rabbit.eat()

例二:

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时,它在 speedy中找不到 found属性,因此它爬升到原型并在那里更改它。这就是为什么 food.length对两只仓鼠来说是相等的,换句话说,它们有相同的胃。

由此我了解到,当编写并添加一个不存在的新属性时,解释器将沿着原型链向上,直到找到该属性,然后对其进行更改。

但在例1中,会发生其他情况:
我们运行 rabbit.eat,它改变 rabbit.full。没有找到 full属性,所以它应该沿着原型链向上(对象? ?)我不知道这里发生了什么。在这个示例中,创建并更改了 rabbit的属性 full,而在第一个示例中,由于找不到该属性,该属性向原型链上移。

我很困惑,不明白为什么会这样。

17517 次浏览

原型是为对象的每个实例实例化的 没有

Hamster.prototype.food = []

仓鼠的每个实例都将共享该数组

如果您需要(在本例中您确实需要)为每只仓鼠分别收集食物的实例,那么您需要在实例上创建属性。例如:

function Hamster() {
this.food = [];
}

要回答关于示例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

Property/member name是特定于实例的,对于 bob 和 ben 是不同的

成员 walk是 Person.model 的一部分,并且对所有实例共享,bob 和 ben 是 Person 的实例,因此它们共享 walk 成员(bob.walk = = ben.walk)。

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

因为 walk ()不能直接在 bob 上找到,所以 JavaScript 将在 Person.Prototype 中查找它,因为这是 bob 的构造函数。如果在那里找不到它,它会查看 Object.Prototype。这就是所谓的原型链。继承的原型部分是通过延长这个链来完成的; 例如 bob = > Employee.model = > Person.Prototype = > Object.model (稍后将详细介绍继承)。

即使 bob、 ben 和所有其他创建的 Person 实例共享 walk,该函数在每个实例中的行为也会有所不同,因为在 walk 函数中它使用了 thisthis的值将是调用对象; 现在我们假设它是当前实例,因此对于 bob.walk(),“ this”将是 bob。(更多关于“ this”和调用对象的内容稍后再说)。

如果本在等红灯,而鲍勃在绿灯处,那么你会对本和鲍勃都调用 walk () ,很明显,本和鲍勃会发生不同的事情。

当我们做类似于 ben.walk=22的动作时,会出现跟踪成员,即使 Bob 和 Ben 共享 walk,从 任务的22到 Ben.walk 不会影响 Bob 和 Ben。这是因为该语句将直接在 ben 上创建一个名为 walk的成员,并为其赋值22。将有两个不同的 walk 成员: ben.walk 和 Person.Prototype.walk。

当请求 bob.walk 时,您将得到 Person.Prototype.walk 函数,因为在 bob 上找不到 walk。然而,请求 ben.walk 会得到值22,因为成员 walk 是在 ben 上创建的,而且因为 JavaScript 发现 walk 是在 ben 上创建的,所以它不会在 Person.雏形中查找。

当使用带有2个参数的 Object.create 时,Object.defeProperty 或 Object.defeProperties 隐藏的工作方式有所不同。

更多关于原型

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

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

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

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成员。这就是构造函数派上用场的地方。它用于创建实例特定的成员。还可以向它传递参数以设置这些实例特定成员的值。

下面的代码展示了另一种实现构造函数的方法,语法不同,但思想是一样的:

  1. 定义一个对象,其成员在许多情况下都是相同的(person 是 bob 和 ben 的蓝图,可以是 jilly、 marie、 clair...)
  2. 定义特定于实例的成员,这些成员对于实例应该是唯一的(bob 和 ben)。
  3. 创建一个运行步骤2中代码的实例。

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

在这段代码中,我已经从原型和食物中删除了名称,因为无论如何,在创建实例时,您最有可能立即隐藏这个名称。Name 现在是具有构造函数中设置的默认值的实例特定成员。因为食物成员也是从原型移动到实例特定成员,所以在将食物添加到 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);//=[]

您可能会遇到类似的模式,它们在帮助创建对象和定义对象时更加健壮。

遗产

下面的代码演示如何继承。这些任务基本上与之前的代码相同,只是有一些额外的内容

  1. 定义对象的实例特定成员(函数 Hamster 和 RussionMini)。
  2. 设置继承的原型部分(RussionMini.model = Object.create (Hamster.model))
  3. 定义可以在实例之间共享的成员
  4. 创建一个实例,运行步骤1中的代码,对于继承的对象,也运行 Parent 代码(Hamster.application (this,reference) ;)

使用一种有些人称之为“经典继承”的模式。如果您对语法感到困惑,我很乐意解释更多或提供不同的模式。

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

Create 设置继承的原型部分

下面是关于 Object.create的文档,它基本上返回第二个参数(在 polyfil 中不支持) ,第一个参数作为返回对象的原型。

如果没有给出第二个参数,它将返回一个带有第一个参数的空对象作为返回对象的原型(返回对象的原型链中使用的第一个对象)。

有些人会把 RussionMini 的原型设置成一个仓鼠的实例(RussionMini.model = new Hamster ())。这是不可取的,因为即使它实现了同样的功能(RussionMini.Protot.model 的原型是 Hamster.model) ,它也会将 Hamster 实例成员设置为 RussionMini.Protot.model 的成员。因此 RussionMini.Prototype.food 将会存在,但是是一个共享成员(还记得在“关于原型的更多信息”中的 Bob 和 Ben 吗?).在创建 RussionMini 时,食物成员将被隐藏起来,因为仓鼠代码是用 Hamster.apply(this,arguments);运行的,然后运行 this.food = [],但是任何仓鼠成员仍然是 RussionMini.model 的成员。

另一个原因可能是,要创建仓鼠,需要对传递的参数进行大量复杂的计算,这些参数可能还不可用,同样,您可以传递虚拟参数,但这可能会不必要地使代码复杂化。

扩展和重写父函数

有时 children需要扩展 parent函数。

你想让“孩子”做一些额外的事情。当 RussionMini 可以调用仓鼠代码做一些事情,然后做一些额外的事情,你不需要复制和粘贴仓鼠代码到 RussionMini。

在下面的例子中,我们假设一只仓鼠每小时可以跑3公里,而一辆俄罗斯迷你车只能跑一半的速度。我们可以在 RussionMini 中硬编码3/2,但是如果这个值发生变化,我们的代码中有多个地方需要改变。下面是我们如何使用 Hamster.model 来获得父级(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。可能有一些模式会像 Java 一样给你 super的优势。

我看到的大多数模式要么在继承级别超过2级(Child = > Parent = > GrandParent)时打破,要么通过 关闭实现 super 使用更多资源。

要重写 Parent (= Hamster)方法,您可以执行相同的操作,但不要执行 Hamster.Prototype.ParentMethod.call (this,... 。

这个,构造函数

构造函数属性包含在 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 必须实现可移动性。在 JavaScript 中,我们可以定义 Mobileand 在这里定义实现,Cat 可以覆盖、扩展它或者我们它的默认实现。

对于 Moveble,我们有实例特定的成员(如 location)。并且我们有非实例特定的成员(如函数 move ())。在创建实例时,将通过调用 mxIns (由 Mixin helper 函数添加)来设置实例特定成员。Prototype 成员将使用混合助手函数从 Movable.model 在 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

这个变量实际上指的是调用对象,它指的是函数之前的对象。

为了澄清,请参阅以下代码:

theInvokingObject.thefunction();

引用错误对象的实例通常是在附加事件侦听器、回调或超时和间隔时。在接下来的两行代码中,我们 pass函数,我们不调用它。传递函数 is: someObject.aFunction并调用它 is: someObject.aFunction()this值并不指向函数声明的对象,而是指向 invokes声明函数的对象。

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

为了使 this在上述情况下引用 some Object,你可以直接传递一个 了结而不是函数:

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 没有私有修饰符。

我同意以下几点: http://blog.millermedeiros.com/a-case-against-private-variables-and-functions-in-javascript/和我个人没有使用过它们。

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

您可以通过 关闭实现私有成员,但是实例特定的私有成员只能被原型上没有的函数访问。

不以闭包的形式实现 private 会泄漏实现,并使您或用户能够扩展代码以使用不属于公共 API 的成员。这可能是好事也可能是坏事。

它很好,因为它使您和其他人能够轻松地模仿某些成员进行测试。它让其他人有机会轻松地改进(修补)您的代码,但这也是不好的,因为不能保证您的代码的下一个版本具有相同的实现和/或私有成员。

通过使用闭包,你不会给别人选择的机会,而通过使用变数命名原则和文档,你可以给别人选择的机会。这并不是 JavaScript 特有的,在其他语言中,您可以决定不使用私有成员,因为您相信其他人知道他们在做什么,并给予他们选择按照他们想要的方式(带有风险)。

如果您仍然坚持使用 private,那么 跟随模式可能会有所帮助。