JS 面向?qū)ο缶幊讨畼?gòu)造函數(shù)和原型

構(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):

  1. 函數(shù)體內(nèi)部使用了this關(guān)鍵字甸怕,代表了所要生成的對象實(shí)例。
  2. 生成對象的時候腮恩,必須使用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屬性,值是測試恨溜。

  1. 使用new命令時符衔,根據(jù)需要,構(gòu)造函數(shù)也可以接受參數(shù)糟袁。
  2. 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í)行下面的步驟开缎。

  1. 創(chuàng)建一個空對象棕叫,作為將要返回的對象實(shí)例。
  2. 將這個空對象的原型啥箭,指向構(gòu)造函數(shù)的prototype屬性谍珊。
  3. 將這個空對象賦值給函數(shù)內(nèi)部的this關(guān)鍵字。
  4. 開始執(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

上面代碼中购公,animal1animal2是同一個構(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'

這里的colorcat1cat2都共享的一個屬性絮爷,當(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的屬性像街。這就是所有對象都有valueOftoString方法的原因,因?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

上面的例子中,fnF的實(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同時是DateObject的實(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

此外,對于undefinednull述暂,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)參考教程

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末艺配,一起剝皮案震驚了整個濱河市察郁,隨后出現(xiàn)的幾起案子衍慎,更是在濱河造成了極大的恐慌,老刑警劉巖绳锅,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件西饵,死亡現(xiàn)場離奇詭異,居然都是意外死亡鳞芙,警方通過查閱死者的電腦和手機(jī)眷柔,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來原朝,“玉大人驯嘱,你說我怎么就攤上這事≡梗” “怎么了鞠评?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長壕鹉。 經(jīng)常有香客問我剃幌,道長,這世上最難降的妖魔是什么晾浴? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任负乡,我火速辦了婚禮,結(jié)果婚禮上脊凰,老公的妹妹穿的比我還像新娘抖棘。我一直安慰自己,他們只是感情好狸涌,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布切省。 她就那樣靜靜地躺著,像睡著了一般帕胆。 火紅的嫁衣襯著肌膚如雪朝捆。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天懒豹,我揣著相機(jī)與錄音右蹦,去河邊找鬼。 笑死歼捐,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的晨汹。 我是一名探鬼主播豹储,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼淘这!你這毒婦竟也來了剥扣?” 一聲冷哼從身側(cè)響起巩剖,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎钠怯,沒想到半個月后佳魔,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡晦炊,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年鞠鲜,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片断国。...
    茶點(diǎn)故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡贤姆,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出稳衬,到底是詐尸還是另有隱情霞捡,我是刑警寧澤,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布薄疚,位于F島的核電站碧信,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏街夭。R本人自食惡果不足惜砰碴,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望莱坎。 院中可真熱鬧衣式,春花似錦、人聲如沸檐什。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽乃正。三九已至住册,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間瓮具,已是汗流浹背荧飞。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留名党,地道東北人叹阔。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像传睹,于是被迫代替她去往敵國和親耳幢。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評論 2 354

推薦閱讀更多精彩內(nèi)容