前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Prototypal inheritance原型继承(实用篇)

Prototypal inheritance原型继承(实用篇)

作者头像
lesM10
发布2019-08-26 16:44:17
5990
发布2019-08-26 16:44:17
举报

本文侧重于如何应用prototype inheritance,想了解基本概念的可以查看基础概念篇

在programing时,我们总是想从已有的事物中继承并扩展。

例如,我们有一个user对象(user有着自己的properties和methods),并且想修改user来实现admin和guest。我们喜欢重用在user中已有的methods,而不是复制或重新实现user的methods。我们想做的只是在user上构建一个新的对象。

Prototypal inheritance是个有助于实现它的一个语言特性。


[Prototype]原型

在JavaScript中,对象都有一个特别的隐藏property(即[Prototype]),prototype要么是null要么引用着另外一个对象。被引用的对象就可以被称为“a prototype”:

object-prototype

[Prototype]有着不可思议的含义。当我们想从对象中读取一个property,但是该对象没有该property时,JavaScript会自动从该对象的prototype中读取该property。这样的事情就被称之为“prototypal inheritance”。许多非常cool的语言特性和编程技巧都是基于“prototypal inheritance”的。

[Prototype] property是内部的和隐藏的,但是仍然有许多方法可以设置它。

使用proto就是其中的一种方法,像这样:

代码语言:javascript
复制
let animal = {
  eats: true
};
let rabbit = {
  jumps: true
};

rabbit.__proto__ = animal;

请注意proto[Prototype]并不完全相同。proto[Prototype]的getter/setter方法。

如果我们在rabbit中查找一个property,但是没找到,JavaScript会自动在animal中查找该property

例如:

代码语言:javascript
复制
let animal = {
  eats: true
};
let rabbit = {
  jumps: true
};

rabbit.__proto__ = animal; // (*)

// 我们现在 在rabbit中 即可找到eats也可找到jumps:
alert( rabbit.eats ); // true (**)
alert( rabbit.jumps ); // true

第八行 设置animalrabbit的prototype。

接着,当alert尝试读取property rabbit.eats时,eats不在rabbit中,所以JavaScript会沿着[Prototype]animal中找到eats(并且是自下而上查找):

rabbit-animal

在此我们可以说:“animalrabbit的原型”,或者“rabbit在原型上 继承自animal”。

所以,如果animal有非常多有用的properties和methods,那么在rabbit中 也可以使用这些properties和methods。这些properties被称作“inherited”。

如果在animal中有一个方法,那么在rabbit上 它也是可以被调用的:

代码语言:javascript
复制
let animal = {
  eats: true,
  walk() {
    alert("Animal walk");
  }
};

let rabbit = {
  jumps: true,
  __proto__: animal
};

// walk 是从Prototype中得来的
rabbit.walk(); // Animal walk

方法walk是从prototype中自动继承来的,像这样:

原型链可以很长:

代码语言:javascript
复制
let animal = {
  eats: true,
  walk() {
    alert("Animal walk");
  }
};

let rabbit = {
  jumps: true,
  __proto__: animal
};

let longEar = {
  earLength: 10,
  __proto__: rabbit
}

// walk is taken from the prototype chain
longEar.walk(); // Animal walk
alert(longEar.jumps); // true (from rabbit)

事实上,Prototype有2个限制:

  1. 引用不能形成环状。如果我们尝试给proto赋值,来形成环状结构,JavaScript会抛出错误。
  2. proto的值要么是一个对象要么是null。没有其它的值。

虽然很明显,但是还是要说一下:一个对象只有一个prototype。一个对象不能同时从2个以上的其它对象继承。


Read/write rules读写规则

Prototype仅用来读取properties。

对于data properties(非getter/setter方法),write/delete操作直接作用于对象自身。在下面的例子中,我们对rabbit自身的walk方法赋值:

代码语言:javascript
复制
let animal = {
  eats: true,
  walk() {
    /* this method won't be used by rabbit */
  }
};

let rabbit = {
  __proto__: animal
}

rabbit.walk = function() {
  alert("Rabbit! Bounce-bounce!");
};

rabbit.walk(); // Rabbit! Bounce-bounce!

从此刻开始,rabbit.walk()rabbit中可以立即找到walk并执行它。而不用求助于prototype:

对于getters/setters方法,如果我们read/write一个property,它们会在prototype中被查找,被调用。例如,在下面代码中查看admin.fullName property:

