JavaScript 用一种称为构建函数的特殊函数来定义对象和它们的特征。构建函数非常有用,因为很多情况下您不知道实际需要多少个对象(实例)。构建函数提供了创建您所需对象(实例)的有效方法,将对象的数据和特征函数按需联结至相应对象。
不像“经典”的面向对象的语言,从构建函数创建的新实例的特征并非全盘复制,而是通过一个叫做原形链的参考链链接过去的。
例:1
2
3
4
5
6function Person(name) {
this.name = name;
this.greeting = function() {
alert('Hi! I\'m ' + this.name + '.');
};
}
这个构建函数是 JavaScript 版本的类。
注: 一个构建函数通常是大写字母开头,这样便于区分构建函数和普通函数。
利用构建函数构造对象:1
2var person1 = new Person('Bob');
var person2 = new Person('Sarah');
这里,当新的对象被创立, 变量 person1 与 person2 有效地包含了以下值:1
2
3
4
5
6
7
8
9
10
11
12
13{
name : 'Bob',
greeting : function() {
alert('Hi! I\'m ' + this.name + '.');
}
}
{
name : 'Sarah',
greeting : function() {
alert('Hi! I\'m ' + this.name + '.');
}
}
之所以说是“有效”, 是因为实际的方法仍然是定义在类里面, 而不是在对象实例里面。
基于原型的语言
JavaScript 常被描述为一种基于原型的语言 (prototype-based language)——每个对象拥有一个原型对象,对象以其原型为模板、从原型继承方法和属性。原型对象也可能拥有原型,并从中继承方法和属性,一层一层、以此类推。这种关系常被称为原型链 (prototype chain),它解释了为何一个对象会拥有定义在其他对象中的属性和方法。
准确地说,这些属性和方法定义在Object的构造器函数(constructor functions)之上的prototype属性上,而非对象实例本身。
在传统的 OOP 中,首先定义“类”,此后创建对象实例时,类中定义的所有属性和方法都被复制到实例中。在 JavaScript 中并不如此复制——而是在对象实例和它的构造器之间建立一个链接(它是__proto__属性,是从构造函数的prototype属性派生的),之后通过上溯原型链,在构造器中找到这些属性和方法。
注意: 理解对象的原型(可以通过Object.getPrototypeOf(obj)或者已被弃用的
__proto__属性获得)与构造函数的prototype属性之间的区别是很重要的。前者是每个实例上都有的属性,后者是构造函数的属性。也就是说,Object.getPrototypeOf(new Foobar())和Foobar.prototype指向着同一个对象。
JavaScript中的原型
在javascript中,函数可以有属性。 每个函数都有一个特殊的属性叫作原型(prototype)。
1 | function doSomething(){} |
结果1
2
3
4
5
6
7
8
9
10
11
12{
constructor: ƒ doSomething(),
`__proto__`: {
constructor: ƒ Object(),
hasOwnProperty: ƒ hasOwnProperty(),
isPrototypeOf: ƒ isPrototypeOf(),
propertyIsEnumerable: ƒ propertyIsEnumerable(),
toLocaleString: ƒ toLocaleString(),
toString: ƒ toString(),
valueOf: ƒ valueOf()
}
}
现在,我们可以添加一些属性到 doSomething 的原型上面,如下所示.1
2
3function doSomething(){}
doSomething.prototype.foo = "bar";
console.log( doSomething.prototype );
结果1
2
3
4
5
6
7
8
9
10
11
12
13{
foo: "bar",
constructor: ƒ doSomething(),
`__proto__`: {
constructor: ƒ Object(),
hasOwnProperty: ƒ hasOwnProperty(),
isPrototypeOf: ƒ isPrototypeOf(),
propertyIsEnumerable: ƒ propertyIsEnumerable(),
toLocaleString: ƒ toLocaleString(),
toString: ƒ toString(),
valueOf: ƒ valueOf()
}
}
然后,我们可以使用 new 运算符来在现在的这个原型基础之上,创建一个 doSomething 的实例。正确使用 new 运算符的方法就是在正常调用函数时,在函数名的前面加上一个 new 前缀. 通过这种方法,在调用函数前加一个 new ,它就会返回一个这个函数的实例化对象. 然后,就可以在这个对象上面添加一些属性。1
2
3
4
5function doSomething(){}
doSomething.prototype.foo = "bar"; // add a property onto the prototype
var doSomeInstancing = new doSomething();
doSomeInstancing.prop = "some value"; // add a property onto the object
console.log( doSomeInstancing );
结果:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16{
prop: "some value",
`__proto__`: {
foo: "bar",
constructor: ƒ doSomething(),
`__proto__`: {
constructor: ƒ Object(),
hasOwnProperty: ƒ hasOwnProperty(),
isPrototypeOf: ƒ isPrototypeOf(),
propertyIsEnumerable: ƒ propertyIsEnumerable(),
toLocaleString: ƒ toLocaleString(),
toString: ƒ toString(),
valueOf: ƒ valueOf()
}
}
}
从以上可以看到: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.
理解原型对象
首先定义一个构造器函数:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15function Person(first, last, age, gender, interests) {
this.name = {
first,
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 + '.');
};
};
然后创建一个对象实例:1
var person1 = new Person('Bob', 'Smith', 32, 'male', ['music', 'skiing']);
在 JavaScript 控制台输入 “person1.”,你会看到,浏览器将根据这个对象的可用的成员名称进行自动补全。在这个列表中,你可以看到定义在 person1 的原型对象、即 Person() 构造器中的成员—— name、age、gender、interests、bio、greeting。同时也有一些其他成员—— watch、valueOf 等等——这些成员定义在 Person() 构造器的原型对象、即 Object 之上。下图展示了原型链的运作机制。
那么,调用 person1 的“实际定义在 Object 上”的方法时,会发生什么?比如:1
person1.valueOf()
这个方法仅仅返回了被调用对象的值。在这个例子中发生了如下过程:
浏览器首先检查,person1对象是否具有可用的valueOf()方法。- 如果没有,则浏览器检查
person1对象的原型对象(即Person构造函数的prototype属性所指向的对象)是否具有可用的valueof()方法。 - 如果也没有,则浏览器检查
Person()构造函数的prototype属性所指向的对象的原型对象(即Object构造函数的prototype属性所指向的对象)是否具有可用的valueOf()方法。这里有这个方法,于是该方法被调用。
注意: 原型链中的方法和属性没有被复制到其他对象——它们被访问需要通过前面所说的“原型链”的方式。
注意: 没有官方的方法用于直接访问一个对象的原型对象,然而,大多数现代浏览器还是提供了一个名为
__proto__(前后各有2个下划线)的属性,其包含了对象的原型。
prototype 属性:继承成员被定义的地方
继承的属性和方法是定义在 prototype 属性之上的, prototype 属性的值是一个对象,我们希望被原型链下游的对象继承的属性和方法,都被储存在其中。
constructor 属性
每个实例对象都从原型中继承了一个constructor属性,该属性指向了用于构造此实例对象的构造函数。
如:1
2person1.constructor
person2.constructor
都将返回 Person() 构造器,因为该构造器包含这些实例的原始定义。
一个小技巧是,你可以在 constructor 属性的末尾添加一对圆括号(括号中包含所需的参数),从而用这个构造器创建另一个对象实例。毕竟构造器是一个函数,故可以通过圆括号调用;只需在前面添加 new 关键字,便能将此函数作为构造器使用。
1 | var person3 = new person1.constructor('Karen', 'Stephenson', 26, 'female', ['playing drums', 'mountain climbing']); |
此外,constructor 属性还有其他用途。比如,想要获得某个对象实例的构造器的名字,可以这么用:1
2
3
4instanceName.constructor.name
// 如:
person1.constructor.name // "Person"
修改原型
修改构造器的 prototype 属性,将会动态更新整条继承链,任何由此构造器创建的对象实例都自动更新该属性。
这种继承模型下,上游对象的方法不会复制到下游的对象实例中;下游对象本身虽然没有定义这些方法,但浏览器会通过上溯原型链、从上游对象中找到它们。这种继承模型提供了一个强大而可扩展的功能系统。
一种极其常见的对象定义模式是,在构造器(函数体)中定义属性、在 prototype 属性上定义方法。如此,构造器只包含属性定义,而方法则分装在不同的代码块,代码更具可读性。例如:
1 | // 构造器及其属性定义 |
原型式继承
JavaScript 使用了不同于真正的面向对象语言的另一套实现方式,继承的对象函数并不是通过复制而来,而是通过原型链继承(通常被称为 原型式继承 —— prototypal inheritance)。
示例:
首先,定义一个 Person()构造器1
2
3
4
5
6
7
8
9function Person(first, last, age, gender, interests) {
this.name = {
first,
last
};
this.age = age;
this.gender = gender;
this.interests = interests;
};
所有的方法都定义在构造器的原型上,比如:1
2
3Person.prototype.greeting = function() {
alert('Hi! I\'m ' + this.name.first + '.');
};
接下来,我们想要创建一个Teacher类,这个类会继承Person的所有成员,同时也包括:
- 一个新的属性,
subject——这个属性包含了教师教授的学科。 - 一个被更新的
greeting()方法,这个方法打招呼听起来比一般的greeting()方法更正式一点——对于一个教授一些学生的老师来说。
我们要做的第一件事就是创建一个Teacher()构造器:1
2
3
4
5function Teacher(first, last, age, gender, interests, subject) {
Person.call(this, first, last, age, gender, interests);
this.subject = subject;
}
call()函数:允许您调用一个在这个文件里别处定义的函数。
从无参构造函数继承
如果您继承的构造函数不从传入的参数中获取其属性值,则不需要在call()中为其指定其他参数。1
2
3
4
5
6
7
8
9
10
11
12
13function Brick() {
this.width = 10;
this.height = 20;
}
// 继承width和height属性
function BlueGlassBrick() {
Brick.call(this);
this.opacity = 0.5;
this.color = 'blue';
}
此时,我们已经定义了一个新的构造器,这个构造器默认有一个空的原型属性。我们需要让Teacher()从Person()的原型对象里继承方法。
先加上下面一行:1
Teacher.prototype = Object.create(Person.prototype);
我们用create()函数来创建一个和Person.prototype一样的新的原型属性值(这个属性指向一个包括属性和方法的对象),然后将其作为Teacher.prototype的属性值。这意味着Teacher.prototype现在会继承Person.prototype的所有属性和方法。
现在Teacher()的prototype的constructor属性指向的是Person(), 这是因为我们生成Teacher()的方式决定的。
我们需要加上一行代码:1
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 ,这样才不会打乱类继承结构。