構(gòu)造函數(shù)
構(gòu)造函數(shù)其實(shí)也是一個對象
var Fun = function () {
this.name = '測試'
}
上面的Fun
就是一個構(gòu)造函數(shù)靡狞,為了與普通函數(shù)區(qū)別,構(gòu)造函數(shù)通常第一個首字母大寫隔嫡。構(gòu)造函數(shù)有兩個特點(diǎn):
- 函數(shù)體內(nèi)部使用了this關(guān)鍵字甸怕,代表了所要生成的對象實(shí)例。
- 生成對象的時候腮恩,必須使用new命令梢杭。
-
new 關(guān)鍵字
new
關(guān)鍵字的作用就是執(zhí)行構(gòu)造函數(shù),返回一個實(shí)例對象秸滴。
var Person= function () {
this.name = '測試'
}
var Tom = new Person()
Tom.name // 測試
上面代碼通過new
命令武契,讓構(gòu)造函數(shù)Person
生成一個實(shí)例對象,保存在變量Tom
中。這個新生成的實(shí)例對象吝羞,從構(gòu)造函數(shù)Person
得到了name
屬性兰伤。new
命令執(zhí)行時,構(gòu)造函數(shù)內(nèi)部的this
钧排,就代表了新生成的實(shí)例對象敦腔,this.name
表示實(shí)例對象有一個name
屬性,值是測試
恨溜。
- 使用
new
命令時符衔,根據(jù)需要,構(gòu)造函數(shù)也可以接受參數(shù)糟袁。 -
new
命令本身就可以執(zhí)行構(gòu)造函數(shù)判族,所以后面的構(gòu)造函數(shù)可以帶括號,也可以不帶括號项戴。但是為了表示這里是函數(shù)調(diào)用形帮,推薦使用括號。
如果忘了使用new
命令周叮,直接調(diào)用構(gòu)造函數(shù)會發(fā)生什么事辩撑?
這種情況下,構(gòu)造函數(shù)就變成了普通函數(shù)仿耽,并不會生成實(shí)例對象合冀。而且由于后面會說到的原因,this
這時代表全局對象项贺,將造成一些意想不到的結(jié)果君躺。
var Person= function (name) {
this.name = '測試'
}
var Tom = new Person('Tom')
Tom.name // Tom
var p = Person()
p // undefind
name // 測試
new 命令的原理
使用new命令時,它后面的函數(shù)依次執(zhí)行下面的步驟开缎。
- 創(chuàng)建一個空對象棕叫,作為將要返回的對象實(shí)例。
- 將這個空對象的原型啥箭,指向構(gòu)造函數(shù)的prototype屬性谍珊。
- 將這個空對象賦值給函數(shù)內(nèi)部的this關(guān)鍵字。
- 開始執(zhí)行構(gòu)造函數(shù)內(nèi)部的代碼急侥。
function _new(/* 構(gòu)造函數(shù) */ constructor, /* 構(gòu)造函數(shù)參數(shù) */ params) {
// 將 arguments 對象轉(zhuǎn)為數(shù)組
var args = [].slice.call(arguments);
// 取出構(gòu)造函數(shù)
var constructor = args.shift();
// 創(chuàng)建一個空對象砌滞,繼承構(gòu)造函數(shù)的 prototype 屬性
var context = Object.create(constructor.prototype);
// 執(zhí)行構(gòu)造函數(shù)
var result = constructor.apply(context, args);
// 如果返回結(jié)果是對象,就直接返回坏怪,否則返回 context 對象
return (typeof result === 'object' && result != null) ? result : context;
}
// 實(shí)例
var actor = _new(Person, '張三', 28);
如果構(gòu)造函數(shù)里面有return
贝润,并且return一個非對象,new
命令執(zhí)行該函數(shù)時會忽略該return
铝宵,返回“構(gòu)造”后的 this
對象打掘;如果return
的是一個與this
無關(guān)的對象华畏,new
會將該對象返回,而不是this
對象尊蚁。
另一方面亡笑,如果對普通函數(shù)(內(nèi)部沒有this
關(guān)鍵字的函數(shù))使用new
命令,則會返回一個空對象横朋。
/** rerurn 非對象 */
var Car= function () {
this.price = 1000;
return 10;
};
(new Car()).price === 1000 // true
(new Car()) === 10 // false
/** return 一個與this無關(guān)的對象 */
var Animal = function () {
this.type = 'cat'
return {name: '小花'}
}
(new Animal()) // {name: '小花'}
(new Animal()).type // undefind
(new Animal()).name // 小花
/** new 一個普通函數(shù) */
var Dog = function () {
return 'this is a Dog仑乌。'
}
var d = new Dog()
d // {}
typeof d // "object"
-
Object.create() 創(chuàng)建實(shí)例對象
構(gòu)造函數(shù)作為模板,可以生成實(shí)例對象琴锭。但是晰甚,有時拿不到構(gòu)造函數(shù),只能拿到一個現(xiàn)有的對象决帖。我們希望以這個現(xiàn)有的對象作為模板厕九,生成新的實(shí)例對象,這時就可以使用Object.create()
方法地回。
var person1 = {
name: '張三',
age: 38,
greeting: function() {
console.log('Hi! I\'m ' + this.name + '.');
}
};
var person2 = Object.create(person1);
person2.name // 張三
person2.greeting() // Hi! I'm 張三.
上面代碼中扁远,對象person1是person2的模板,后者繼承了前者的屬性和方法落君。
-
構(gòu)造函數(shù)的缺點(diǎn)
通過構(gòu)造函數(shù)為實(shí)例對象定義屬性穿香,雖然很方便铁材,但是有一個缺點(diǎn)猾普。每一次new
都是將構(gòu)造函數(shù)復(fù)制一份賦值給新的實(shí)例對象绢涡,同一個構(gòu)造函數(shù)的多個實(shí)例之間,無法共享屬性纹冤,從而造成對系統(tǒng)資源的浪費(fèi)。
var Animal = function (name) {
this.name = name
this.type = 'cat'
this.eat = function () {
console.log('animal is eat' )
}
}
var animal1 = new Animal('小花')
var animal2 = new Animal('小黃')
animal1.eat() === animal2.eat() // false
上面代碼中购公,animal1
和animal2
是同一個構(gòu)造函數(shù)的兩個實(shí)例萌京,它們都具有eat
方法。由于eat
方法是生成在每個實(shí)例對象上面宏浩,所以兩個實(shí)例就生成了兩次知残。也就是說,每新建一個實(shí)例比庄,就會新建一個eat
方法求妹。這既沒有必要,又浪費(fèi)系統(tǒng)資源佳窑,因?yàn)樗?code>eat方法都是同樣的行為制恍,完全應(yīng)該共享。
這個問題的解決方法神凑,就是 JavaScript 的原型對象(prototype)净神。
原型
-
prototype屬性的作用
JavaScript 規(guī)定何吝,每個函數(shù)都有一個prototype
屬性,指向一個對象鹃唯。
對于普通函數(shù)來說爱榕,該屬性基本無用。但是坡慌,對于構(gòu)造函數(shù)來說呆细,生成實(shí)例的時候,該屬性會自動成為實(shí)例對象的原型八匠。
function fn() {}
typeof fn.prototype // "object"
function Animal(name) {
this.name = name
}
Animal.prototype.color = 'white'
var cat1 = new Animal('小花')
var cat2 = new Animal('小黃')
cat1.color // 'white'
cat2.color // 'white'
Animal.prototype.color = 'yellow'
cat1.color // 'yellow'
cat2.color // 'yellow'
這里的color
是cat1
和cat2
都共享的一個屬性絮爷,當(dāng)Animal的color屬性改變時,cat1和cat2對應(yīng)的屬性也會改變梨树。
當(dāng)實(shí)例對象本身沒有某個屬性或方法的時候坑夯,它會到原型對象去尋找該屬性或方法。這就是原型對象的特殊之處抡四。
cat1.color = 'black';
cat1.color // 'black'
cat2.color // 'yellow'
Animal.prototype.color // 'yellow';
如果實(shí)例本身有這個屬性或方法柜蜈,就不會去原型對象上尋找。
-
原型鏈
JavaScript 規(guī)定指巡,所有對象都有自己的原型對象(prototype)淑履。一方面,任何一個對象藻雪,都可以充當(dāng)其他對象的原型秘噪;另一方面,由于原型對象也是對象勉耀,所以它也有自己的原型指煎。因此,就會形成一個“原型鏈”(prototype chain):對象到原型便斥,再到原型的原型……
所有對象的原型最終都可以上溯到Object.prototype
至壤,即Object
構(gòu)造函數(shù)的prototype
屬性。也就是說枢纠,所有對象都繼承了Object.prototype
的屬性像街。這就是所有對象都有valueOf
和toString
方法的原因,因?yàn)檫@是從Object.prototype
繼承的晋渺。
Object.prototype
的原型是null
镰绎。null
沒有任何屬性和方法,也沒有自己的原型些举。因此跟狱,原型鏈的盡頭就是null
。
Object.getPrototypeOf(Object.prototype) // null
讀取對象的某個屬性時户魏,JavaScript 引擎先尋找對象本身的屬性驶臊,如果找不到挪挤,就到它的原型去找,如果還是找不到关翎,就到原型的原型去找扛门。如果直到最頂層的Object.prototype
還是找不到,則返回undefined
纵寝。如果對象自身和它的原型论寨,都定義了一個同名屬性,那么優(yōu)先讀取對象自身的屬性爽茴,這叫做“覆蓋”(overriding)葬凳。
注意,一級級向上室奏,在整個原型鏈上尋找某個屬性火焰,對性能是有影響的。所尋找的屬性在越上層的原型對象胧沫,對性能的影響越大昌简。如果尋找某個不存在的屬性,將會遍歷整個原型鏈绒怨。
-
constructor 屬性
prototype
對象有一個constructor
屬性纯赎,默認(rèn)指向prototype
對象所在的構(gòu)造函數(shù)。
function F () {}
F.prototype.constructor === F // true
由于constructor
屬性定義在prototype
對象上面南蹂,意味著可以被所有實(shí)例對象繼承犬金。
function F () {}
F.prototype.constructor === F // true
var fn = new F()
fn.constructor === F // true
fn.constructor === F.prototype.constructo // true
fn.hasOwnPrototype('constructor') // false
上面的例子中,fn
是F
的實(shí)例對象碎紊,fn
沒有自己的constructor
屬性佑附,該屬性其實(shí)是讀取原型鏈上面的F.prototype.constructor
屬性。
constructor
屬性的作用是仗考,可以得知某個實(shí)例對象,到底是哪一個構(gòu)造函數(shù)產(chǎn)生的词爬。
另一方面秃嗜,有了constructor
屬性,就可以從一個實(shí)例對象新建另一個實(shí)例顿膨。
function Constr() {}
var x = new Constr()
var y = new x.constructor()
y instanceof Constr // true
constructor
屬性表示原型對象與構(gòu)造函數(shù)之間的關(guān)聯(lián)關(guān)系锅锨,如果修改了原型對象,一般會同時修改constructor
屬性恋沃,防止引用的時候出錯必搞。
function Person(name) {
this.name = name;
}
Person.prototype.constructor === Person // true
Person.prototype = {
method: function () {}
};
Person.prototype.constructor === Person // false
Person.prototype.constructor === Object // true
上面代碼中,構(gòu)造函數(shù)Person
的原型對象改掉了囊咏,但是沒有修改constructor
屬性恕洲,導(dǎo)致這個屬性不再指向Person
塔橡。由于Person
的新原型是一個普通對象,而普通對象的contructor
屬性指向Object
構(gòu)造函數(shù)霜第,導(dǎo)致Person.prototype.constructor
變成了Object
葛家。
所以,修改原型對象時泌类,一般要同時修改constructor
屬性的指向癞谒。
// 壞的寫法
C.prototype = {
method1: function (...) { ... },
// ...
}
// 好的寫法
C.prototype = {
constructor: C,
method1: function (...) { ... },
// ...
}
// 更好的寫法
C.prototype.method1 = function (...) { ... }
如果不能確定constructor
屬性是什么函數(shù),還有一個辦法:通過name
屬性刃榨,從實(shí)例得到構(gòu)造函數(shù)的名稱弹砚。
function Foo() {}
var f = new Foo()
f.constructor.name // "Foo"
-
instanceof 運(yùn)算符
instanceof
運(yùn)算符返回一個布爾值,表示對象是否為某個構(gòu)造函數(shù)的實(shí)例(檢查的是整個原型鏈)枢希。
instanceof
運(yùn)算符的左邊是實(shí)例對象桌吃,右邊是構(gòu)造函數(shù)。它會檢查右邊構(gòu)建函數(shù)的原型對象(prototype)晴玖,是否在左邊對象的原型鏈上读存。因此,下面兩種寫法是等價的呕屎。
v instanceof Vehicle
// 等同于
Vehicle.prototype.isPrototypeOf(v)
// instanceof檢查的是整個原型鏈
var d = new Date()
d instanceof Date // true
d instanceof Object // true
上面代碼中让簿,d
同時是Date
和Object
的實(shí)例,因此對這兩個構(gòu)造函數(shù)都返回true
秀睛。
instanceof
的原理是檢查右邊構(gòu)造函數(shù)的prototype
屬性尔当,是否在左邊對象的原型鏈上。有一種特殊情況蹂安,就是左邊對象的原型鏈上椭迎,只有null
對象。這時田盈,instanceof
判斷會失真畜号。
var obj = Object.create(null)
typeof obj // "object"
obj instanceof Object // false
Object.create(null) instanceof Object // false
注意,instanceof
運(yùn)算符只能用于對象允瞧,不適用原始類型的值简软。
var s = 'hello'
s instanceof String // false
此外,對于undefined
和null
述暂,instanceof
運(yùn)算符總是返回false
痹升。
undefined instanceof Object // false
null instanceof Object // false
利用instanceof
運(yùn)算符,還可以巧妙地解決畦韭,調(diào)用構(gòu)造函數(shù)時疼蛾,忘了加new
命令的問題。
function Fubar (foo, bar) {
if (this instanceof Fubar) {
this._foo = foo
this._bar = bar
} else {
return new Fubar(foo, bar)
}
}
參考文獻(xiàn):
阮一峰JavaScript 標(biāo)準(zhǔn)參考教程