问题背景
我们已经实现了小狗原型,使用小狗原型能创建出大量的小狗对象实例;
在此基础上,对于一部分特殊的表演犬,它们具有与普通小狗不同的方法和属性;
我们希望仍然利用原型来创建这些表演犬对象实例,但又不希望从头开始另行编写一个表演犬原型
原型链
- 对象不仅可以继承一个原型,还可以继承一个原型链
- JavaScript中,可以有多个原型:可建立供对象继承的原型链,即继承了其他原型的原型。这就好比你不只是继承了父母的特质,还继承了祖父母、曾祖父母等的一些特质。
- 注意,对“上游的”原型所做的任何修改都将(直接或间接)影响继承该原型的所有实例:例如,修改小狗原型的bark方法,不仅影响小狗实例,也影响表演犬实例(除非有重写)
在这里的情况就是,通过扩展小狗原型来创建表演犬原型:重用小狗原型的行为,并且在此基础上添加表演犬的特有方法,最终得到表演犬原型
对表演犬实例调用bark方法时,规则是沿着继承链向上查找:优先在对象实例中查找bark方法,找不到就去表演犬原型中查找;再找不到就去小狗原型中查找(并最终在小狗原型中找到bark方法并调用)
创建原型链
复习:
- 原型也是对象:原型指的是其属性和行为被继承的对象
在创建一个原型时:
- 构造函数用于将传入的参数赋给对象实例特有的属性和方法
构造函数.prototype
指向原型对象,设置原型对象的属性和方法,从而指定原型所包含的属性和方法
创建原型链与创建原型有何不同
- 创建一个原型:直接使用构造函数,并设置其
prototype
对象的属性和方法 - 创建原型链:实质上是创建 继承另一个原型的原型对象
i.e. 表演犬原型是一个继承小狗原型的对象
因此,无论创建原型还是原型链,本质就是设置构造函数的prototype
属性
- 构造函数有默认的原型Object,即
prototype
属性默认指向Object - 但也可将
prototype
设置为其他(自定义的)原型,从而获得继承原型的原型
创建原型链的方法
在创建一个原型链时:
- 需创建构造函数
- 关键点和不同点在于:还需设置
构造函数.prototype
指向小狗原型(表演犬原型继承小狗原型)
实际中不那么做,而是将其指向一个小狗对象实例,它与小狗原型的属性与方法完全相同,即ShowDog.prototype=new Dog()
(注意,没有传入参数,否则得到的是一个包含特有属性和方法的小狗对象实例)
下面演示基于小狗原型创建表演犬原型(原型链)
首先假定小狗原型已经编写好
function Dog(name, breed, weight) {
this.name = name;
this.breed = breed;
this.weight = weight;
}
Dog.prototype.species = "Canine";
Dog.prototype.bark = function() {
console.log(this.name + " says Woof!");
};
Dog.prototype.run = function() {
console.log("Run!");
};
- 设置构造函数:用它接收参数并赋给表演犬实例特有的的属性
传入每只表演犬特有的属性,从而创建新的表演犬实例
function ShowDog(name, breed, weight, handler) {
this.name = name;
this.breed = breed;
this.weight = weight;
this.handler = handler;
}
- 关键点和不同点:设置
构造函数.prototype
指向小狗原型(实际是将其指向一个小狗对象实例,它与小狗原型的属性与方法完全相同)
注意,这一步是创建原型链(原型继承原型)的关键:
ShowDog.prototype = new Dog(); //表演犬原型本质上是一个小狗实例
这里为何不向构造函数传递参数:
1.设置表演犬原型时,我们唯一的要求是表演犬继承小狗原型
因此我们仅希望得到一个通用小狗实例(而非一个具体小狗实例)
2.如果传入参数name、breed、weight等,表演犬将继承这些属性
但表演犬实例也看不到这些值,因为表演犬也有name、breed、weight等属性,并总是重写这些属性
ShowDog.prototype = new Dog();
对应上图
这条语句的实质是:
new Dog()
创建了一个继承小狗原型的通用小狗实例(而没有传入参数,不是一个特定的具体小狗实例)- 然后将这个实例赋给
ShowDog.prototype
,即:表演犬原型本质是一个小狗实例
- 设置原型:设置原型对象(
构造函数.prototype
对象)的属性和方法,从而指定原型所包含的属性和方法
注意,这里千万不能写成ShowDog.prototype=某个对象
的形式,这样会覆盖上一步的ShowDog.prototype = new Dog();
,将表演犬原型指向另外一个对象
因此必须写ShowDog.prototype.xxx = function()
的形式,才能在继承后为原型添加更多方法
将这些属性和方法添加到表演犬原型中,让所有表演犬继承它们
ShowDog.prototype.league = "Webville";
ShowDog.prototype.gait = function(kind) {
console.log(kind + "ing");
};
ShowDog.prototype.groom = function() {
console.log("Groom");
};
修复属性constructor不正确的问题(见后文)
// We do this to make sure the constructor property is correct
ShowDog.prototype.constructor = ShowDog;
最终效果如上图
至此,我们创建了一个原型链
我们说表演犬原型“扩展”了小狗原型:它继承了小狗原型的属性和方法,并添加了一些新属性和方法。
最终可以创建一个新的表演犬实例
var scotty = new ShowDog("Scotty", "Scottish Terrier", 15, "Cookie");
scotty.gait("Walk");
scotty.groom();
instanceof
与constructor
属性
instanceof
指出对象是否是某个构造函数的实例(考虑原型链);
而对象的constructor
属性指出对象是由哪个构造函数创建的
用小狗原型创建一个小狗实例;用表演犬原型创建一个表演犬实例;
下面看输出
var fido = new Dog("Fido", "Mixed", 38);
if (fido instanceof Dog) 表达式为true
if (fido instanceof ShowDog) 表达式为false
console.log(fido.constructor); 输出"function Dog"
var scotty = new ShowDog("Scotty", "Scottish Terrier", 15, "Cookie");
if (scotty instanceof Dog) 表达式为true
if (scotty instanceof ShowDog) 表达式为true
console.log(scotty.constructor); 输出"function Dog"
-
instanceof
不仅考虑当前对象的类型,还考虑它继承的所有对象(考虑原型链)
①显然,fido是小狗实例,不是表演犬实例
②scotty虽然是作为表演犬创建的,但表演犬继承了小狗,因此scotty也是小狗,即:scotty既是小狗实例又是表演犬实例。 -
对象的
constructor
属性指出该对象是通过哪个构造函数创建的
①显然,fido的构造函数是Dog
②然而,scotty的构造函数也是Dog,但实际上它是用构造函数ShowDog创建的,这不合理。
scotty的构造函数也是Dog,到底是怎么回事呢?
这是一个需要修复的漏洞。查看属性scotty.constructor,由于我们没有显式地为表演犬设置这个属性,它将从小狗原型那里继承该属性
换句话说,如果不显式地设置表演犬原型的属性constructor,就没人会这样做。虽然不这样做,一切也都将正常运行,但访问scotty.constructor时,结果将不是预期的ShowDog,让人感到迷惑。
修复属性constructor
不正确的问题
如上面所述的,对于继承其他原型的原型,我们需要正确的设置该原型的constructor
属性,使其正确的指向该原型对应的构造函数
对于这里,就是要将表演犬原型的constructor
属性显式地设置为构造函数ShowDog
// We do this to make sure the constructor property is correct
ShowDog.prototype.constructor = ShowDog;
需要澄清的是,虽然不这样做,代码也能正常运行,但给对象设置正确的构造函数是一种最佳实践,以免有一天另一位开发人员接手这些代码并查看表演犬对象的情况时感到迷惑。
附:对于prototype
的理解
无论创建原型还是原型链,本质就是设置构造函数的prototype
属性
- 构造函数有默认的原型Object,即
prototype
属性默认指向Object - 但也可将
prototype
设置为其他(自定义的)原型,从而获得继承原型的原型
而创建原型链,就是:
- 将
prototype
设置为要继承的原型的实例,ShowDog.prototype = new Dog();
- 然后根据需要添加本原型所需要的一些方法
ShowDog.prototype.xxx = function()
,在继承后为原型添加更多方法
好好理解下面三种情况
- 表演犬原型
ShowDog.prototype
设置为通用小狗实例,从而表演犬原型继承小狗原型 - 表演犬原型
ShowDog.prototype
可以被设置为任意对象,表演犬将继承该对象中定义的属性和方法
例如
function ShowDog2=function(){
this.name = name;
//...
}
ShowDog2.prototype={//将原型设置为某个对象
species:"Canine",
bark:function(){
...}
};
- 表演犬原型
ShowDog.prototype
可以不设置为某个对象,表演犬原型就没有继承任何其他原型(这就是小狗原型的情况)