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
,这样才不会打乱类继承结构。