137

所以我有这两个例子,来自 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.fullfull属性无处可寻,所以它应该沿着原型链上升到(对象??),好吧,我不确定这里会发生什么。在这个例子中, 的属性fullrabbit创建和更改,而在第一个例子中,它沿着原型链上升,因为它找不到属性。

我很困惑,不明白为什么会发生这种情况。

4

2 回答 2

180

构造函数介绍

您可以使用函数作为构造函数来创建对象,如果构造函数名为 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”和稍后的调用对象)。

如果本正在等待红灯,而鲍勃正在等待绿灯;然后你会在 ben 和 bob 上调用 walk() ,显然 ben 和 bob 会发生一些不同的事情。

当我们执行类似的操作时,会发生跟踪成员ben.walk=22,即使 bob 和 benwalk将22分配给 ben.walk 也不会影响 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 发现 walk on ben,它不会在 Person.prototype 中查找。

当使用带有 2 个参数的 Object.create 时,Object.defineProperty 或 Object.defineProperties 阴影的工作方式有点不同。更多信息在这里

更多关于原型

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

在前面的部分中,我们已经看到重新分配来自实例原型(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 共享成员。只有一个人,它被设置为 bob 和 ben 的原型(person 被用作原型链中的第一个对象,用于查找实例上不存在的请求成员)。上面代码的问题是 bob 和 ben 应该有自己的food成员。这就是构造函数的用武之地。它用于创建特定于实例的成员。您还可以将参数传递给它以设置这些实例特定成员的值。

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

  1. 定义一个对象,该对象的成员对于许多实例都是相同的(人是 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.prototype = Object.create(Hamster.prototype))
  3. 定义可以在实例之间共享的成员。(Hamster.prototype 和 RussionMini.prototype)
  4. 创建一个运行步骤 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 的原型设置为仓鼠实例 (RussionMini.prototype = new Hamster())。这是不可取的,因为即使它完成了相同的操作(RussionMini.prototype 的原型是 Hamster.prototype),它也将 Hamster 实例成员设置为 RussionMini.prototype 的成员。所以 RussionMini.prototype.food 将存在,但它是一个共享成员(还​​记得“更多关于原型”中的 bob 和 ben 吗?)。创建 RussionMini 时,食物成员将被遮蔽,因为仓鼠代码会Hamster.apply(this,arguments);依次运行,this.food = []但任何仓鼠成员仍将是 RussionMini.prototype 的成员。

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

扩展和覆盖父函数

有时children需要扩展parent功能。

您希望“孩子”(=RussionMini)做一些额外的事情。当 RussionMini 可以调用仓鼠代码做某事然后做一些额外的事情时,您不需要将仓鼠代码复制并粘贴到 RussionMini。

在下面的示例中,我们假设仓鼠每小时可以跑 3 公里,但俄罗斯迷你只能跑一半的速度。我们可以在 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.

缺点是您对 Hamster.prototype 进行了硬编码。可能有一些模式会给您带来与superJava 一样的优势。

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

要覆盖 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

带有混合的“多重继承”

有些东西最好不要继承,如果 Cat 可以移动,那么 Cat 不应该从 Movable 继承。猫不是可移动的,而是猫可以移动。在基于类的语言中,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 变量实际上是指调用对象,它指的是函数之前的对象。

要澄清,请参阅以下代码:

theInvokingObject.thefunction();

this 引用错误对象的实例通常是在附加事件侦听器、回调或超时和间隔时。在接下来的两行代码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 没有私有修饰符。

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

您可以通过命名成员_aPrivate或将所有私有变量放在名为_.

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

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

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

通过使用闭包,您不会给其他人选择,而是通过使用文档的命名约定。这不是 JavaScript 特有的,在其他语言中,您可以决定不使用私有成员,因为您相信其他人知道他们在做什么,并让他们选择做他们想做的事(涉及风险)。

如果您仍然坚持使用私人,那么以下模式可能会有所帮助。它虽然没有实现私有,但实现了受保护。

于 2013-04-17T15:08:37.240 回答
15

不会为对象的每个实例实例化原型。

Hamster.prototype.food = []

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

如果您需要(并且在这种情况下确实需要)为每个仓鼠提供单独的食物集合实例,则需要在实例上创建属性。例如:

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

要回答您关于示例 1 的问题,如果它在原型链中的任何位置都找不到该属性,它会在目标对象上创建该属性。

于 2013-04-17T14:58:57.687 回答