五、原型
在本章中,您将了解函数对象的 prototype
属性。理解原型如何工作是学习 JavaScript 语言的一个重要部分。毕竟,JavaScript 被归类为具有基于原型的对象模型。原型没有什么特别难的,但它是一个新概念,因此有时可能需要一些时间来理解。这是 JavaScript 中的其中一件事(闭包是另一件事),一旦你“得到”它们,它们就变得如此明显,而且非常有意义。就像这本书的其余部分一样,我们强烈鼓励你输入和玩例子;这使得学习和记忆概念变得容易得多。
本章将讨论以下主题:
-
每个函数都有一个
prototype
属性,它包含一个对象 -
向原型对象添加属性
-
使用添加到原型中的属性
-
自身属性和原型属性的区别
-
__proto__
,每个对象与其原型保持的秘密联系 -
方法如
isPrototypeOf(), hasOwnProperty()
、propertyIsEnumerable()
-
如何增强内置对象,如数组或字符串
原型属性
JavaScript 中的函数是对象,它们包含方法和属性。你已经熟悉的一些方法是 apply()
和 call()
,一些属性是 length
和 constructor
。功能对象的另一个属性是 prototype
。
如果您定义了一个简单的函数 foo()
,您可以像访问任何其他对象一样访问它的属性:
>>> function foo(a, b){return a * b;}
>>> foo.length
2
>>> foo.constructor
Function()
prototype
是一个一旦定义了函数就被创建的属性。它的初始值是一个空对象。
>>> typeof foo.prototype
"object"
就好像你自己这样添加了这个属性:
>>> foo.prototype = {}
您可以用属性和方法来扩充这个空对象。它们不会对 foo()
功能本身产生任何影响;它们只会在你使用 foo()
作为构造函数时使用。
使用原型添加方法和属性
在前一章中,您学习了如何定义构造函数,这些函数可以用来创建(构造)新的对象。主要思想是在用 new
调用的函数中,您可以访问值 this
,它包含构造函数要返回的对象。增强(向 this
对象添加方法和属性)是向正在创建的对象添加功能的方法。
让我们来看看构造函数 Gadget()
,它使用 this
向它创建的对象添加两个属性和一个方法。
function Gadget(name, color) {
this.name = name;
this.color = color;
this.whatAreYou = function(){
return 'I am a ' + this.color + ' ' + this.name;
}
}
向构造函数的 prototype
属性添加方法和属性是向该构造函数生成的对象添加功能的另一种方式。让我们再添加两个属性, price
和 rating
,以及一个 getInfo()
方法。由于 prototype
包含一个对象,您可以像这样继续添加:
Gadget.prototype.price = 100;
Gadget.prototype.rating = 3;
Gadget.prototype.getInfo = function() {
return 'Rating: ' + this.rating + ', price: ' + this.price;
};
而不是添加到 prototype
对象,另一种实现上述结果的方法是完全覆盖原型,用您选择的对象替换它:
Gadget.prototype = {
price: 100,
rating: 3,
getInfo: function() {
return 'Rating: ' + this.rating + ', price: ' + this.price;
}
};
使用原型的方法和属性
一旦使用构造函数创建了一个新对象,您添加到原型中的所有方法和属性都可以直接使用。如果使用 Gadget()
构造函数创建 newtoy
对象,则可以访问已经定义的所有方法和属性。
>>> var newtoy = new Gadget('webcam', 'black');
>>> newtoy.name;
"webcam"
>>> newtoy.color;
"black"
>>> newtoy.whatAreYou();
"I am a black webcam"
>>> newtoy.price;
100
>>> newtoy.rating;
3
>>> newtoy.getInfo();
"Rating: 3, price: 100"
需要注意的是,原型是“活的”。对象在 JavaScript 中通过引用传递,因此原型不会与每个新的对象实例一起复制。这在实践中意味着什么?这意味着您可以随时修改原型,并且所有对象(甚至是那些在修改之前创建的对象)都将继承这些更改。
让我们继续这个例子,向原型添加一个新方法:
Gadget.prototype.get = function(what) {
return this[what];
};
即使 newtoy
是在定义 get()
方法之前创建的, newtoy
仍然可以使用新方法:
>>> newtoy.get('price');
100
>>> newtoy.get('color');
"black"
自有属性与原型属性
在上例中 getInfo()
在内部使用 this
来寻址对象。它也可以用 Gadget.prototype
达到同样的结果:
Gadget.prototype.getInfo = function() {
return 'Rating: ' + Gadget.prototype.rating + ', price: ' + Gadget.prototype.price;
};
有什么区别?为了回答这个问题,让我们更详细地研究原型是如何工作的。
我们再来看看我们的 newtoy
对象:
>>> var newtoy = new Gadget('webcam', 'black');
当你试图访问一个属性 newtoy
,比方说 newtoy.name
时,JavaScript 引擎会在对象的所有属性中寻找一个名为 name
的属性,如果找到它,就会返回它的值。
>>> newtoy.name
"webcam"
如果你试图访问 rating
属性呢?JavaScript 引擎将检查 newtoy
的所有属性,并且不会找到名为 rating
的属性。然后脚本引擎将识别用于创建该对象的构造函数的原型(就像你做 newtoy.constructor.prototype
一样)。如果在原型中找到该属性,则使用该属性。
>>> newtoy.rating
3
这与直接访问原型是一样的。每个对象都有一个构造函数属性,它是对创建该对象的函数的引用,因此在我们的例子中:
>>> newtoy.constructor
Gadget(name, color)
>>> newtoy.constructor.prototype.rating
3
现在让我们把这个查找再向前推进一步。每个对象都有一个构造函数。原型是一个对象,所以它也必须有一个构造函数。它又有一个原型。换句话说,你可以做到:
>>> newtoy.constructor.prototype.constructor
Gadget(name, color)
>>> newtoy.constructor.prototype.constructor.prototype
Object price=100 rating=3
这可能会持续一段时间,这取决于原型链有多长,但您最终会得到内置的 Object()
对象,这是最高级别的父对象。实际上,这意味着如果你尝试 newtoy.toString()
newtoy
没有自己的 toString()
方法,它的原型也没有,最终你会得到对象的 toString()
>>> newtoy.toString()
"[object Object]"
用自己的属性覆盖原型的属性
正如上面的讨论所展示的,如果你的一个对象没有自己的特定属性,它可以在原型链的某个地方使用一个(如果存在的话)。如果对象确实有自己的属性,原型也有一个同名的属性呢?自己的属性优先于原型的属性。
让我们来看一个场景,其中属性名既作为自己的属性存在,也作为原型对象的属性存在:
function Gadget(name) {
this.name = name;
}
Gadget.prototype.name = 'foo';
“foo”
创建一个新的对象并访问其 name
属性可以获得该对象自己的 name
属性。
>>> var toy = new Gadget('camera');
>>> toy.name;
"camera"
如果删除此属性,原型的同名属性将“穿透”:
>>> delete toy.name;
true
>>> toy.name;
"foo"
当然,您总是可以重新创建对象自己的属性:
>>> toy.name = 'camera';
>>> toy.name;
"camera"
枚举属性
如果要列出一个对象的所有属性,可以使用 for-in
循环。在第 2 章中,您看到了如何循环遍历数组的所有元素:
var a = [1, 2, 3];
for (var i in a) {
console.log(a[i]);
}
数组是对象,因此您可以预期 for-in
循环也适用于对象:
var o = {p1: 1, p2: 2};
for (var i in o) {
console.log(i + '=' + o[i]);
}
这会产生:
p1=1
p2=2
有一些细节需要注意:
-
并非所有属性都出现在
for-in
循环中。例如,length
(用于数组)和constructor
属性将不会显示。出现的属性称为可枚举。您可以借助每个对象提供的propertyIsEnumerable()
方法来检查哪些是可枚举的。 -
原型链中的原型也会出现,前提是它们是可枚举的。您可以使用
hasOwnProperty()
方法检查某个属性是自有属性还是原型属性。 -
propertyIsEnumerable()
将返回所有原型属性的false
,甚至是那些可枚举的并且将出现在for-in
循环中的属性。
让我们看看这些方法的实际应用。就拿这个简化版 Gadget():
来说吧
function Gadget(name, color) {
this.name = name;
this.color = color;
this.someMethod = function(){return 1;}
}
Gadget.prototype.price = 100;
Gadget.prototype.rating = 3;
创建新对象:
var newtoy = new Gadget('webcam', 'black');
现在,如果使用 for-in
循环,您会看到对象的所有属性,包括来自原型的属性:
for (var prop in newtoy) {
console.log(prop + ' = ' + newtoy[prop]);
}
结果还包含对象的方法(因为方法只是碰巧是函数的属性):
名称=网络摄像头
颜色=黑色
some method = function(){ return 1;}
价格= 100
额定值= 3
如果要区分对象自身属性和原型属性,请使用 hasOwnProperty()
。先试试:
>>> newtoy.hasOwnProperty('name')
true
>>> newtoy.hasOwnProperty('price')
false
让我们再次循环,但只显示自己的属性:
for (var prop in newtoy) {
if (newtoy.hasOwnProperty(prop)) {
console.log(prop + '=' + newtoy[prop]);
}
}
结果是:
名称=网络摄像头
颜色=黑色
some method = function(){ return 1;}
现在让我们试试 propertyIsEnumerable()
。对于非内置属性,该方法返回 true
:
>>> newtoy.propertyIsEnumerable('name')
true
大多数内置属性和方法不可枚举:
>>> newtoy.propertyIsEnumerable('constructor')
false
原型链中的任何属性都是不可枚举的:
>>> newtoy.propertyIsEnumerable('price')
false
但是,请注意,如果您到达原型中包含的对象并调用其 propertyIsEnumerable()
,则这些属性是可枚举的。
>>> newtoy.constructor.prototype.propertyIsEnumerable('price')
true
ispro type of()
每个对象也得到 isPrototypeOf()
法。此方法告诉您该特定对象是否用作另一个对象的原型。
我们拿一个简单的对象 monkey
来说。
var monkey = {
hair: true,
feeds: 'bananas',
breathes: 'air'
};
现在让我们创建一个 Human()
构造函数,并将它的 prototype
属性设置为指向 monkey
。
function Human(name) {
this.name = name;
}
Human.prototype = monkey;
现在如果你创建一个名为 george
的新 Human
对象,然后问:“是 monkey george's
原型吗?”,你会得到 true
。
>>> var george = new Human('George');
>>> monkey.isPrototypeOf(george)
true
秘密 __ 原型 _ _ 链接
正如您已经知道的,当您试图访问当前对象中不存在的属性时,将会参考原型属性。
让我们再次拥有一个名为 monkey
的对象,并在使用 Human()
构造函数创建对象时将其用作原型。
var monkey = {
feeds: 'bananas',
breathes: 'air'
};
function Human() {}
Human.prototype = monkey;
现在让我们创建一个 developer
对象,并赋予它一些属性:
var developer = new Human();
developer.feeds = 'pizza';
developer.hacks = 'JavaScript';
现在让我们参考一些属性。 hacks
是 developer
对象的属性。
>>> developer.hacks
"JavaScript"
feeds
也可以在对象中找到。
>>> developer.feeds
"pizza"
breathes
并不作为 developer
对象的属性存在,所以原型是向上看的,好像有一个秘密链接指向原型对象。
>>> developer.breathes
"air"
你能从开发者对象到原型对象吗?嗯,你可以,利用 constructor
作为中间人,所以拥有类似 developer.constructor.prototype
的东西应该指向 monkey
。问题是这个不太可靠,因为 constructor
更多的是为了信息的目的,可以随时轻松覆盖。您可以用甚至不是对象的东西覆盖它,这不会影响原型链的正常功能。
让我们将构造函数属性设置为某个字符串:
>>> developer.constructor = 'junk'
"junk"
似乎 prototype
现在全乱了:
>>> typeof developer.constructor.prototype
"undefined"
...但事实并非如此,因为 developer
还是 breathes
T2【空气】:
>>> developer.breathes
"air"
这表明原型的秘密链接仍然存在。这个秘密链接在火狐中被公开为 __proto__
属性(单词“proto”前面有两个下划线,后面有两个下划线)。
>>> developer.__proto__
Object feeds=bananas breathes=air
您可以将此秘密属性用于学习目的,但在您的真实脚本中使用它并不是一个好主意,因为它不存在于 Internet Explorer 中,所以您的脚本不会是可移植的。例如,假设您已经创建了多个以 monkey
为原型的对象,现在您想要更改所有对象中的某些内容。您可以更改 monkey
,所有实例都将继承该更改:
>>> monkey.test = 1
1
>>> developer.test
1
__proto__
不同于 prototype. __proto__
是实例的属性,而 prototype
是构造函数的属性。
>>> typeof developer.__proto__
"object"
>>> typeof developer.prototype
"undefined"
同样,您应该仅将 __proto__
用于学习或调试目的。
扩充内置对象
内置的对象,比如构造函数 Array, String
,甚至 Object
、 Function
都可以通过它们的原型进行扩充,这意味着你可以,比如说,给 Array
原型添加新的方法,这样就可以让所有数组都可以使用它们。我们开始吧。
在 PHP 中有一个名为 in_array()
的函数,它告诉你数组中是否存在一个值。在 JavaScript 中没有 inArray()
方法,所以让我们实现它并将其添加到 Array.prototype
中。
Array.prototype.inArray = function(needle) {
for (var i = 0, len = this.length; i < len; i++) {
if (this[i] === needle) {
return true;
}
}
return false;
}
现在所有的数组都会有新的方法。让我们测试一下:
>>> var a = ['red', 'green', 'blue'];
>>> a.inArray('red');
true
>>> a.inArray('yellow');
false
那很简单!我们再来一次。想象一下,你的应用经常需要反转字符串,你觉得应该有一个内置的 reverse()
方法来处理字符串对象。毕竟阵列有 reverse()
。你可以很容易地将这个 reverse()
方法添加到字符串原型中,借用 Array.prototype.reverse()
(在第 4 章的末尾有一个类似的练习)。
String.prototype.reverse = function() {
return Array.prototype.reverse.apply(this.split('')).join('');
}
这段代码使用 split()
从一个字符串创建一个数组,然后在这个数组上调用 reverse()
方法,产生一个反向数组。使用 join()
将结果数组变回字符串。让我们测试一下新方法:
>>> "Stoyan".reverse();
"nayotS"
扩充内置对象—讨论
通过原型扩充内置对象是一种非常强大的技术,您可以使用它以任何您喜欢的方式来塑造 JavaScript。由于它的力量,在使用这种方法之前,您应该始终彻底考虑您的选择。
以流行的叫做 Prototype 的 JavaScript 库为例。它的创造者非常喜欢这种方法,甚至以它的名字给图书馆命名。使用这个库,您可以使用与 Ruby 语言非常相似的方法来使用 JavaScript。
YUI(雅虎!用户界面)库是另一个流行的 JavaScript 库。它的创建者恰恰相反:他们不会以任何方式修改内置对象。原因是,一旦你知道了 JavaScript,你就会期望它以同样的方式工作,不管你使用的是哪个库。修改核心对象只会混淆库的用户,并产生意外的错误。
事实是,JavaScript 改变了,浏览器出现了支持更多功能的新版本。今天您认为缺少的特性,并决定扩充原型,明天可能是一个内置的方法。在这种情况下,不再需要您的方法。但是,如果您已经编写了大量使用方法的代码,并且您的方法与新的内置实现略有不同,该怎么办?
您至少可以在实现该方法之前检查它是否存在。我们的最后一个例子应该是这样的:
if (!String.prototype.reverse) {
String.prototype.reverse = function() {
return Array.prototype.reverse.apply(this.split('')).join('');
}
}
注
最佳实践
如果您决定用一个新属性来扩充内置对象的原型,请首先检查新属性是否存在。
一些原型陷阱
以下是处理原型时需要考虑的两个有趣的行为:
-
原型链是活动的,除非您完全替换了原型对象
-
prototype.constructor
不可靠
创建简单的构造函数和两个对象:
>>> function Dog(){this.tail = true;}
>>> var benji = new Dog();
>>> var rusty = new Dog();
即使在创建对象之后,您仍然可以向原型添加属性,并且对象可以访问新属性。让我们抛出方法 say():
>>> Dog.prototype.say = function(){return 'Woof!';}
这两个对象都可以访问新方法:
>>> benji.say();
"Woof!"
>>> rusty.say();
"Woof!"
到目前为止,如果您咨询您的对象,询问哪个构造函数被用来创建它们,它们会正确地报告它。
>>> benji.constructor;
Dog()
>>> rusty.constructor;
Dog()
有趣的是,如果问原型对象的构造函数是什么,也会得到 Dog()
,不太正确。原型只是一个用 Object()
创建的普通对象。它不具有用 Dog()
构建的对象的任何属性。
>>> benji.constructor.prototype.constructor
Dog()
>>> typeof benji.constructor.prototype.tail
"undefined"
现在让我们用一个全新的对象完全覆盖原型对象:
>>> Dog.prototype = {paws: 4, hair: true};
原来,我们的旧对象无法访问新原型的属性;他们仍然保留着指向旧原型对象的秘密链接:
>>> typeof benji.paws
"undefined"
>>> benji.say()
"Woof!"
>>> typeof benji.__proto__.say
"function"
>>> typeof benji.__proto__.paws
"undefined"
从现在开始,您创建的任何新对象都将使用更新的原型:
>>> var lucy = new Dog();
>>> lucy.say()
TypeError: lucy.say is not a function
>>> lucy.paws
4
秘密 __proto__
链接指向新的原型对象:
>>> typeof lucy.__proto__.say
"undefined"
>>> typeof lucy.__proto__.paws
"number"
现在,新对象的构造函数属性不再正确报告。它应该指向 Dog()
,而不是指向 Object()
。
>>> lucy.constructor
Object()
>>> benji.constructor
Dog()
最令人困惑的部分是当您查找构造函数的原型时:
>>> typeof lucy.constructor.prototype.paws
"undefined"
>>> typeof benji.constructor.prototype.paws
"number"
下面将修复上述所有意外行为:
>>> Dog.prototype = {paws: 4, hair: true};
>>> Dog.prototype.constructor = Dog;
注
最佳实践
覆盖原型时,最好重置 constructor
属性。
总结
让我们总结一下你在本章中学到的最重要的话题。
-
所有函数都有一个名为
prototype
的属性。最初它包含一个空对象。 -
您可以向原型对象添加属性和方法。你甚至可以用一个你选择的对象完全代替它。
-
当您使用函数作为构造函数(使用
new
)创建对象时,对象将有一个指向其原型的秘密链接,并且可以访问原型的属性作为自己的属性。 -
自己的属性优先于同名原型的属性。
-
使用
hasOwnProperty()
方法区分自身属性和原型属性。 -
有一个原型链:如果你的对象
foo
没有属性bar
,当你做foo.bar
的时候,JavaScript 会寻找原型的bar
属性。如果没有找到,它将继续在原型的原型中搜索,然后原型的原型的原型,并一直向上到最高级别的父级Object
。 -
您可以扩充内置的构造函数,所有对象都会看到您的添加。给
Array.prototype.flip
分配一个函数,所有数组会立刻得到一个flip()
方法,[1,2,3].flip()
。一定要检查您想要添加的方法/属性是否已经存在,这样您就可以对您的脚本进行未来验证。
练习
-
1.创建一个名为
shape
的对象,该对象具有type
属性和getType()
方法。 -
2.定义一个原型为
shape
的Triangle()
构造函数。使用Triangle()
创建的对象应该有三个自己的属性—a, b, c
代表三角形的边。 -
3.给原型增加一个新的方法叫做
getPerimeter()
。 -
4.使用以下代码测试您的实现:
>>> var t = new Triangle(1, 2, 3);
>>> t.constructor
Triangle(a, b, c)
>>> shape.isPrototypeOf(t)
true
>>> t.getPerimeter()
6
>>> t.getType()
"triangle"
-
5.在
t
上循环,只显示自己的属性和方法(没有原型)。 -
6.让这段代码发挥作用:
>>> [1,2,3,4,5,6,7,8,9].shuffle()
[2, 4, 1, 8, 9, 6, 5, 3, 7]
版权属于:月萌API www.moonapi.com,转载请注明出处