相信你已经知道了,Javascript函数也能够做为对象构造器。好比,为了模拟面向对象编程中的Class
,能够用以下的代码javascript
function Person(name){ this.name = name }
注意:我不使用分号由于我是个异教徒!
无论怎么说,你如今有了一个function
,你可使用new
操做符来建立一个Person
php
var bob = new Person('Bob') // {name: 'Bob'}
为了确认bob
确实是一个Person
,能够这么作css
bob instanceof Person // true
你一样能够把Person
做为一个普通函数调用——不使用new
java
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 }
这段代码作了三件事函数
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
上建立方法会更加高效。
正如你所见,函数凭借添加到对象上而成为了一个对象的方法,那么这个函数内的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'
如今,有趣的事情来了。
当你想调用一个有若干个参数的函数时,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是一门灵活的语言。即便它有些使用方法不是你想要的,你也能够模拟去作。
这篇文章到这里就结束了,咱们学习了
apply
& call
new
方法