一些廢話
剛接觸js時柄瑰,都說原型鏈?zhǔn)莏s中最難的部分涣狗,看完教程妹沙,不以為然杭隙。直到一年多之后映穗,仍然被問到很多答不上來和答錯的問題暂幼,終于又感覺到糊肤,曾經(jīng)“我以為”的東西病涨,或許并非“我以為”的樣子熬北。經(jīng)過多次的原來如此的“恍然大悟”之后疙描,終于有了一個相對系統(tǒng)的認(rèn)識,但即使是多年之后的現(xiàn)在讶隐,依然會有一些問題起胰,令我困惑。
要講清楚原型鏈的基本原理巫延,其實只需兩三句話效五。但要理解它,首先要對js的對象炉峰、函數(shù)畏妖、數(shù)據(jù)類型等有深入的理解和認(rèn)識,對于初學(xué)者來說疼阔,大部分內(nèi)容都在短時間內(nèi)學(xué)完戒劫,基礎(chǔ)并不扎實半夷,所以對于比較抽象的理論,都覺得有些難迅细。另一方面巫橄,大部分初級工程師的大部分工作,都是在搭好的架子下填充業(yè)務(wù)代碼茵典,對于他們來說嗦随,多背幾個API才是提高工作效率的最佳方式,原型繼承的設(shè)計和應(yīng)用離他們相去甚遠(yuǎn)敬尺,一些沒有實踐的抽象理論枚尼,注定無法深入理解,也注定被快速遺忘砂吞,是以很多人覺得署恍,原型鏈很難,同時并沒有什么實用價值蜻直,這是認(rèn)識上的誤區(qū)盯质。廢話有點多了,言歸正傳概而,下面從什么是原型鏈說起呼巷。
一、 什么是原型鏈
對于一些沒有明確定義赎瑰,又很難一句話全面概況的諸如“xxx是什么”這樣的問題其實是挺蛋疼的王悍,就像遇到外星人問你“什么是筷子”一樣,你可以說那是一種用來吃飯的工具餐曼,但這種定義是不準(zhǔn)確压储,不全面的。如果遇到一些2B屬性的面試官源譬,也經(jīng)常會被問到“什么是原型鏈”這樣的問題集惋。曾經(jīng)為了防備,也曾對原型鏈的概念進行過歸納定義:原型鏈?zhǔn)侵窲S中由各級對象的__ proto__屬性連續(xù)繼承實現(xiàn)的鏈?zhǔn)浇Y(jié)構(gòu)踩娘,保存了對象的共有屬性和方法刮刑,控制著對象屬性的使用順序。這概念顯然也不怎么嚴(yán)謹(jǐn)和全面养渴,不懂的人看了會覺得蛋疼雷绢,懂的人看了更加蛋疼。
為了大概解釋清楚原型鏈?zhǔn)窃趺匆换厥潞衤觯€是引用一下《JavaScript高級程序設(shè)計》里面的解釋吧习寸。
ECMAScript將原型鏈作為實現(xiàn)繼承的主要方法胶惰。其基本思想是利用原型讓一個引用類型繼承另一個引用類型的屬性和方法傻工。每個構(gòu)造函數(shù)都有一個原型對象,原型對象都包含一個指向構(gòu)造函數(shù)的指針,而實例都包含一個指向原型對象的內(nèi)部指針中捆。假如另一個原型又是另一個類型的實例鸯匹,那么上述關(guān)系依然成立,如此層層遞進泄伪,就構(gòu)成了實例與原型的鏈條殴蓬。這就是所謂原型鏈的基本概念。
本文適合有一定基礎(chǔ)但離“牛逼”這個形容詞還相去甚遠(yuǎn)的屌絲閱讀蟋滴,如果你連上面的解釋都沒看懂或者下面的所有例子不假思索就看出結(jié)果染厅,下面的內(nèi)容都不值得浪費時間。關(guān)于原型鏈的基本用法和解釋津函,例子就不列舉了肖粮,關(guān)于構(gòu)造函數(shù)、constructor屬性尔苦、prototype屬性涩馆、__ proto__屬性、實例對象允坚,它們之間的邏輯關(guān)系魂那,也不畫圖了(因為隨便百度下都能查到海量的內(nèi)容,大同小異稠项,甚而千篇一律)涯雅。下面主要通過一些例子,總結(jié)下初級前端普遍存在的認(rèn)識偏差問題展运,還有一些我也不懂怎么解釋的問題斩芭。
二、一些例子
var a = 300;
function Fn1(){
this.a = 100;
this.b = 200;
return function(){
console.log(this.a); // ?
}.call(arguments[0]);
}
function Fn2(){
this.a = new Fn1();
this.name = "Cindy";
}
function Fn3(){
this.age =16;
}
/******* 第1類 ********/
var a = new Fn1().b; // 問題1 輸出乐疆?
var v = new Fn1(Fn2()); // 問題2 輸出划乖?
v.constructor === Fn1; // 問題3 ? // true
Fn1.prototype.constructor === Fn1; // 問題4 ?
Fn1.prototype instanceof Fn1; // 問題5 ?
Fn1.prototype.constructor = null;
new Fn1().constructor === null; // 問題6 ?
/******* 第2類 ********/
Fn3.age = 22;
Fn3.name = "Andy";
Fn3.prototype.age = 22;
Fn3.prototype.name = "Andy";
var f3 = new Fn3();
f3.age === 22; // 問題7 ?
f3.name === "Andy"; // 問題8 ?
f3 instanceof Fn3 === f3 instanceof Object; // 問題9 ?
Fn3.prototype = new Fn2();
f3.name === Fn3.name; // 問題10 ?
f3 instanceof Fn3; // 問題11 ?
/******* 第3類 ********/
Date.__proto__ === Function.prototype; // 問題12 ?
Date.constructor == Function; // 問題13 ?
Function.__proto__ === Function.prototype; // 問題14 ?
Function.constructor === Function; // 問題15 ?
typeof Date.prototype; // 問題16 ?
typeof Function.prototype; // 問題17 ?
typeof Object.prototype; // 問題18 ?
Object.__proto__ === Function.prototype; 問題19 ?
Object.constructor == Function; // 問題20 ?
Function.prototype.__proto__ === Object.prototype; // 問題21 ?
Object.prototype === Object.__proto__.__proto__; // 問題22 ?
Object.prototype.__proto__; // 問題23 ?
Function.prototype.prototype; // 問題24 ?
typeof Object.prototype.toString; // 問題25 ?
Object.prototype.toString.call(Object.prototype.toString); // 問題26 ?
Object.prototype.toString.prototype; // 問題27 ?
Object.prototype.toString.__proto__.prototype; // 問題28 ?
第1類
問題1、問題2比較簡單挤土,但涉及的知識點很多琴庵,如下:
實例化一個對象發(fā)生的事情:大概可以分為三步,第一創(chuàng)建一個空對象obj仰美,第二將這個空對象的__ proto__屬性指向構(gòu)造函數(shù)對象的prototype成員對象迷殿,第三將構(gòu)造函數(shù)的作用域賦給新對象并調(diào)用構(gòu)造函數(shù)。
構(gòu)造函數(shù)中如果返回一個應(yīng)用類型的對象(普通對象咖杂、函數(shù)庆寺、數(shù)組),則不再創(chuàng)建新對象诉字,直接返回該對象懦尝,例子如下:
function foo(name) {
this.name = name;
return [1,2,3,4]
}
console.log(new foo('cindy')); // 'Array(4) [1, 2, 3, 4]'
- 第2種情況知纷,如果返回的是一個立即執(zhí)行的匿名函數(shù),則仍然會創(chuàng)建新對象陵霉。匿名函數(shù)的this指向是誰調(diào)用它琅轧,它指向誰。下面代碼匿名函數(shù)指向window踊挠,alert的是 Andy乍桂。
var name = 'Andy';
function foo(name) {
this.name = name;
return (function(){alert(this.name)})()
}
console.log(new foo('cindy')); // 'Andy' '{name: "cindy"}'
- call方法中,如果不傳參數(shù)或者第一個參數(shù)是 null/undefined 時效床,this 的指向為全局對象睹酌,在瀏覽器宿主環(huán)境指 window。 構(gòu)造器 Fn1 中返回的函數(shù)加了call方法剩檀,相當(dāng)于一個立即執(zhí)行的匿名函數(shù)忍疾,所以new Fn1() 時還是會創(chuàng)建新對象。問題1谨朝、問題2中Fn1的arguments[0]是undefined卤妒,返回的方法執(zhí)行時, this指向 window字币,問題1则披、問題2打印的都是window.a,問題1中值是300洗出,問題2中值是200士复。
var a = new Fn1().b; // 問題1 輸出 300
var v = new Fn1(Fn2()); // 問題2 輸出 200 {a: 100, b: 200}
問題3-6,都是 constructor 的指向問題翩活。問題3阱洪,實例的 constructor 指向構(gòu)造函數(shù),沒毛病菠镇,true冗荸;問題4,構(gòu)造函數(shù)的原型對象的 constructor 屬性指向當(dāng)前構(gòu)造函數(shù)利耍,true蚌本;問題5,構(gòu)造函數(shù)的原型對象并非當(dāng)前構(gòu)造函數(shù)的實例隘梨,false程癌;問題6,實例中自身并沒有 constructor 屬性轴猎,實例對象的 constructor都是通過繼承而來的嵌莉,改變了原型中的 constructor 指向,實例中 constructor 屬性會動態(tài)改變捻脖,false锐峭。
v.constructor === Fn1; // 問題3 true
Fn1.prototype.constructor === Fn1; // 問題4 true
Fn1.prototype instanceof Fn1; // 問題5 false
Fn1.prototype.constructor = null;
new Fn1().constructor === null; // 問題6 {a: 100, b: 200} true
知識點誤區(qū):
1. 構(gòu)造函數(shù)的原型對象 (如Fn1.prototype指向的對象)是當(dāng)前構(gòu)造函數(shù)的實例
網(wǎng)上很多文章這樣說中鼠,有的可能是為了便于讀者理解其他問題,有的可能是沒有深入去理解只祠。其實除了 constructor 屬性指向當(dāng)前構(gòu)造函數(shù),F(xiàn)n1.prototype不具備Fn1實例的一切特點扰肌,連 instanceof 檢測都通不過抛寝。按我的理解,構(gòu)造函數(shù)的原型對象是 Object 的實例(Fn1.prototype.__ proto__指向Object.prototype)曙旭,是一個普通對象盗舰。但如果是Object 的實例,F(xiàn)n1.prototype.constructor應(yīng)該指向 Object桂躏,我的理解是(不知道實際是不是)在創(chuàng)建 Fn1 的時候钻趋,預(yù)定義了 Fn1.prototype 并改變了其 constructor 的指向,目的是便于Fn1 的實例能夠沿著原型通過 constructor 找到 Fn1剂习。
2. 每個對象都有一個預(yù)定義屬性 constructor蛮位,指向構(gòu)造函數(shù)
為了便于理解,很多文章和教材都這樣說鳞绕,但通過問題6可以很明顯的看出失仁,普通對象的 constructor 屬性是繼承而來的,并非自身屬性们何。這個認(rèn)識的偏差萄焦,有時候也會產(chǎn)生很多問題。
第2類
問題7-11冤竹,主要說明以下幾個問題拂封。
js中,函數(shù)也是對象鹦蠕,可以往里面添加/修改屬性和方法冒签,但這對它的實例沒有直接影響(修改了構(gòu)造函數(shù)的 prototype 屬性除外)。在繼承中钟病,永遠(yuǎn)是自有屬性的優(yōu)先級大于繼承屬性镣衡,f3有自有屬性age,也有繼承于原型對象的age档悠,直接讀取f3.age廊鸥,輸出自有屬性。Fn3.age = 22語句對實例 f3 沒有影響辖所,F(xiàn)n3.prototype.age = 22讀取優(yōu)先級較低惰说。所以,f3.age依然是16缘回,問題7為false吆视。
根據(jù)1可以總結(jié)出典挑,繼承存在于實例與構(gòu)造函數(shù)的原型對象之間,而不是存在于實例與構(gòu)造函數(shù)之間啦吧。問題8中方f3.name繼承了 Fn3.prototype.name您觉,true。
instanceof 操作符主要用于判斷默認(rèn)情況下(未修改原型對象或改變構(gòu)造函數(shù)prototype的指向)授滓,一個對象是否是某個構(gòu)造函數(shù)的實例琳水。判斷的方法是檢測構(gòu)造函數(shù)的prototype屬性是否出現(xiàn)在實例對象的原型鏈中的任何位置。問題9中Fn3和Object的prototype屬性都存在f3中般堆,true在孝。問題11,改變Fn3的prototype屬性指向后淮摔,f3和Fn3的 instanceof 關(guān)系不復(fù)存在私沮。因此,這種檢測方法是很不嚴(yán)謹(jǐn)?shù)摹?/p>
問題10中和橙,f3.name 依然是 Andy仔燕,F(xiàn)n3.prototype只是一個指針,指向構(gòu)造函數(shù)的原型對象魔招,這個對象在f3實例化的時候已經(jīng)綁定了涨享,通過Fn3.prototype修改原型對象的屬性,可以讓實例實現(xiàn)動態(tài)繼承仆百,但直接修改 Fn3.prototype 的指向厕隧,并不會改變已經(jīng)實例化的對象的__ proto__屬性的指向,但之后實例化的對象會指向新的原型俄周。函數(shù)對象中的name屬性為保留屬性吁讨,不可修改,所以問題10中 "Andy" === "Fn1"峦朗,false建丧。
Fn3.age = 22;
Fn3.name = "Andy";
Fn3.prototype.age = 22;
Fn3.prototype.name = "Andy";
var f3 = new Fn3();
f3.age === 22; // 問題7 false
f3.name === "Andy"; // 問題8 true
f3 instanceof Fn3 === f3 instanceof Object; // 問題9 true
Fn3.prototype = new Fn2();
f3.name === Fn3.name; // 問題10 false
f3 instanceof Fn3; // 問題11 false
有一種說法,對象主要通過__ proto__屬性而非prototype實現(xiàn)繼承的波势,有一定的合理性翎朱,但也忽視了prototype屬性在原型鏈中的作用,__ proto__是一個通道尺铣,但是它最初是通過prototype才找到原型對象的拴曲,并且prototype一直擁有修改原型對象的權(quán)利。我的理解凛忿,__ proto__是承上澈灼,prototype是啟下,少了任何一環(huán),都形成不了原型鏈叁熔。
第3類
第3類主要想弄清楚JS中一堆大佬的資歷和倫理問題委乌。在ES6標(biāo)準(zhǔn)中,JS有12大內(nèi)置對象荣回,分別是String遭贸、Number、Boolean 心软、Array壕吹、Date、RegExp糯累、Math算利、Error册踩、Function 泳姐、Object、Global (在瀏覽器中被替換為Window)暂吉、JSON胖秒。這其中,Global不可訪問慕的,Window不是ES標(biāo)準(zhǔn)阎肝,暫不討論。余下的11大內(nèi)置對象肮街,除了Math风题,JSON是以 object 類型存在外,其他都是 function 類型的內(nèi)置構(gòu)造器嫉父,意味著可以通過new操作符實例化沛硅,比如 Object 是一切對象的祖宗(proto指針的頂端),F(xiàn)unction是一切函數(shù)的祖宗(constructor 的頂端)绕辖,它們在js中摇肌,是骨灰級的存在。
問題12-15結(jié)果都為true仪际,一句話總結(jié):所有的構(gòu)造器都是Function的實例围小,包括根構(gòu)造器 Object 及 Function 自身。所有構(gòu)造器都繼承了 Function.prototype 的屬性及方法树碱。如length肯适、call、apply成榜、bind等(這里僅舉例其中兩個疹娶,其它的可以自己測試)。
Date.__proto__ === Function.prototype; // 問題12 true
Date.constructor == Function; // 問題13 true
Function.__proto__ === Function.prototype; // 問題14 true
Function.constructor === Function; // 問題15 true
問題16-18伦连,結(jié)果為"object"雨饺,"function"钳垮,"object",一句話總結(jié):除了Function.prototype额港,其他所有構(gòu)造函數(shù)的原型對象皆為"object"類型饺窿,可以自己試下。按常規(guī)理解移斩,原型對象都應(yīng)該是普通對象(object類型)肚医,不應(yīng)該是函數(shù)對象。具體為什么 Function.prototype 是函數(shù)對象向瓷,我也不理解肠套,只能先記著了,如果你知道猖任,一定要告訴我你稚。
typeof Date.prototype; // 問題16 object
typeof Function.prototype; // 問題17 function
typeof Object.prototype; // 問題18 object
問題19-22,問題19朱躺、問題20說明 Object 是 Function 的實例刁赖,繼承了 Function 的原型對象的方法;問題21說明 Function 的原型對象是 Object 的實例长搀,繼承了 Object 的原型對象的方法宇弛,問題22是前面幾個的等式替換,繼承到最后源请,Object 只能繼承它自己的原型對象枪芒。
Object.__proto__ === Function.prototype; // 問題19 true
Object.constructor == Function; // 問題20 true
Function.prototype.__proto__ === Object.prototype; // 問題21 true
Object.prototype === Object.__proto__.__proto__; // 問題22 true
問題23-24,終極問題谁尸,F(xiàn)unction 的原型對象沒有原型對象舅踪,Object 的原型對象的proto屬性指向null。原型鏈至此到了最頂端症汹。
Object.prototype.__proto__; // 問題23 null
Function.prototype.prototype; // 問題24 undefined
問題25-28是自己思考的一些問題和疑惑:很多文章和教材都說過硫朦,任何函數(shù)都可以作為構(gòu)造函數(shù)使用,函數(shù)對象都有 prototype 屬性背镇∫д梗《JavaScript高級程序設(shè)計》和《JavaScript權(quán)威指南》也有相同或類似的表述。對于自定義的函數(shù)來說瞒斩,這個結(jié)論沒有毛病破婆,但對于所有預(yù)定義的API函數(shù),也包括上面討論的 Function.prototype 這種類型的函數(shù)胸囱,都沒有prototype屬性祷舀,也無法作為構(gòu)造器使用。
typeof Object.prototype.toString; // 問題25 function
Object.prototype.toString.call(Object.prototype.toString); // 問題26 "[object Function]"
Object.prototype.toString.prototype; // 問題27 undefined
Object.prototype.toString.__proto__.prototype; // 問題28 undefined
三、 總結(jié)和思考:另一些廢話
JS的特點
JS是一門很松散的語言裳扯,很多東西可以這樣寫抛丽,也可以那樣寫。同樣的功能饰豺,有的人寫出來是水亿鲜,有的人是冰,有的人是雪花冤吨,拘謹(jǐn)?shù)娜讼訔壦碾S意蒿柳,隨意的人覺得它靈活。也正由于它的不嚴(yán)謹(jǐn)漩蟆,弄出好些諸如 typeof null 為 object 這樣的問題垒探。永遠(yuǎn)不要說自己很精通JS(面試的時候除外),很多問題或許連設(shè)計者都未曾預(yù)見怠李。隨著學(xué)習(xí)的深入圾叼,或許有一天,你會推翻自己總結(jié)的所有結(jié)論扔仓,因為總有特例褐奥。諸如上面任何函數(shù)都可以作為構(gòu)造函數(shù)使用咖耘,函數(shù)對象都有 prototype 屬性這個結(jié)論翘簇,可以覆蓋幾乎所有我們可能用作構(gòu)造函數(shù)的函數(shù),但作為命題儿倒,只要找出一個特例就可以推翻版保。
關(guān)于學(xué)習(xí)
關(guān)于這個問題,從設(shè)計者的角度來說夫否,預(yù)定義的API也的確不應(yīng)該允許當(dāng)做構(gòu)造函數(shù)使用彻犁,實際應(yīng)用中,應(yīng)該也沒有人會做 var obj = new Function.prototype.slice() 這樣的操作凰慈,總結(jié)的時候也很難想到這種情況汞幢。即使會被推翻,我覺得仍然應(yīng)該總結(jié)微谓,不總結(jié)就很難有提高森篷,總結(jié)而不糾結(jié),并保持思考和鉆研是我認(rèn)為學(xué)習(xí)者應(yīng)有的態(tài)度豺型。
上面的這些例子仲智,從常規(guī)思維來說,很多人會覺得有病才會思考這樣的問題姻氨。但如果你去騰訊钓辆、阿里等大公司面試,你會發(fā)現(xiàn)問的全部都是這一類問題,不管哪個模塊的知識點前联,問題要么偏功戚,要么深,要么很特例似嗤∫咄或許在他們眼里,只會答常規(guī)問題的人双谆,根本沒資格面試壳咕。
JS中的矛盾
任何問題,追根究底顽馋,都會有矛盾谓厘,如宇宙的起源,時間的始終寸谜,哲學(xué)上說矛盾貫穿一切事物的始終竟稳。JS中也有很有意思的矛盾問題:
對象是怎么來的?
由構(gòu)造函數(shù)實例化出來的熊痴。
構(gòu)造函數(shù)哪來的他爸?
由更高級的構(gòu)造函數(shù)實例化出來的。
最高級的構(gòu)造函數(shù)是果善?
Function诊笤。
Function哪來的?
它自己把自己構(gòu)造出來的巾陕。(笑哭)
還有:
Object的本質(zhì)是什么讨跟?
是一個構(gòu)造函數(shù)。
構(gòu)造函數(shù)的祖先是誰鄙煤?
Function晾匠。
Function是不是一個對象?
是梯刚。
對象的祖先是不是Object凉馆?
是。
讓我們來想一下亡资,天地混沌之中澜共,不知道如何就產(chǎn)生了一個叫 Function 的東西,生了一個很厲害的兒子叫 Object沟于,他們共同創(chuàng)造了眾神咳胃、眾生。最后 Function 發(fā)現(xiàn)旷太, Object 是它的祖先展懈。以前看過一個講原型鏈的視頻教程销睁,老師把原型對象比作爹,把構(gòu)造函數(shù)比作媽存崖,最后又說實例的爹是實例它媽生的冻记,所以JS是亂倫的,哈哈哈来惧。
++++++++++++++++++++++++++ 完整答案 ++++++++++++++++++++++++++++++++++++
var a = 300;
function Fn1(){
this.a = 100;
this.b = 200;
return function(){
console.log(this.a);
}.call(arguments[0]);
}
function Fn2(){
this.a = new Fn1();
this.name = "Cindy";
}
function Fn3(){
this.age =16;
}
/******* 第1類 ********/
var a = new Fn1().b; // 問題1 輸出 300
var v = new Fn1(Fn2()); // 問題2 輸出 200 {a: 100, b: 200}
v.constructor === Fn1; // 問題3 // true
Fn1.prototype.constructor === Fn1; // 問題4 ? true
Fn1.prototype instanceof Fn1; // 問題5 false
Fn1.prototype.constructor = null;
new Fn1().constructor === null; // 問題6 {a: 100, b: 200} true
/******* 第2類 ********/
Fn3.age = 22;
Fn3.name = "Andy";
Fn3.prototype.age = 22;
Fn3.prototype.name = "Andy";
var f3 = new Fn3();
f3.age === 22; // 問題7 ?false
f3.name === "Andy"; // 問題8 true
f3 instanceof Fn3 === f3 instanceof Object; // 問題9 true
Fn3.prototype = new Fn2();
f3.name === Fn3.name; // 問題10 false
f3 instanceof Fn3; // 問題11 false
/******* 第3類 ********/
Date.__proto__ === Function.prototype; // 問題12 true
Date.constructor == Function; // 問題13 true
Function.__proto__ === Function.prototype; // 問題14 true
Function.constructor === Function; // 問題15 true
typeof Date.prototype; // 問題16 object
typeof Function.prototype; // 問題17 function
typeof Object.prototype; // 問題18 object
Object.__proto__ === Function.prototype; 問題19 true
Object.constructor == Function; // 問題20 true
Function.prototype.__proto__ === Object.prototype; // 問題21 true
Object.prototype === Object.__proto__.__proto__; // 問題22 true
Object.prototype.__proto__; // 問題23 null
Function.prototype.prototype; // 問題24 undefined
typeof Object.prototype.toString; // 問題25 function
Object.prototype.toString.call(Object.prototype.toString); // 問題26 "[object Function]"
Object.prototype.toString.prototype; // 問題27 undefined
Object.prototype.toString.__proto__.prototype; // 問題28 undefined