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

转载请注明出处

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,463评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,868评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,213评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,666评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,759评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,725评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,716评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,484评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,928评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,233评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,393评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,073评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,718评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,308评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,538评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,338评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,260评论 2 352

推荐阅读更多精彩内容