关于javascript:原型继承 – 写作

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开始:当代码到达speedy.found时,它在speedy中找不到found属性,因此它爬上原型并在那里进行更改。 这就是为什么food.length对于两只仓鼠都是相同的,换句话说,它们具有相同的胃。
据我所知,在编写和添加一个不存在的新属性时,解释器将上升到原型链,直到找到属性,然后更改它。

但是在示例1中还发生了其他事情:
我们运行rabbit.eat,它会改变rabbit.fullfull属性无处可寻,所以应该将原型链上升到(对象??),好吧,我不知道这里发生了什么。 在此示例中,rabbit的属性full已创建并更改,而在第一个示例中,它会上升到原型链,因为它无法找到该属性。

我很困惑,也看不出为什么会这样。


构造函数介绍

您可以使用函数作为构造函数来创建对象,如果构造函数名为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

属性/成员name是特定于实例的,对于bob和ben来说是不同的

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

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函数中它使用thisthis的值将是调用对象;现在让我们说它是当前的实例,所以对于bob.walk()"这个"将是bob。 (更多关于"this"和稍后调用的对象)。

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

当我们做像ben.walk=22这样的事情时会发生阴影成员,即使bob和ben共享walk 22对ben.walk的赋值也不会影响bob.walk。这是因为该语句将直接在ben上创建一个名为walk的成员,并为其赋值22.将有2个不同的walk成员:ben.walk和Person.prototype.walk。

当请求bob.walk时,你将得到Person.prototype.walk函数,因为在bob上找不到walk。然后要求ben.walk将获得值22,因为成员遍历已在ben上创建,并且因为JavaScript发现在Ben上行走它将不会在Person.prototype中查找。

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

更多关于原型

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

在前面的部分中,我们已经看到重新分配来自实例原型(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应该有自己的food成员。这是构造函数的用武之地。它用于创建特定于实例的成员。您还可以向其传递参数以设置这些特定于实例的成员的值。

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

  • 定义一个对象,其成员对于许多实例都是相同的(人是bob和ben的蓝图,可以是jilly,marie,clair ...)
  • 定义实例特定成员,这些成员对于实例(bob和ben)应该是唯一的。
  • 在步骤2中创建运行代码的实例。
  • 使用构造函数,您将在以下代码中的步骤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);//=[]

    您可能会遇到类似的模式,这些模式更强大,可以帮助创建对象和定义对象。

    遗产

    以下代码显示了如何继承。这些任务与之前的代码基本相同,但需要额外的一些

  • 定义对象的实例特定成员(函数Hamster和RussionMini)。
  • 设置继承的原型部分(RussionMini.prototype = Object.create(Hamster.prototype))
  • 定义可以在实例之间共享的成员。(Hamster.prototype和RussionMini.prototype)
  • 在步骤1中创建一个运行代码的实例,对于继承它们的对象,也运行父代码(Hamster.apply(this,arguments);)
  • 使用模式有些人会称之为"经典继承"。如果您对语法感到困惑,我会很乐意解释更多或提供不同的模式。

    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.apply(this,arguments);运行,而Hamster.apply(this,arguments);依次运行this.food = [],但任何Hamster成员仍将是RussionMini.prototype的成员。

    另一个原因可能是创建一个Hamster需要对传递的参数进行许多复杂的计算,这些参数可能还没有,再次你可以传入伪参数,但它可能会不必要地使你的代码复杂化。

    扩展和覆盖父函数

    有时children需要扩展parent函数。

    你希望'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一样为您提供super的优势。

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

    要覆盖Parent(= Hamster)方法,您可以执行相同的操作,但不要执行Hamster.prototype.parentMethod.call(this,....

    this.constructor

    构造函数属性由JavaScript包含在原型中,您可以更改它,但它应该指向构造函数。所以Hamster.prototype.constructor应该指向Hamster。

    如果在设置继承的原型部分后,您应该再次指向正确的功能。

    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,我们有特定于实例的成员(如location)。我们的成员不是特定于实例的(如函数move())。在创建实例时,将通过调用mxIns(由mixin helper函数添加)来设置特定于实例的成员。原型成员将使用mixin辅助函数从Movable.prototype中逐个复制到Cat.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
    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引用当前实例。

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

    澄清一下,看下面的代码:

    1
    theInvokingObject.thefunction();

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

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

    要在上述情况下使this引用someObject,您可以直接传递闭包而不是函数:

    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(Hamster.apply(this,arguments);)时,我们假设Hamster以相同的顺序使用与RussionMini相同的参数。对于调用其他函数的函数,我通常使用另一种方法来传递参数。

    我通常将一个对象传递给一个函数并让该函数改变它需要的任何东西(设置默认值),然后该函数将它传递给另一个将执行相同操作的函数,依此类推。这是一个例子:

    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

    在许多情况下,这种在函数链中传递参数的方法很有用。当您正在处理可以计算某些内容的代码时,您希望重新计算某种货币的总和,您可能需要更改许多函数来传递货币的值。你可以扩大货币价值范围(甚至是window.currency='USD'这样的全球范围),但这是解决问题的一种不好的方法。

    通过传递一个对象,只要它在函数链中可用,就可以将货币添加到args,并在需要时随时改变/使用它而不更改其他函数(显式必须在函数调用中传递它)。

    私有变量

    JavaScript没有私有修饰符。

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

    您可以通过命名_aPrivate或将所有私有变量放在名为_的对象变量中,向其他程序员指示成员是私有的。

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

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

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

    通过使用闭包,您不会给其他人一个选择,并使用命名约定和您所做的文档。这不是特定于JavaScript的,在其他语言中,您可以决定不使用私有成员,因为您相信其他人知道他们正在做什么并让他们选择按照自己的意愿行事(涉及风险)。

    如果你仍然坚持私有,那么以下模式可能有所帮助。它不实现私有,但实现受保护。

    好。


    原型未针对对象的每个实例进行实例化。

    1
    Hamster.prototype.food = []

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

    如果您需要(在这种情况下),每个Hamster的食物集合的单独实例,您需要在实例上创建属性。 例如:

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

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