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
}

这段代码做了三件事

  1. 检查this对象是否是Person的实例——如果使用new操作符的话就是。
  2. 如果它确实是Person的实例,执行原有的代码。
  3. 如果它不是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对象。

注:原始数据类型还包含undefinednull。但如果你使用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不同于传统的面向对象语言,它使用原型继承。简而言之,原型继承的工作原理如下:

  1. 一个对象有许多属性,包含普通属性和函数。
  2. 一个对象有一个特殊的父属性,它也被称为这个对象的原型,用__proto__表示。这个对象可以继承它父对象的所有属性。
  3. 一个对象可以通过在自身设置属性,重写父对象的的同名属性
  4. 构造器用于创建对象。每一个构造器都有一个相关联的prototype对象,它其实也是一个普通对象。
  5. 创建一个对象时,该对象的父对象(__proto__)被设置为创建它的构造器的prototype对象。

好的!现在你应该明白原型继承是怎么一回事了,接下来我们一行一行看Cat这个例子

首先,我们创建了一个构造器Mammal

function Mammal(){
}

这时候,Mammal已经有了一个prototype属性

Mammal.prototype
// {}

我们创建一个实例

var mammal = new Mammal()

现在,我们验证一下上面提到的第2条

mammal.__proto__ === Mammal.prototype
// true

接下来,我们在Mammalprototype属性上增加一个方法breathe

Mammal.prototype.breathe = function(){
    // do some breathing
}

这时候,实例mammal就可以调用breathe了

mammal.breathe()

因为它从Mammal.prototype继承过来。往下

function Cat(){
}
Cat.prototype = new Mammal()

我们创建了一个Cat构造器,设置Cat.prototypeMammal的实例。为什么要这么做呢?

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的实例,你就会创建一个二级原型链,即garfieldCat.prototype的子对象,而Cat.prototypeMammal的实例,所以也是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对象,jackjill

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方法如何知道jackjill的名字呢?

答案:this指针没有绑定到任何对象上,直到函数被调用时才进行绑定。

当你调用jack.sayHi()时,sayHithis指针就会绑定到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是一门灵活的语言。即使它有些使用方法不是你想要的,你也可以模拟去做。

总结

这篇文章到这里就结束了,我们学习了

  1. Constructors构造器
  2. Methods and Prototypes方法和原型
  3. apply & call
  4. 实现一个new方法

推荐阅读更多精彩内容