Javascript构造函数和原型
原文:http://tobyho.com/2010/11/22/javascript-constructors-and/
相信你已经知道了,Javascript函数也可以作为对象构造器。比如,为了模拟面向对象编程中的Class
,可以用如下的代码
function Person(name){
this.name = name
}
*注意:我不使用分号因为我是个异教徒! *
不管怎么说,你现在有了一个function
,你可以使用new
操作符来创建一个Person
var bob = new Person('Bob')
// {name: 'Bob'}
为了确认bob
确实是一个Person
,可以这么做
bob instanceof Person
// true
你同样可以把Person
作为一个普通函数调用——不使用new
Person('Bob')
// undefined
但是这里会返回undefined
.同时,你在不经意间创建了一个全局变量name
,这可不是你想要的。
name
// 'Bob'
嗯...这一点也不好,特别是如果你已经有一个名为name
的全局变量,那么它将会被覆盖。这是因为你直接调用了一个函数(不适用new
),this
对象被设置为全局对象——在浏览器中,就是window
对象。
window.name
// 'Bob'
this === window
// true
所以...如果你想写一个构造器函数,那么就用构造器的方式使用它(使用new
),如果你想写一个普通函数,那么就以函数的方式使用它(直接调用),不要相互混淆。
注:一个较好的代码习惯就是,构造器函数首字母大写,普通函数首字母小写。如
function Person(){}
是一个构造器函数,function showMsg(){}
是一个普通函数。
有些人也许会指出,可以使用一个小技巧避免污染全局变量。
function Person(name){
if (!(this instanceof Person))
return new Person(name)
this.name = name
}
这段代码做了三件事
- 检查
this
对象是否是Person
的实例——如果使用new
操作符的话就是。 - 如果它确实是
Person
的实例,执行原有的代码。 - 如果它不是
Person
的实例,使用new
操作符创建一个Person
的实例——这才是正确的使用姿势,然后返回它。
这就允许使用函数形式调用构造器函数,返回一个Person
对象,不会污染全局命名空间。
Person('Bob')
// {name: 'Bob'}
name
// undefined
神奇的是使用new
操作符同样可行
new Person('Bob')
// {name: 'Bob'}
为什么呢?这是因为当你使用new操作符创建一个对象时,如果你在构造函数里面主动返回一个对象,那么new表达式的值就是这个返回的对象;如果没有主动返回,那么构造函数会默认返回this
。但是,你可能会想,我可不可以返回一个非Person
对象呢?这就有点像欺诈了~
function Cat(name){
this.name = name
}
function Person(name){
return new Cat(name)
}
var bob = new Person('Bob')
bob instanceof Person
// false
bob instanceof Cat
// true
所以,我创建一个Person
结果我得到了一个Cat
?好吧,在Javascript中这确实可能发生。你甚至可以返回一个Array
function Person(name){
return [name]
}
new Person('Bob')
// ['Bob']
但是这有一个限制,如果你返回一个原始数据类型,返回值将不起作用。
function Person(name){
this.name = name
return 5
}
new Person('Bob')
// {name: 'Bob'}
Number
,String
,Boolean
,都是原始数据类型。
如果你在构造器函数里面返回这些类型的值,那么它将会被忽略,构造器将按照正常情况,返回this
对象。
注:原始数据类型还包含
undefined
和null
。但如果你使用new
操作符创建原始数据类型,它将会是一个对象
typeof (new String('hello')) === 'object' // true
typeof (String('hello')) === 'string' // true
方法
在最开始的时候我,我说过函数也可以作为构造器,事实上,它更像身兼三职。函数同样可以作为方法。
如果你了解面向对象编程的话,你会知道方法是对象的行为——描述对象可以做什么。在Javascript中,方法就是链接到对象上的函数——你可以通过创建一个函数并把它赋值到对象上,来创建对象的方法。
function Person(name){
this.name = name
this.sayHi = function(){
return 'Hi, I am ' + this.name
}
}
Bob现在可以say Hi了!
var bob = new Person('Bob')
bob.sayHi()
// 'Hi, I am Bob'
事实上,我们可以脱离构造函数,创建对象的方法
var bob = {name: 'Bob'} // this is a Javascript object!
bob.sayHi = function(){
return 'Hi, I am ' + this.name
}
这同样可行。或者,如果你喜欢的话,把它写成一个更大的object
var bob = {
name: 'Bob',
sayHi: function(){
return 'Hi, I am ' + this.name
}
}
所以,我们为什么还需要构造函数呢?答案是继承。
原型和继承
好吧,我们谈谈继承。你肯定知道继承,对吧?比如在Java中,你可以让一个类继承另一个类,就可以自动得到所有父类的方法和变量了。
public class Mammal{
public void breathe(){
// do some breathing
}
}
public class Cat extends Mammal{
// now cat too can breathe!
}
那么,在Javascript中,我们可以做同样的事情,只是有些不同。首先,我们甚至没有类!取而代之的是prototype
。下面就是与Java代码等价的Javascript代码。
function Mammal(){
}
Mammal.prototype.breathe = function(){
// do some breathing
}
function Cat(){
}
Cat.prototype = new Mammal()
Cat.prototype.constructor = Cat
// now cat too can breathe!
Javascript不同于传统的面向对象语言,它使用原型继承。简而言之,原型继承的工作原理如下:
- 一个对象有许多属性,包含普通属性和函数。
- 一个对象有一个特殊的父属性,它也被称为这个对象的原型,用
__proto__
表示。这个对象可以继承它父对象的所有属性。 - 一个对象可以通过在自身设置属性,重写父对象的的同名属性
- 构造器用于创建对象。每一个构造器都有一个相关联的
prototype
对象,它其实也是一个普通对象。 - 创建一个对象时,该对象的父对象(
__proto__
)被设置为创建它的构造器的prototype
对象。
好的!现在你应该明白原型继承是怎么一回事了,接下来我们一行一行看Cat
这个例子
首先,我们创建了一个构造器Mammal
function Mammal(){
}
这时候,Mammal
已经有了一个prototype
属性
Mammal.prototype
// {}
我们创建一个实例
var mammal = new Mammal()
现在,我们验证一下上面提到的第2条
mammal.__proto__ === Mammal.prototype
// true
接下来,我们在Mammal
的prototype
属性上增加一个方法breathe
Mammal.prototype.breathe = function(){
// do some breathing
}
这时候,实例mammal
就可以调用breathe了
mammal.breathe()
因为它从Mammal.prototype
继承过来。往下
function Cat(){
}
Cat.prototype = new Mammal()
我们创建了一个Cat
构造器,设置Cat.prototype
为Mammal
的实例。为什么要这么做呢?
var garfield = new Cat()
garfield.breathe()
现在所有的cat实例都继承自Mammal
,所以它也能够调用breathe
方法,往下
Cat.prototype.constructor = Cat
确保cat确实是Cat
的实例
garfield.__proto__ === Cat.prototype
// true
Cat.prototype.constructor === Cat
// true
garfield instanceof Cat
// true
每当你创建一个Cat
的实例,你就会创建一个二级原型链,即garfield
是Cat.prototype
的子对象,而Cat.prototype
为Mammal
的实例,所以也是Mammal.prototype
的子对象。
那么,Mammal.prototype
的父对象是谁呢?没错,你也许猜到了,那就是Object.prototype
。所以,实际上是三级原型链。
garfield -> Cat.prototype -> Mammal.prototype -> Object.prototype
你可以在garfield
的父对象上增加属性,然后garfield
就可以神奇的访问到这些属性,即使在garfield
对象创建之后!
Cat.prototype.isCat = true
Mammal.prototype.isMammal = true
Object.prototype.isObject = true
garfield.isCat // true
garfield.isMammal // true
garfield.isObject // true
你也可以知道它是否有某个属性
'isMammal' in garfield
// true
并且你也可以区分自身的属性和继承而来的属性
garfield.name = 'Garfield'
garfield.hasOwnProperty('name')
// true
garfield.hasOwnProperty('breathe')
// false
在原型上创建方法
现在你应该理解了原型继承的原理,让我们回到第一个例子
function Person(name){
this.name = name
this.sayHi = function(){
return 'Hi, I am ' + this.name
}
}
直接在对象上定义方法是一种低效率的方式。一个更好的方法是在Person.prototype
上定义方法。
function Person(name){
this.name = name
}
Person.prototype.sayHi = function(){
return 'Hi, I am ' + this.name
}
为什么这种方式更好?
在第一种方式中,每当我们创建一个person
对象,一个新的sayHi
方法就要被创建,而在第二种方式中,只有一个sayHi
方法被创建了,并且在所有Person
的实例中共享——这是因为Person.prototype
是它们的父对象。所以,在prototype
上创建方法会更加高效。
Apply & Call
正如你所见,函数凭借添加到对象上而成为了一个对象的方法,那么这个函数内的this
指针应该始终指向这个对象,不是么?事实并不是这样。我们看看之前的例子。
function Person(name){
this.name = name
}
Person.prototype.sayHi = function(){
return 'Hi, I am ' + this.name
}
你创建两个Person
对象,jack
和jill
var jack = new Person('Jack')
var jill = new Person('Jill')
jack.sayHi()
// 'Hi, I am Jack'
jill.sayHi()
// 'Hi, I am Jill'
在这里,sayHi
方法不是添加在jack
或者jill
对象上的,而是添加在他们的原型对象上:Person.prototype
。那么,sayHi
方法如何知道jack
和jill
的名字呢?
答案:
this
指针没有绑定到任何对象上,直到函数被调用时才进行绑定。
当你调用jack.sayHi()
时,sayHi
的this
指针就会绑定到jack上;当你调用jill.sayHi()
是,它则会绑定到jill
上。但是,绑定this
对象不改变方法本身——它还是同样的一个函数!
你同样可以为一个方法指定所要绑定的this指针的对象。
function sing(){
return this.name + ' sings!'
}
sing.apply(jack)
// 'Jack sings!'
apply
方法属于Function.prototype
(没错,函数也是一个对象并且有prototypes
和自身的属性!)。所以,你可以在任何函数中使用apply
方法绑定this
指针为指定的对象,即使这个函数没有添加到这个对象上。事实上,你甚至可以绑定this
指针为不同的对象。
function Flower(name){
this.name = name
}
var tulip = new Flower('Tulip')
jack.sayHi.apply(tulip)
// 'Hi, I am Tulip'
你可能会说
等等,郁金香怎么会说话呢!
我可以回答你
任何人是任何事,任何事是任何人,颤抖吧人类@_@
只要这个对象有一个name
属性,sayHi
方法就会很乐意把它打印出。这就是鸭子类型准则
如果一个东西像鸭子一样嘎嘎叫,并且它走起来像鸭子一样,对我来说它就是鸭子!
那么回到apply
函数:如果你想使用apply
传递参数,你可以把它们构造成一个数组作为第二个参数。
function singTo(other){
return this.name + ' sings for ' + other.name
}
singTo.apply(jack, [jill])
// 'Jack sings for Jill'
Function.prototype
也有call
函数,它和apply
函数非常相似,唯一的区别就是call
函数依次把参数列在末尾传递,而apply
函数接收一个数组作为第二个参数。
sing.call(jack, jill)
// 'Jack sings for Jill'
new方法
现在,有趣的事情来了。
当你想调用一个有若干个参数的函数时,apply
方法十分的方便。比如,Math.max
方法接受若干个number参数
Math.max(4, 1, 8, 9, 2)
// 9
这很好,但是不够抽象。我们可以使用apply
获取到任意数组的最大值。
Math.max.apply(Math, myarray)
这有用多了!
既然apply
这么有用,你可能会在很多地方想使用它,比起
Math.max.apply(Math, args)
你可能更想在构造器函数中使用
new Person.apply(Person, args)
遗憾的是,这不起作用。它会认为你把Person.apply
整体当做了构造函数。那么这样呢?
(new Person).apply(Person, args)
这同样也不起作用,因为他会首先创建一个person
对象,然后在尝试调用apply
方法。
怎么办呢?StackOverflow上的这个回答是个好主意
我们可以在Function.prototype
上创建一个new方法
Function.prototype.new = function(){
var args = arguments
var constructor = this
function Fake(){
constructor.apply(this, args)
}
Fake.prototype = constructor.prototype
return new Fake
}
这样,所有的构造器函数都有一个new
方法
var bob = Person.new('Bob')
我们分析一下new
方法的原理
首先
var args = arguments
var constructor = this
function Fake(){
constructor.apply(this, args)
}
我们创建了一个Fake
构造器,在constructor
上调用apply方法。在new
方法的上下文中,this
对象指的就是真实的构造器函数——我们把它保存在constructor
变量中,同样的,我们也把new
方法上下文的arguments
保存在args
变量中,以便在Fake
构造器中使用。往下
Fake.prototype = constructor.prototype
我们设置Fake.prototype
为原来的构造器的prototype
。因为constructor
指向的还是原始的构造函数,他的prototype
属性还是原来的。所以通过Fake
创建的对象还是原来的构造器函数的实例。最后
return new Fake
使用Fake
构造器创建一个新对象并返回。
明白了么?第一次不明白没关系,多看几遍就能理解了!
总而言之,现在我们可以干一些很酷的事情了。
var children = [new Person('Ben'), new Person('Dan')]
var args = ['Bob'].concat(children)
var bob = Person.new.apply(Person, args)
很好!为了不写两遍Person,我们可以添加一个辅助方法
Function.prototype.applyNew = function(){
return this.new.apply(this, arguments)
}
现在你可以这样使用
var bob = Person.applyNew(args)
这就展示了Javascript是一门灵活的语言。即使它有些使用方法不是你想要的,你也可以模拟去做。
总结
这篇文章到这里就结束了,我们学习了
- Constructors构造器
- Methods and Prototypes方法和原型
-
apply
&call
- 实现一个
new
方法