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 著做權歸做者全部,轉載請聯繫做者得到受權,並標註「簡書做者」。
相關文章
相關標籤/搜索