3月份幾乎天天都能看到面試的人從我身邊通過,前段時間同事聊面試話題提到了原型鏈,頓時激起了我在開始學習前端時不少心酸的回憶。第一次接觸js的面向對象思想是在讀《js高程設計》(紅寶書)的時候,這部份內容卡了我整整一個多月,記得那會兒用了很笨的辦法,我把這兩個章節來回讀了一遍又一遍,仍然不能徹底理解,大部分是憑藉機械記憶。由於入門的時候很喜歡紅寶書,在差很少一年的自學時間裏基礎部分翻了將近10遍。固然,原型鏈也讀了10遍。很遺憾,那會兒我以爲本身只掌握了50%。直到讀了一個系列的書叫 《你不知道的javascript》,這本書神奇的叩開了我通往js學習之路的另外一扇大門,簡直顛覆了我對js以前的全部認識。尤爲是上卷關於this、閉包、原型鏈繼承的理解思想潛移默化的影響了我對這門語言的認知。我還記得這本書是我在北京的地鐵裏用kindle讀完的,而後在博客裏寫了4篇讀書筆記。對於原型鏈,我曾經很偏執的喜歡,後來在決定要轉前端以後到杭州的一次面試,由於面試是在週末,跟一家作人工智能的公司技術負責人聊了將近兩個小時,他給了我不少前端職業發展的中肯建議(初到杭州面試的那段時間真的獲得了不少陌生人的指引跟幫助),糾正了我不少偏見的認知,至今我還記得他的花名。javascript
原型鏈設計機制一直是大多數前端開發最難理解的部分,聽說當初 Brendan Eich 設計之初不想引入類的概念,可是爲了將對象聯繫起來,加入的C++ new的概念,可是new沒有辦法共享屬性,就在構造函數裏設置了一個prototype屬性,這一設計理念成爲了js跟其餘面嚮對象語言不一樣的地方,同時也埋下了巨大的坑!css
爲了解決由於委託機制帶來的各類各樣的缺點及語法問題,es6以後引入的class,class的實質仍是基於原型鏈封裝的語法糖,可是卻大大簡化的前端開發的代碼,也解決了不少歷史遺留的問題,(這裏並不想展開討論)。可是,es6以後,原型鏈真的不須要被瞭解了嗎?在知乎上有一篇被瀏覽了130多萬的話題 :《面試一個5年的前端,卻連原型鏈也搞不清楚,滿口都是Vue,React之類的實現,這樣的人該用嗎?》曾經引發過熱議。接下來咱們就來聊聊js的原型鏈吧!前端
在聊原型鏈以前,我想先聊聊new,這是一個常常會在面試中被問到的基礎問題。怎麼使用這裏不詳細介紹,只是提一下js裏new的設計原理:java
建立一個新對象;es6
讓空對象的[[prototype]](IE9如下沒有該屬性,在js代碼裏寫法爲__proto__)成員指向了構造函數的prototype成員對象;web
使用apply調用構造器函數,this綁定到空對象obj上;面試
返回新對象。segmentfault
function NEW_OBJECT(Foo){
var obj={};
obj.__proto__=Foo.prototype;
obj.constructor=Foo;
Foo.apply(obj,arguments)
return obj;
}
構造函數的主要問題是,每一個方法都要再每一個實例上從新建立一遍,不一樣實例上的同名函數是不相等的。例如:閉包
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){
alert(this.name);
};
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
alert(person1.sayName == person2.sayName); /*false*/
然而,建立兩個完成一樣任務的Function 實例的確沒有必要,經過把函數定義轉移到構造函數外部來解決這個問題。app
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = sayName;
}
function sayName(){
alert(this.name);
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
新問題又來了:在全局做用域中定義的函數實際上只能被某個對象調用,這讓全局做用域有點名存實亡。而更讓人沒法接受的是:若是對象須要定義不少方法,那麼就要定義不少個全局函數,因而咱們這個自定義的引用類型就絲毫沒有封裝性可言了。這時候,該原型鏈登場了!
1:[[prototype]]
JavaScript 中的對象有一個特殊的[[Prototype]] 內置屬性,其實就是對於其餘對象的引用。幾乎全部的對象在建立時[[Prototype]] 屬性都會被賦予一個非空的值。全部普通的[[Prototype]] 鏈最終都會關聯到內置的Object.prototype。
當咱們試圖訪問一個對象下的某個屬性的時候,會在JS引擎觸發一個GET的操做,首先會查找這個對象是否存在這個屬性,若是沒有找的話,則繼續在prototype關聯的對象上查找,以此類推。若是在後者上也沒有找到的話,繼續查找的prototype,這一系列的連接就被稱爲原型鏈。
2:prototype
只要建立了一個新函數,就會根據一組特定的規則爲該函數建立一個prototype屬性
3:constructor
對象的.constructor 會默認關聯一個函數,這個函數能夠經過對象的.prototype引用,.constructor 並非一個不可變屬性。它是不可枚舉的,可是它的值是可寫的(能夠被修改)。._ proto _ === .constructor.prototype
function Foo() { /* .. */ }
Foo.prototype = { /* .. */ }; // 建立一個新原型對象,並改寫constructor
var a1 = new Foo();
a1.constructor === Foo; // false!
a1.constructor === Object; // true!
四種寫法的思考
1:A.prototype = B.prototype
這種方法很容易理解,A要繼承B原型鏈屬性,直接改寫A的Prototype關聯到B的prototype,可是,若是在A上執行從B繼承過來的某一個屬性或方法,例如:A.prototype.myName =…會直接修改B.prototype自己。
2:A.prototype = new B()
這種方式會建立關聯到B原型上的新對象,可是因爲使用構造函數,在B上若是修改狀態、主車道其餘對象,會影響到A的後代。
3:A.prototype = Object.create(B.prototype) (ES5新增)
Object.create()是個頗有意思的函數,用一段簡單的polyfill來實現它的功能:
Object.create = function(o) {
function F(){}
F.prototype = o;
return new F();
};
Object.create(null) 會建立一個擁有空( 或者說null)[[Prototype]]連接的對象,這個對象由於沒有原型鏈沒法進行委託
var anotherObject = {
cool: function() {
console.log( "cool!" );
}
};
var myObject = Object.create( anotherObject );
myObject.doCool = function() {
this.cool(); // 內部委託!
};
myObject.doCool(); // "cool!"
4:Object.setPrototypeOf( A.prototype, B.prototype ); (ES6新增)
在segementfault上有這麼一道面試題:
var str = new String("hello world");
console.log(str instanceof String);//true
console.log(String instanceof Function);//true
console.log(str instanceof Function);//false
先把這道題放一邊,咱們都知道typeof能夠判斷基本數據類型,若是是判斷某個值是什麼類型的對象的時候就無能爲力了,instanceof用來判斷某個 構造函數 的prototype是否在要檢測對象的原型鏈上。
function Fn(){};
var fn = new Fn();
console.log(fn instanceof Fn) //true
//判斷fn是否爲Fn的實例,而且是否爲其父元素的實例
function Aoo();
function Foo();
Foo.prototype = new Aoo();
let foo = new Foo();
console.log(foo instanceof Foo); //true
console.log(foo instanceof Aoo); //true
//instanceof 的複雜用法
console.log(Object instanceof Object) //true
console.log(Function instanceof Function) //true
console.log(Number instanceof Number) //false
console.log(Function instaceof Function) //true
console.log(Foo instanceof Foo) //false
看到上面的代碼,你大概會有不少疑問吧。有人將ECMAScript-262 edition 3中對instanceof的定義用代碼翻譯以下:
function instance_of(L, R) {//L 表示左表達式,R 表示右表達式
var O = R.prototype;// 取 R 的顯示原型
L = L.__proto__;// 取 L 的隱式原型
while (true) {
if (L === null)
return false;
if (O === L)// 這裏重點:當 O 嚴格等於 L 時,返回 true
return true;
L = L.__proto__;
}
}
咱們知道每一個對象都有proto([[prototype]])屬性,在js代碼中用__proto__來表示,它是對象的隱式屬性,在實例化的時候,會指向prototype所指的對象;對象是沒有prototype屬性的,prototype則是屬於構造函數的屬性。經過proto屬性的串聯構建了一個對象的原型訪問鏈,起點爲一個具體的對象,終點在Object.prototype。
Object instanceof Object :
// 區分左側表達式和右側表達式
ObjectL = Object, ObjectR = Object;
O = ObjectR.prototype = Object.prototype;
L = ObjectL.__proto__ = Function.prototype ( Object做爲一個構造函數,是一個函數對象,因此他的__proto__指向Function.prototype)
// 第一次判斷
O != L
// 循環查找 L 是否還有 __proto__
L = Function.prototype.__proto__ = Object.prototype ( Function.prototype是一個對象,一樣是一個方法,方法是函數,因此它必須有本身的構造函數也就是Object)
// 第二次判斷
O == L
// 返回 true
Foo instanceof Foo :
FooL = Foo, FooR = Foo;
// 下面根據規範逐步推演
O = FooR.prototype = Foo.prototype
L = FooL.__proto__ = Function.prototype
// 第一次判斷
O != L
// 循環再次查找 L 是否還有 __proto__
L = Function.prototype.__proto__ = Object.prototype
// 第二次判斷
O != L
// 再次循環查找 L 是否還有 __proto__
L = Object.prototype.__proto__ = null
// 第三次判斷
L == null
// 返回 false
理解了這兩條判斷的原理,咱們回到剛纔的面試題:
console.log(str.__proto__ === String.prototype); //true
console.log(str instanceof String);//true
console.log(String.__proto__ === Function.prototype) //true
console.log(String instanceof Function);//true
console.log(str__proto__ === String.prototype)//true
console.log(str__proto__.__proto__. === Function.prototype) //true
console.log(str__proto__.__proto__.__proto__ === Object.prototype) //true
console.log(str__proto__.__proto__.__proto__.__proto__ === null) //true
console.log(str instanceof Function);//false
總結以上,str的原型鏈是:
str ---> String.prototype ---> Function.prototype ---> Object.prototype
最後,提一個能夠通用的來判斷原始數據類型和引用數據類型的方法吧:Object.prototype.toString.call()
ps:在js中,valueOf跟toString是兩個神奇的存在!!!
console.log(Object.prototype.toString.call(123)) //[object Number]
console.log(Object.prototype.toString.call('123')) //[object String]
console.log(Object.prototype.toString.call(undefined)) //[object Undefined]
console.log(Object.prototype.toString.call(true)) //[object Boolean]
console.log(Object.prototype.toString.call({})) //[object Object]
console.log(Object.prototype.toString.call([])) //[object Array]
console.log(Object.prototype.toString.call(function(){})) //[object Function]
面向委託 VS 類:
我以爲可能畢竟面向對象的不少語言都有類,而js的繼承不少學習過其餘語言的摸不着頭腦,就致使了js一直向模仿類的形式發展,es6就基於原型鏈的語法糖封裝了一個不三不四的class,讓人覺得js實際上也有類,真得是爲了讓相似學習過java的朋友容易理解,狠起來連本身都騙!我很贊成你不知道的javascript做者對於js中封裝類的見解:ES6 的class 想假裝成一種很好的語法問題的解決方案,可是實際上卻讓問題更難解決並且讓JavaScript 更加難以理解。
這兩個的區別我並不想說太多,由於實際上我對類的理解也很少,只知道它的思想是定義好一個子類以後,相對於父類來講它就是一個獨立而且徹底不一樣的類。子類會包含父類行爲的原始副本,可是也能夠重寫全部繼承的行爲甚至定義新行爲。子對父是真正的複製。
而在js中沒有真正意思的複製,實質上都是基於一個委託機制,複製的只是一個引用(相似C語言中指針的理解,js高程中習慣用指針思惟來解釋,不過我更喜歡你不知道的javascript中的委託機制的說法。)
class的用法再也不提,寫到這裏,已經寫的很累了,儘管在一年前寫過相似的文章,可是從新整理起來仍是不過輕鬆的一件事,並且我如今也以爲對於JS的類理解的不是那麼透徹,之後再慢慢深刻理解吧!
參考文獻:
1: JS高程設計 第六章
2: 你不知道的JavaScript(上卷)
3: JavaScript instanceof 運算符深刻剖析
4: Javascript中一個關於instanceof的問題