Javascript构造函数和原型

作者: 文兴 | 来源:发表于2016-11-08 20:20 被阅读554次

    原文: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方法

    相关文章

      网友评论

      • 一番君:【你不使用分号因为你是个异教徒! 】
        瓦特!XD

      本文标题:Javascript构造函数和原型

      本文链接:https://www.haomeiwen.com/subject/bpiauttx.html