原文: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
}
這段代碼做了三件事
- 檢查
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
上創建方法會更加高效。
Apply & Call
正如你所見,函數憑借添加到對象上而成為了一個對象的方法,那么這個函數內的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'
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是一門靈活的語言。即使它有些使用方法不是你想要的,你也可以模擬去做。
總結
這篇文章到這里就結束了,我們學習了
- Constructors構造器
- Methods and Prototypes方法和原型
-
apply
&call
- 實現一個
new
方法