Prototypal inheritance原型继承(实用篇)

本文侧重于如何应用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就是其中的一种方法,像这样:

let animal = {
  eats: true
};
let rabbit = {
  jumps: true
};

rabbit.__proto__ = animal;

请注意proto[[Prototype]]并不完全相同。proto[[Prototype]]的getter/setter方法。 如果我们在rabbit中查找一个property,但是没找到,JavaScript会自动在animal中查找该property 例如:

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上 它也是可以被调用的:

let animal = {
  eats: true,
  walk() {
    alert("Animal walk");
  }
};

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

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

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

原型链可以很长:

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方法赋值:

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:

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:

// 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个对象,接着修改了它们。

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

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部分。

我们有一个对象:

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__

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

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,它们都继承自通用的仓鼠对象。

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

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=来赋值,那么像上面的事情就不会发生:

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,来避免问题的出现。

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

转载请注明出处

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏三掌柜的技术空间

Android开发:App点击跳转到网页的实现

在Android开发过程中,点击App里面的控件进行网页跳转是经常会遇到的需求,但是Android的网页跳转有两种方式实现App点击控件跳转到网页,第一种就是A...

42730
来自专栏明丰随笔

浅谈消息队列 2

JMS的客户端之间可以通过JMS服务进行异步的消息传输。API是一个消息服务的标准或者说是规范,允许应用程序组件基于JavaEE平台创建、发送、接收和读取消息。...

10840
来自专栏凯哥Java

设计模式讲解-第1篇初识设计模式

    设计模式是人们在面对同类型软件工程设计问题所总结出的一些有用的经验(是前辈们对代码开发经验的总结,是解决特定问题的一系列套路)。设计模式不是代码,而是某...

9730
来自专栏格姗知识圈

Spring 常犯的 10 大错误,尤其是最后一个!

译文 | cnblogs.com/liululee/p/11235999.html

9530
来自专栏测试游记

​测试开发进阶(二)

https://pythonguidecn.readthedocs.io/zh/latest/writing/structure.html

11440
来自专栏Java架构学习路线

一道78%的Java程序员搞不清的Spring bean面试题

熟悉Spring开发的朋友都知道Spring提供了5种scope分别是singleton、prototype、request、session、global se...

7300
来自专栏灵魂画师牧码

七夕算法:521. 最长特殊序列 Ⅰ

https://leetcode-cn.com/problems/longest-uncommon-subsequence-i/

11730
来自专栏品茗IT

SpringBoot入门建站全系列(八)集成模板引擎(thymeleaf)渲染页面

本文不讲前后端分离,先讲下模板引擎,Springboot支持很多模板引擎,thymeleaf算是比较好用的一种。

16650
来自专栏码匠的流水账

聊聊sharding-jdbc的SQLExecutionHook

incubator-shardingsphere-4.0.0-RC1/sharding-core/sharding-core-execute/src/main/...

9100
来自专栏学院君的专栏

Go 语言基础入门教程 —— 第一个 Go 程序

使用 Go 语言之前,首先要安装 Go。Go 为 Linux、Mac、Windows 等不同的平台分别提供了相应的安装包:https://golang.goog...

10430

扫码关注云+社区

领取腾讯云代金券

年度创作总结 领取年终奖励