JavaScript 构建函数与原型

JavaScript 用一种称为构建函数的特殊函数来定义对象和它们的特征。构建函数非常有用,因为很多情况下您不知道实际需要多少个对象(实例)。构建函数提供了创建您所需对象(实例)的有效方法,将对象的数据和特征函数按需联结至相应对象。

不像“经典”的面向对象的语言,从构建函数创建的新实例的特征并非全盘复制,而是通过一个叫做原形链的参考链链接过去的。

例:

1
2
3
4
5
6
function Person(name) {
this.name = name;
this.greeting = function() {
alert('Hi! I\'m ' + this.name + '.');
};
}

这个构建函数是 JavaScript 版本的类。

注: 一个构建函数通常是大写字母开头,这样便于区分构建函数和普通函数。

利用构建函数构造对象:

1
2
var person1 = new Person('Bob');
var person2 = new Person('Sarah');

这里,当新的对象被创立, 变量 person1person2 有效地包含了以下值:

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
2
3
4
5
6
7
8
function doSomething(){}
console.log( doSomething.prototype );

// It does not matter how you declare the function, a
// function in javascript will always have a default
// prototype property.
var doSomething = function(){};
console.log( doSomething.prototype );

结果

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
3
function 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
5
function 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
15
function 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() 构造器中的成员—— nameagegenderinterestsbiogreeting。同时也有一些其他成员—— watchvalueOf 等等——这些成员定义在 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
2
person1.constructor
person2.constructor

都将返回 Person() 构造器,因为该构造器包含这些实例的原始定义。

一个小技巧是,你可以在 constructor 属性的末尾添加一对圆括号(括号中包含所需的参数),从而用这个构造器创建另一个对象实例。毕竟构造器是一个函数,故可以通过圆括号调用;只需在前面添加 new 关键字,便能将此函数作为构造器使用。

1
var person3 = new person1.constructor('Karen', 'Stephenson', 26, 'female', ['playing drums', 'mountain climbing']);

此外,constructor 属性还有其他用途。比如,想要获得某个对象实例的构造器的名字,可以这么用:

1
2
3
4
instanceName.constructor.name

// 如:
person1.constructor.name // "Person"

修改原型

修改构造器的 prototype 属性,将会动态更新整条继承链,任何由此构造器创建的对象实例都自动更新该属性。

这种继承模型下,上游对象的方法不会复制到下游的对象实例中;下游对象本身虽然没有定义这些方法,但浏览器会通过上溯原型链、从上游对象中找到它们。这种继承模型提供了一个强大而可扩展的功能系统。

一种极其常见的对象定义模式是,在构造器(函数体)中定义属性、在 prototype 属性上定义方法。如此,构造器只包含属性定义,而方法则分装在不同的代码块,代码更具可读性。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 构造器及其属性定义

function Test(a,b,c,d) {
// 属性定义
};

// 定义第一个方法

Test.prototype.x = function () { ... }

// 定义第二个方法

Test.prototype.y = function () { ... }

// 等等……

原型式继承

JavaScript 使用了不同于真正的面向对象语言的另一套实现方式,继承的对象函数并不是通过复制而来,而是通过原型链继承(通常被称为 原型式继承 —— prototypal inheritance)。

示例:
首先,定义一个 Person()构造器

1
2
3
4
5
6
7
8
9
function Person(first, last, age, gender, interests) {
this.name = {
first,
last
};
this.age = age;
this.gender = gender;
this.interests = interests;
};

所有的方法都定义在构造器的原型上,比如:

1
2
3
Person.prototype.greeting = function() {
alert('Hi! I\'m ' + this.name.first + '.');
};

接下来,我们想要创建一个Teacher类,这个类会继承Person的所有成员,同时也包括:

  • 一个新的属性,subject——这个属性包含了教师教授的学科。
  • 一个被更新的greeting()方法,这个方法打招呼听起来比一般的greeting()方法更正式一点——对于一个教授一些学生的老师来说。

我们要做的第一件事就是创建一个Teacher()构造器:

1
2
3
4
5
function 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
13
function 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()prototypeconstructor属性指向的是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 ,这样才不会打乱类继承结构。

-------------本文结束感谢您的阅读-------------
0%