相信你已經知道了,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
方法