Javascript構造函數和原型

原文: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方法
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,836評論 6 540
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,275評論 3 428
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 177,904評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,633評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,368評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,736評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,740評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,919評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,481評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,235評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,427評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,968評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,656評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,055評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,348評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,160評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,380評論 2 379

推薦閱讀更多精彩內容