什么是原型链?
答: 所有的JS对象都有一个prototype属性,指向它的原型对象。当试图访问一个对象的属性时,如果没有在该对象找到,它还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。
对象基础
对象是一个包含相关数据和方法的集合(通常由一些变量和函数组成,我们称之为对象里面的属性和方法)
<script>
var person = {
name : ['Bob', 'Smith'],
age : 32,
gender : 'male',
interests : ['music', 'skiing'],
bio : function() {
alert(this.name[0] + ' ' + this.name[1] + ' is ' + this.age + ' years old. He likes ' + this.interests[0] + ' and ' + this.interests[1] + '.');
},
greeting: function() {
alert('Hi! I\'m ' + this.name[0] + '.');
}
}
</script>
对象的值可以是任意的,我们的person对象里有字符串(string),数字(number),两个数组(array),两个函数(function)。前四个成员是资料项目,被称为对象的属性(property),后两个成员是函数,允许对象对资料做一些操作,被称为对象的方法(method)。
面向对象
面向对象的程序(Object-oriented programming)的基本思想是:在程序里,我们通过使用对象去构建现实世界的模型,把原本很难(或不可)能被使用的功能,简单化并提供出来,以供访问。
在一些面向对象的语言中,我们用类(class)的概念去描述一个对象(在下面就能看到JavaScript使用了一个完全不同的术语),类并不完全是一个对象,它更像是一个定义对象特质的模板。
当一个对象需要从类中创建出来时,类的构造函数就会运行来创建这个实例。这种创建对象的过程我们称之为实例化-实例对象被类实例化。
构造函数JavaScript用一种称为构造函数的特殊函数来定义对象和它们的特征,构造函数非常有用因为很多情况下不知道实际需要多少个对象(实例)。构建函数提供了创建所需对象(实例)的有效方法,将对象的数据和特征函数按需联结至相应对象。
<script>
function Person(name){
this.name = name;
this.greeting = function(){
alert('Hi! I\'m ' + this.name + '.');
};
}
</script>
基于原型的语言
JavaScript常被描述为一种基于原型的语言(prototype-type-language)——每个对象拥有一个原型对象,对象以其原型为模板、从原型机城方法和属性。原型对象也可能拥有原型,并从中集成方法和属性,一层一层、以此类推。这种关系常被称为原型链(prototype chain),它解释为何一个对象会拥有定义在其他对象中的属性和方法。
准确地说,这些属性和方法定义在Object的构造器函数(constructor functions)之上的prototype属性上,而非对象实例本身。
在传统的OOP中,首先定义“类”,此后创建对象实例时,类中定义的所有属性和方法都被复制到实例中。在JavaScript中并不如此复制——而是在对象实例和它的构造器之间建立一个连接(它是_proto_属性,是从构造函数的prototype属性派生的),之后通过上溯原型链,在构造器中找到这些属性和方法。
在javascript中,函数可以有属性。每个函数都有一个特殊的属性叫做原型(prototype),正如下面所展示的。
正如上面所看到的,doSomething函数有一个默认的原型函数,它在控制台上面呈现了出来,运行这段代码之后,控制台上面应该出现了先这样的一个对象。
现在,我们可以添加一些属性到doSomething的原型上面,如下所示。
结果:
然后,我们可以使用new运算符来在现在的这个原型基础上,创建一个doSomething的实例。正确使用new运算符的方法就是在正常调用函数时,在函数名的前面加上一个new前缀。通过这种方法,在调用函数前加上一个new,它就会返回一个返回函数的实例化对象。然后,就可以在这个对象上面添加一些属性。
就像上面看到的, doSomeInstancing 的 proto 属性就是doSomething.prototype. 但是这又有什么用呢? 好吧,当你访问 doSomeInstancing 的一个属性, 浏览器首先查找 doSomeInstancing 是否有这个属性. 如果 doSomeInstancing 没有这个属性, 然后浏览器就会在 doSomeInstancing 的 proto 中查找这个属性(也就是 doSomething.prototype). 如果 doSomeInstancing 的 proto 有这个属性, 那么 doSomeInstancing 的 proto 上的这个属性就会被使用. 否则, 如果 doSomeInstancing 的 proto 没有这个属性, 浏览器就会去查找 doSomeInstancing 的 proto 的 proto ,看它是否有这个属性. 默认情况下, 所有函数的原型属性的 proto 就是 window.Object.prototype. 所以 doSomeInstancing 的 proto 的 proto (也就是 doSomething.prototype 的 proto (也就是 Object.prototype)) 会被查找是否有这个属性. 如果没有在它里面找到这个属性, 然后就会在 doSomeInstancing 的 proto 的 proto 的 proto 里面查找. 然而这有一个问题: doSomeInstancing 的 proto 的 proto 的 proto 不存在. 最后, 原型链上面的所有的 proto 都被找完了, 浏览器所有已经声明了的 proto 上都不存在这个属性,然后就得出结论,这个属性是 undefined.
理解原型对象
让我们回到Person()构造器的例子。
<script>
function Person(first,last,age,gender,interests){
this.name = {
'first':first,
'last':last
};
this.age = age;
this.gender = gender;
this.interests = interests;
this.bio = function(){
alert(this.name.first + ' ' + this.name.last + ' is ' + this.age + ' years old. He likes ' + this.interests[0] + ' and ' + this.interests[1] + '.');
};
this.greeting = function() {
alert('Hi! I\'m ' + this.name.first + '.');
};
}
var person1 = new Person('Bob', 'Smith', 32, 'male', ['music', 'skiing']);
</script>
在JavaScript控制台输入“person1.”,你会看到,浏览器将根据这个对象的可用的成员名称进行自动补全:
在这个列表,你可以看到定义在person1的原型对象、即Person()构造器中的成员——name、age、gender、interests、bio、greeting。同时也有一些其他成员——watch、valueOf等等——这些成员定义在Person()构造器的原型对象、即Object之上。下图展示了原型链的运作机制。
那么,调用person1的实际定义在Object上的方法时,会发生什么?比如:
person1.ValueOf()
这个方法仅仅返回了被调用对象的值。在这个例子中发生了如下过程:
- 浏览器首先检查,person1 对象是否具有可用的 valueOf() 方法。
- 如果没有,则浏览器检查 person1 对象的原型对象(即 Person构造函数的prototype属性所指向的对象)是否具有可用的 valueof() 方法。
- 如果也没有,则浏览器检查 Person() 构造函数的prototype属性所指向的对象的原型对象(即 Object构造函数的prototype属性所指向的对象)是否具有可用的 valueOf() 方法。这里有这个方法,于是该方法被调用。
原型式的继承
在经典的面向对象语言中,可能倾向于定义类对象,然后可以简单地定义那些类继承哪些类,JavaScript使用了另一套实现方式,继承的对象函数并不是通过复制而来,而是通过原型链继承(通常被称为原型式继承——prototypal inheritance).
开始
首先。定义了一个只有一些属性的Person()构造器,与之前通过模块来实现所有功能的Person的Person的构造器类似。
<script>
function Person(first,last,age,gender,interests){
this.name = {
first,
last
};
this.age = age;
this.gender = gender;
this.interests = interests;
};
</script>
所有的方法都定义在构造器的原型上,比如:
Person.prototype.greeting = function(){
console.log(`Hi!,I'm${this.name.first + this.name.last}`)
}
比如我们想要创建一个Teacher类,就想我们前面在面向对象概念解释时用的那个一样。这个类会继承Person的所有成员,同时也包括:
- 一个新的属性,subject——这个属性包含了教师教授的科目。
- 一个被更新的greeting()方法,这个方法打招呼听起来比一般的greeting()方法更正式一点——对于一个教授一些学生来说。
定义Teacher()构造器函数
我们要做的第一件事是创建一个Teacher()构造器——将下面的代码加入到现有代码之下:
function Teacher(first,last,age,gender,interests,subject){
Person.call(this, first, last, age, gender, interests);
this.subject = subject;
}
这在很多方面看起来都和Person的构造器很像,但是这里有一些我们从见过的奇怪玩意——call()函数。基本上,这个函数允许您调用一个在这个文件里别处定义的函数。第一个参数指明了在运行这个函数时想对“this”指定的值,也就是说,您可以重新指定您调用的函数里所有“this”指向的对象。其他的变量指明了所有目标函数运行时接受的参数。
在这个例子里,我们很有效的在Teacher()构造函数里运行了Person()构造函数(见上文),得到了和在Teacher()里定义的一样的属性,但是用的是传送给Teacher(),而不是Person()的值,我们简单使用这里的this作为传给call()的this,意味着this指向Teacher()函数。
设置Teacher()的原型和构造器引用
到目前为止一切看起来都还行,但是我们遇到问题了。我们已经定义了一个新的构造器,这个构造器默认有一个空的原型属性。我们需要让Teacher()从Person()的原型对象里继承方法。我们要怎么做呢?
1.在先前添加的代码的下面增加以下一行:
Teacher.prototype = Object.create(Person.prototype)
这里我们的老朋友create()又来帮忙了——在这个例子里我们用这个函数来创建一个和Person.prototype一样的新的原型属性值(这个属性指向一个包含属性和方法的对象),然后将其作为Teacher.prototype的属性值。这意味着Teacher.prototype现在会继承Person.prototype的所有属性和方法。
2.接下来,在我们动工之前,还需要完成一件事——现在Teacher()的prototype的constructor属性指向的是Person(),这是由我们生产Teacher()的方式决定的。
Teacher.prototype.constructor
这或许会成为很大的问题,所以我们需要将其正确设置——可以回到源代码,在底下加上这一行来解决:
Teacher.prototype.constructor = Teacher;
当您保存并刷新页面以后,输入Teacher.prototype.constructor就会得到Teacher()。
**注:**每个函数对象(Function)都有一个prototype属性,并且只有函数对象有prototype属性,因为prototype本身就是定义在Function对象下的属性。当我们输入类似var person1 = new Person()来构造对象时,JavaScript实际上参考的是Person.prototype指向的对象来生成person1。另一方面,person()函数是Person.prototype的构造函数,也就是说Person==Person.prototype.constructor。
在定义新的构造函数Teacher时,我们通过function.call来调用父类的构造函数,但是这样无法自动指定Teacher.prototype的值,这样Teacher.prototype就只能包含在构造函数里构造的属性,而没有方法。因此我们利用Object.create()方法将Person.prototype作为Teacher.prototype的原型对象,并改变其构造器指向,使之与Teacher关联。
任何想要被继承的方法都应该定义在构造函数的prototype对象里,并且永远使用父类的prototype来创造子类的prototype,这样才不会打乱类继承结构。
向Teacher()添加一个新的greeting()函数
为了完善代码,您还需要在构造函数Teacher()上定义一个新的函数greeting()。最简单的方法是在Teacher的原型上定义它。
最终代码示例
<script>
function Person(first,last,age,gender,interests){
this.name = {
first,
last
};
this.age = age;
this.gender = gender;
this.interests = interests;
};
Person.prototype.greeting = function(){
console.log(`Hi!,I'm${this.name.first + this.name.last}`)
}
var person1 = new Person("ji","mengda",24,"male","sleep")
console.log(person1.name)
person1.greeting()
function Teacher(first,last,age,gender,interests,subject){
Person.call(this, first, last, age, gender, interests);
this.subject = subject;
}
Teacher.prototype = Object.create(Person.prototype)
Teacher.prototype.constructor = Teacher;
Teacher.prototype.greeting = function(){
var prefix;
if(this.gender === 'male' || this.gender === 'Male' || this.gender === 'm' || this.gender === 'M'){
prefix = 'Mr.';
}else if(this.gender === 'female' || this.gender === 'Female' || this.gender === 'f' || this.gender === 'F'){
prefix = 'Mrs.'
}else{
prefix = 'Mx.'
}
alert('Hello. My name is' + prefix + '' + this.name.last + ',and I teach' + this.subject + '.')
}
var teacher1 = new Teacher('Dave', 'Griffiths', 31, 'male', ['football', 'cookery'], 'mathematics');
teacher1.name.first;
teacher1.interests[0];
teacher1.bio();
teacher1.subject;
teacher1.greeting();
</script>
前面三个进入到Person()的构造器继承的属性和方法,后面两个则是只有Teacher()的构造器才有的属性和方法。
这里讲的技巧并不是JavaScript中创建继承类的唯一方式,但是这个技巧也不错,非常好的告诉了如何在JavaScript中实现继承操作。