代码语言:javascript
复制
let user = {
  name: "John",
  surname: "Smith",

  set fullName(value) {
    [this.name, this.surname] = value.split(" ");
  },

  get fullName() {
    return `${this.name} ${this.surname}`;
  }
};

let admin = {
  __proto__: user,
  isAdmin: true
};

alert(admin.fullName); // John Smith (*)

// setter triggers!
admin.fullName = "Alice Cooper"; // (**)

在代码中的第19行,property admin.fullName在prototype user中有一个getter方法,所以该getter方法就被调用了。在代码的第22行,property admin.fullName在prototype中有一个setter方法,所以它就被调用了。

The value of "this" - this指针的值

在上述的例子中,会浮现出一个有趣的问题:在set fullName(value)中,this指针的值是什么?properties this.name和this.surname被写在哪里:user? 还是admin?

答案很简单:this是不会被prototypes影响的。

不管方法是在哪儿被找到的:在对象中还是在对象的原型中。在调用一个方法时,this指针总是.前的对象

所以,setter方法实际上是使用admin作为this的,而不是user

这确实是个非常重要的事情,因为我们可能有一个拥有很多方法的对象,并且我们可能会从该对象继承。接着,我们可以在继承的对象上调用原型的方法,并且这些方法会修改继承对象的状态,而不是原型对象的状态。

例如,这儿的animal代表着一个方法容器,rabbit会使用animal中的方法。

函数调用rabbit.sleep()rabbit对象上设置this.isSleeping:

代码语言:javascript
复制
// animal has methods
let animal = {
  walk() {
    if (!this.isSleeping) {
      alert(`I walk`);
    }
  },
  sleep() {
    this.isSleeping = true;
  }
};

let rabbit = {
  name: "White Rabbit",
  __proto__: animal
};

// modifies rabbit.isSleeping
rabbit.sleep();

alert(rabbit.isSleeping); // true
alert(animal.isSleeping); // undefined (no such property in the prototype)

结果图:

如果我们有其它诸如bird,snake之类的继承自animal的对象,它们也有访问animal方法的权利。但是,在各自方法中的this指针会是相应的对象(调用函数时,在.前的对象),而不是animal。所以,当我们向this中写入数据时,这些数据是被存储到对应的对象中了。

因此,方法是被共享的,但是对象的状态不应该是共享的。


Summary 总结

  • 在JavaScript中,所有的对象都有个隐藏的[[Prototype]] property,该[[Prototype]] property的值 要么是另一个对象,要么为null。
  • 我们可以使用obj.__proto__来访问[[Prototype]] property
  • [[Prototype]]引用的对象被叫做“prototype
  • 如果我们想读取对象的一个property,或者调用对象的一个method,但是该propertymethod在对象中不存在,接着JavaScript会尝试在prototype中查找该propertymethodWrite/delete操作会直接作用于对象上,而不是作用于prototype上(除非property是个setter方法)
  • 如果我们调用obj.method(),并且该method来自于prototypethis仍然指向obj。所以,methods总是作用于当前对象,即使这些methods继承自prototype

下面是一些帮助理解的例子:

Working with prototype

下面的代码创建了2个对象,接着修改了它们。

在修改的过程中它们会显示什么值?

代码语言:javascript
复制
let animal = {
  jumps: null
};
let rabbit = {
  __proto__: animal,
  jumps: true
};

alert( rabbit.jumps ); // ? (1)

delete rabbit.jumps;

alert( rabbit.jumps ); // ? (2)

delete animal.jumps;

alert( rabbit.jumps ); // ? (3)

Solution:

  1. true, 从rabbit中找到jumps
  2. null, 从animal中找到jumps
  3. undefined, rabbit和animal中都找不到jumps

Searching algorithm

任务有2部分。

我们有一个对象:

代码语言:javascript
复制
let head = {
  glasses: 1
};

let table = {
  pen: 3
};

let bed = {
  sheet: 1,
  pillow: 2
};

let pockets = {
  money: 2000
};
  1. 使用proto修改prototypes,使得任意的property查找都沿着路径:pockets → bed → table → head。例如pockets.pen应该是3(在table中找到), bed.glasses应该是1(在head中找到)。
  2. 使用pocket.glasses或者head.glasses来得到glasses,哪种方式更快?需要的话,说出判断依据。

Solution:

让我们添加__proto__

