本文侧重于如何应用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”:
[[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
(并且是自下而上查找):
在此我们可以说:“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个限制:
- 引用不能形成环状。如果我们尝试给proto赋值,来形成环状结构,JavaScript会抛出错误。
- 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
,但是该property
或method
在对象中不存在,接着JavaScript会尝试在prototype
中查找该property
或method
。Write/delete
操作会直接作用于对象上,而不是作用于prototype
上(除非property
是个setter
方法)- 如果我们调用
obj.method()
,并且该method来自于prototype
,this
仍然指向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:
- true, 从rabbit中找到jumps
- null, 从animal中找到jumps
- 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
};
- 使用proto修改
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。如果有些事情改变了,这些引擎也足够智能去更新内部缓存。
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个仓鼠: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 ); // apple
Solution:
让我们来仔细观察下,在函数调用(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
,都通常被写入到那个对象中。这样可以避免类似的问题。
转载请注明出处