原型和原型式继承
- JavaScript使用原型式继承,对象可从其他对象那里继承属性和行为
- 原型也是对象:原型指的是其属性和行为被继承的对象
- 新对象继承了原型对象后,便可访问其所有方法和属性,同时可以在新对象中添加额外的属性
利用继承原型创建大量对象,思路是:
- 将所有对象中相同的属性和方法(希望重用的那部分)包含在原型中;
ps. 另外,想要将所有对象的某个属性统一设置初始值,也可使用原型 - 对于那些随对象而异的属性和方法,等到继承原型创建新对象时,再针对不同对象添加其特有的属性
获取原型
对象构造函数有一个prototype
属性,这是一个指向原型的引用
是的,函数也有属性,因为JavaScript中函数也是对象
实际上,在JavaScript中,几乎所有的东西都是对象,数组也是。但就目前而言,只需知道构造函数都包含属性prototype。后面将更深入地讨论函数以及其他对象
创建原型(和构造函数)
- 原型也是对象:原型指的是其属性和行为被继承的对象
在创建一个原型时:
- 构造函数用于将传入的参数赋给对象实例特有的属性和方法
构造函数.prototype
指向原型对象,设置原型对象的属性和方法,从而指定原型所包含的属性和方法
- 创建构造函数:添加每个对象实例所特有的属性和方法(这些不同的部分需要单独用构造函数定制)
function Dog(name, breed, weight) {
this.name = name;
this.breed = breed;
this.weight = weight;
}
- 设置原型:设置原型对象(
构造函数.prototype
对象)的属性和方法,从而指定原型所包含的属性和方法
ps.Dog.prototype
指向原型对象
Dog.prototype.species = "Canine";
Dog.prototype.bark = function() {
if (this.weight > 25) {
console.log(this.name + " says Woof!");
} else {
console.log(this.name + " says Yip!");
}
};
Dog.prototype.run = function() {
console.log("Run!");
};
注意:位于原型中的bark方法仍然使用this.name
在没有使用原型的情况下(使用构造函数时),这很容易解释,因为this指的是方法被调用的对象。
调用原型中的方法bark时,可能误以为this指的是原型对象,但情况并非如此。
在任何情况下,this都指向原始对象,即方法被调用的对象,即便该方法位于原型中亦如此。因此,即便方法bark位于原型中,调用对象的bark方法时,this被设置为方法被调用的对象(即原始小狗对象),即使在对象中未找到该方法,而是在原型中找到了它,也不会修改this的值。得到的结果也是我们期望的:在哪个对象中调用了bark方法,则this就始终指向该对象。
- 最后,如以前一样正常使用构造函数创建新的对象实例,即可实现继承原型
(因为这里的构造函数与以前不同了:我们通过prototype
属性设置了其原型) - 另外还可以重写原型(不希望继承原型中已定义好的属性和方法,而是定制这些属性和方法)
- 重写不会改变原型,也不会影响其他继承原型的对象实例
这之所以可行,源于继承的工作原理:优先在具体的对象实例中查找属性和方法;如果找不到,再在原型中查找
由此,在对象中重写属性和方法后,JavaScript调用它们时,直接在对象实例中找到所需的属性和方法,而不必再劳神去原型中查找
继承原型,并创建新的对象实例
var fido = new Dog("Fido", "Mixed", 38);
重写原型的bark方法
spot.bark = function() {
console.log(this.name + " says WOOF!");
};
测试新的对象实例
fido.bark(); 使用的是 重写的定制bark方法
fido.run(); 使用的是 原型的run方法
修改原型:原型是动态的
已经知道,在创建构造函数和原型后,可以通过继承原型获取新的对象实例;
此后,可以随时给原型添加/修改属性和方法,继承该原型的所有对象实例能立即看到并使用这些属性和方法
这之所以可行,仍源于继承的工作原理:优先在具体的对象实例中查找属性和方法;如果找不到,再在原型中查找
因此,可以随时按需要修改原型,从而动态地更新原型,继承原型的对象也能立即看到原型的这些改动,并使用改动后的方法和属性(前提是对象实例没有重写这个方法或属性)
原型的另一个应用:设置初始值、用方法重写原型
再次运用原型的核心思想:消除重复,一旦有重复的部分,就将其包含在原型中(在这里,“重复部分”就是大量对象共同使用的初始值)
- 之前使用原型,目的是将所有对象中重复的属性和方法统一编写,实现重用
- 想要将所有对象的某个属性统一设置初始值,也可使用原型
例如,统一将所有小狗的初始状态设置为站立,sit方法可以使小狗坐下
- 在原型中新增sitting属性,其默认值为false;
- 同时在原型中新增sit方法,如果调用sit方法,就对小狗实例重写原型的属性sitting,即在对象中添加属性sitting的值为true。
这样,我们能够给所有小狗对象指定默认值,并在需要时对各个小狗进行定制。
改写原型,为小狗设置初始状态为站立,而sit方法可以使小狗坐下
Dog.prototype.sitting = false;
Dog.prototype.sit = function() {
if (this.sitting) {
console.log(this.name + " is already sitting");
} else {
this.sitting = true;
console.log(this.name + " is now sitting");
}
};
测试
var fido = new Dog("Fido", "Mixed", 38);
fido.sit();输出"fido is now sitting"
fido.sit();输出"fido is already sitting"
注意分析这里的原理:
Dog.prototype.sit = function() {
if (this.sitting) {
...
- 首次调用fido.sit时,fido对象中没有sitting属性,sitting的值从原型中获取;
...
else {
this.sitting = true;
console.log(this.name + " is now sitting");
}
- 但接下来将sitting设置为true时,是在对象实例中进行的(而非原型),相当于重写原型,在对象实例中添加了sitting属性,并设置为true。
为什么这里对象实例使用了原型中的sit方法,能够重写原型呢?
始终记住,在任何情况下,this都指向原始对象,即方法被调用的对象,即便该方法位于原型中亦如此。调用fido.sit()
,即使因为fido没有sit方法而使用了原型的sit方法,该方法中的this仍始终指向fido
- 第二次调用fido.sit时,fido对象实例中已经添加sitting属性,sitting的值从对象实例中获取
如何判断使用的属性包含在实例中还是原型中?
对象实例都包含方法Obj.hasOwnProperty("property")
- 传入一个字符串,以查询某个属性是否在该对象中有定义
- 如果属性是在对象实例中定义的,这个方法将返回true
- 如果能够访问某属性,但是它又不是在对象实例中定义的,则可肯定是在原型中定义的
以上面的sitting属性为例,一开始sitting仅包含在原型中,故
fido.hasOwnProperty("sitting");
返回false;
而调用fido.sit();
后,在对象实例中添加了属性sitting,重写了原型,则fido.hasOwnProperty("sitting");
返回true;