Javascript构造函数和原型

相信你已经知道了,Javascript函数也能够做为对象构造器。好比,为了模拟面向对象编程中的Class,能够用以下的代码javascript

function Person(name){ this.name = name }

注意:我不使用分号由于我是个异教徒!
无论怎么说,你如今有了一个function,你可使用new操做符来建立一个Personphp

var bob = new Person('Bob') // {name: 'Bob'}

为了确认bob确实是一个Person,能够这么作css

bob instanceof Person // true

你一样能够把Person做为一个普通函数调用——不使用newjava

Person('Bob') // undefined

可是这里会返回undefined.同时,你在不经意间建立了一个全局变量name,这可不是你想要的。编程

name
// 'Bob'

嗯...这一点也很差,特别是若是你已经有一个名为name的全局变量,那么它将会被覆盖。这是由于你直接调用了一个函数(不适用new),this对象被设置为全局对象——在浏览器中,就是window对象。数组

window.name // 'Bob' this === window // true

因此...若是你想写一个构造器函数,那么就用构造器的方式使用它(使用new),若是你想写一个普通函数,那么就以函数的方式使用它(直接调用),不要相互混淆。浏览器

注:一个较好的代码习惯就是,构造器函数首字母大写,普通函数首字母小写。如function Person(){}是一个构造器函数,function showMsg(){}是一个普通函数。bash

有些人也许会指出,可使用一个小技巧避免污染全局变量。app

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方法



文/文兴(简书做者) 原文连接:http://www.jianshu.com/p/322b90d489b8 著做权归做者全部,转载请联系做者得到受权,并标注“简书做者”。
相关文章
相关标签/搜索