本文侧重于如何应用prototype inheritance,想了解基本概念的可以查看基础概念篇。
在programing时,我们总是想从已有的事物中继承并扩展。
例如,我们有一个user对象(user有着自己的properties和methods),并且想修改user来实现admin和guest。我们喜欢重用在user中已有的methods,而不是复制或重新实现user的methods。我们想做的只是在user上构建一个新的对象。
Prototypal inheritance是个有助于实现它的一个语言特性。
在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第八行 设置animal为rabbit的prototype。
接着,当alert尝试读取property rabbit.eats时,eats不在rabbit中,所以JavaScript会沿着[Prototype]在animal中找到eats(并且是自下而上查找):

rabbit-animal
在此我们可以说:“animal是rabbit的原型”,或者“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个限制:
虽然很明显,但是还是要说一下:一个对象只有一个prototype。一个对象不能同时从2个以上的其它对象继承。
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方法,所以它就被调用了。
在上述的例子中,会浮现出一个有趣的问题:在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中写入数据时,这些数据是被存储到对应的对象中了。
因此,方法是被共享的,但是对象的状态不应该是共享的。
[[Prototype]] property,该[[Prototype]] property的值 要么是另一个对象,要么为null。obj.__proto__来访问[[Prototype]] property。[[Prototype]]引用的对象被叫做“prototype”property,或者调用对象的一个method,但是该property或method在对象中不存在,接着JavaScript会尝试在prototype中查找该property或method。Write/delete操作会直接作用于对象上,而不是作用于prototype上(除非property是个setter方法)obj.method(),并且该method来自于prototype,this仍然指向obj。所以,methods总是作用于当前对象,即使这些methods继承自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:
任务有2部分。
我们有一个对象:
let head = {
glasses: 1
};
let table = {
pen: 3
};
let bed = {
sheet: 1,
pillow: 2
};
let pockets = {
money: 2000
};prototypes,使得任意的property查找都沿着路径:pockets → bed → table → head。例如pockets.pen应该是3(在table中找到), bed.glasses应该是1(在head中找到)。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。如果有些事情改变了,这些引擎也足够智能去更新内部缓存。
我们的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。
我们有2个仓鼠:speedy和lazy,它们都继承自通用的仓鼠对象。
当我们喂它们中的一个时,另外一个也会饱。为什么?怎样修改代码才能修正这个问题?
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 ); // appleSolution:
让我们来仔细观察下,在函数调用(speed.eat("apple"))期间究竟发生了什么?
speedy.eat是在原型(hamster)中找到的, 接着使用this=speedy(在.前的对象)执行speedy.eat。this.stomach.push()需要找到stomach property, 并在stomach上调用push。首先在this=speedy中查找stomach,但是没有找到。prototype中查找,并在hamster中找到stomach。hamster的stomach上调用push方法,添加food到hamster的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 转载请注明出处