原型继承 - 编写

IT技术 javascript inheritance prototype
2020-12-19 22:31:41

所以我有这 2 个例子,来自 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创建和改变,而在第一个例子是上升的原型链,因为它无法找到属性。

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

2个回答

构造函数介绍

您可以使用函数作为构造函数来创建对象,如果构造函数名为 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 和 ben 共享22 给 ben.walkwalk分配也不会影响 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 将在 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. 定义一个对象,它的成员在许多实例中都是相同的(person 是 bob 和 ben 的蓝图,可以是 jilly、marie、clair ...)
  2. 定义对于实例(bob 和 ben)应该是唯一的实例特定成员。
  3. 创建一个实例,运行第 2 步中的代码。

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

在这段代码中,我从原型和食物中删除了名称,因为无论如何在创建实例时,您很可能会几乎立即隐藏它。Name 现在是一个实例特定的成员,在构造函数中设置了默认值。因为 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);//=[]

您可能会遇到类似的模式,它们在帮助对象创建和对象定义方面更加强大。

inheritance

下面的代码展示了如何继承。任务与之前的代码基本相同,但有一点额外

  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 的原型设置为 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 的成员。

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

扩展和覆盖父函数

有时children需要扩展parent功能。

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

在下面的示例中,我们假设仓鼠每小时可以跑 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.

缺点是您对 Hamster.prototype 进行了硬编码。可能有一些模式可以为您super提供 Java 中的优势

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

要覆盖父 (=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。在 JavaScript 中,我们可以定义 Movable 并在此处定义实现,Cat 可以覆盖、扩展它,也可以将其作为默认实现。

对于 Movable,我们有特定于实例的成员(如location)。而且我们有不特定于实例的成员(如函数 move())。创建实例时,将通过调用 mxIns(由 mixin 辅助函数添加)来设置实例特定成员。Prototype 成员将使用 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

这种在函数链中传递参数的方式在很多情况下都很有用。当您编写代码来计算某物的总和,然后您想将某物的总和重新分解为某种货币时,您可能需要更改许多函数以传递货币值。您可以扩大货币value的范围(甚至像 全球一样window.currency='USD'),但这是解决它的一种糟糕方法。

通过传递一个对象,您可以将货币添加到args函数链中可用的任何时候,并在需要时改变/使用它而无需更改其他函数(明确必须在函数调用中传递它)。

私有变量

JavaScript 没有私有修饰符。

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

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

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

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

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

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

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

@HMR如果我给你信用并链接到这个问题,我可以将你的答案复制到我博客上的一篇博文中吗?
2021-02-14 22:31:41
希望我能把这个投票提高 10 倍。做得好!
2021-02-23 22:31:41

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

Hamster.prototype.food = []

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

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

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

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

“如果它没有在原型链的任何地方找到该属性,它就会在目标对象上创建该属性。” - 那是我遗漏的信息 :) 谢谢
2021-02-13 22:31:41
@HMR 这是否意味着该值仅在它是对象时才共享?
2021-02-15 22:31:41
对于像 string 和 int 这样的简单值会让人感到困惑,因为实例不共享该值。
2021-02-28 22:31:41
@frrlod:说“只要它是原始类型,就不会共享该值”会/稍微/更正确例如字符串、数字、布尔值。javascriptweblog.wordpress.com/2010/09/27/...
2021-03-01 22:31:41