代码语言:javascript
复制
let head = {
  glasses: 1
};

let table = {
  pen: 3,
  __proto__: head
};

let bed = {
  sheet: 1,
  pillow: 2,
  __proto__: table
};

let pockets = {
  money: 2000,
  __proto__: bed
};

alert( pockets.pen ); // 3
alert( bed.glasses ); // 1
alert( table.money ); // undefined

在现如今的引擎中,不管我们从object中,还是从prototype中,获得property,都没有区别。因为现如今的引擎会记得property是从哪儿找到的,并在下次的请求中直接使用,而不用重新查找。例如,对于pocket.glasses,它们记得它们是在哪儿找到的glasses(在head中),下次会直接在head中搜索glasses。如果有些事情改变了,这些引擎也足够智能去更新内部缓存。

Where it writes?

我们的rabbit继承自animal

如果我们调用rabbit.eats(), 哪一个对象接受这个full property: animal还是rabbit

代码语言:javascript
复制
let animal = {
  eat() {
    this.full = true;
  }
};

let rabbit = {
  __proto__: animal
};

rabbit.eat();

Solution:

答案:rabbit

这是因为rabbit.前的对象,所以rabbit.eat()修改了rabbit

property的查找和执行是2个完全不同的事情。方法rabbit.eat是在prototype中找到的,接着使用this=rabbit来执行rabbit.eat

Why two hamsters are full?

我们有2个仓鼠:speedylazy,它们都继承自通用的仓鼠对象。

当我们喂它们中的一个时,另外一个也会饱。为什么?怎样修改代码才能修正这个问题?

代码语言:javascript
复制
let hamster = {
  stomach: [],

  eat(food) {
    this.stomach.push(food);
  }
};

let speedy = {
  __proto__: hamster
};

let lazy = {
  __proto__: hamster
};

// This one found the food
speedy.eat("apple");
alert( speedy.stomach ); // apple

// This one also has it, why? fix please.
alert( lazy.stomach ); // apple

Solution:

让我们来仔细观察下,在函数调用(speed.eat("apple"))期间究竟发生了什么?

  1. 方法speedy.eat是在原型(hamster)中找到的, 接着使用this=speedy(在.前的对象)执行speedy.eat
  2. 接着this.stomach.push()需要找到stomach property, 并在stomach上调用push。首先在this=speedy中查找stomach,但是没有找到。
  3. 接着在prototype中查找,并在hamster中找到stomach
  4. 接着在hamster的stomach上调用push方法,添加foodhamster的stomach

所以,所有的仓鼠共用了一个stomach!

每次stomach都来自prototype, 接着stomach.push修改stomach

请注意,如果使用this.stomach=来赋值,那么像上面的事情就不会发生:

代码语言:javascript
复制
let hamster = {
  stomach: [],

  eat(food) {
    // assign to this.stomach instead of this.stomach.push
    this.stomach = [food];
  }
};

let speedy = {
   __proto__: hamster
};

let lazy = {
  __proto__: hamster
};

// Speedy one found the food
speedy.eat("apple");
alert( speedy.stomach ); // apple

// Lazy one's stomach is empty
alert( lazy.stomach ); // <nothing>

现在所有的都完美工作,因为this.stomach不会执行stomach的查找。值会被直接写入到this.

我们也可以通过给每个仓鼠新建一个它自己的stomach,来避免问题的出现。

代码语言:javascript
复制
let hamster = {
  stomach: [],

  eat(food) {
    this.stomach.push(food);
  }
};

let speedy = {
  __proto__: hamster,
  stomach: []
};

let lazy = {
  __proto__: hamster,
  stomach: []
};

// Speedy one found the food
speedy.eat("apple");
alert( speedy.stomach ); // apple

// Lazy one's stomach is empty
alert( lazy.stomach ); // <nothing>

作为一个通用解决方案,所有描述某个特殊对象的状态的properties, 像上面例子中的stomach,都通常被写入到那个对象中。这样可以避免类似的问题。

本文翻译自:http://javascript.info/prototype-inheritance 转载请注明出处

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2017.06.18 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • [Prototype]原型
  • Read/write rules读写规则
  • The value of "this" - this指针的值
  • Summary 总结
    • 下面是一些帮助理解的例子:
    • Working with prototype
    • Searching algorithm
    • Where it writes?
    • Why two hamsters are full?
    相关产品与服务
    容器服务
    腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档