最近又攀登了一下JS三座大山中的第二座。爬山過程很酸爽,一路發現了許多以前沒曾注意到的美景。本着獨樂樂不如衆樂樂的原則,這裏和你們分享一下。es6
有些人認爲 JavaScript 不是真正的面向對象的語言,好比它沒有像許多面向對象的語言同樣有用於建立class類的聲明(在 ES2015/ES6 中引入了 class 關鍵字,但那只是語法糖,JavaScript 仍然是基於原型的)
。JavaScript 用一種稱爲構建函數的特殊函數來定義對象和它們的特徵。不像「經典」的面向對象的語言,從構建函數建立的新實例的特徵並不是全盤複製,而是經過一個叫作原形鏈的參考鏈連接過去的。同理,原型鏈也是實現繼承的主要方式(
ES6的extends只是語法糖
)。面試
一直在猶豫,究竟是先講建立對象的方法仍是先講原型。爲了後面保證講建立對象方法的連貫性,這裏仍是先講講原型吧,
這裏爲了權威,直接就摘抄MD
N的定義了segmentfault
JavaScript 常被描述爲一種基於原型的語言 (prototype-based language)
——每一個對象擁有一個原型對象
,對象以其原型爲模板、從原型繼承方法和屬性。原型對象也可能擁有原型,並從中繼承方法和屬性,一層一層、以此類推。這種關係常被稱爲原型鏈 (prototype chain)
,它解釋了爲什麼一個對象會擁有定義在其餘對象中的屬性和方法。準確地說,這些屬性和方法定義在Object的構造器函數(constructor functions)之上的prototype屬性上,而非對象實例自己。數組
這個__proto__屬性有什麼用呢?在傳統的 OOP 中,首先定義「類」,此後建立對象實例時,類中定義的全部屬性和方法都被複制到實例中。在 JavaScript 中並不如此複製,而是在對象實例和它的構造器之間創建一個連接(它是__proto__屬性,是從構造函數的prototype屬性派生的),以後經過上溯原型鏈,在構造器中找到這些屬性和方法。瀏覽器
簡單的說,就是實例對象能經過本身的__proto__屬性去訪問「類」
原型(prototype)上的方法和屬性,類若是也是個實例,就會不斷往上層類的原型去訪問,直到找到app
補充:
1.「類」的原型有一個屬性叫作constructor指向「類」
2.__proto__已被棄用,提倡使用Object.getPrototypeOf(obj)
函數
舉例:性能
var arr = [1,2,3] //arr是一個實例對象(數組類Array的實例) arr.__proto__ === Array.prototype //true 實例上都有一個__proto__屬性,指向「類」的原型 Array.prototype.__proto__ === Object.prototype //true 「類」的原型也是一個Object實例,那麼就必定有一個__proto__屬性,指向「類」object的原型
這裏補充一個知識點:
瀏覽器在在Array.prototype上內置了pop方法,在Object.prototype上內置了toString方法測試
上圖是我畫的一個原型鏈圖this
[1,2,3].pop() //3 [1,2,3].toString() //'1,2,3' [1,2,3].constructor.name //"Array" [1,2,3].hehe() //[1,2,3].hehe is not a function
當咱們調用pop()的時候,在實例[1,2,3]上面沒有找到該方法,則沿着原型鏈搜索"類"Array的原型,找到了pop方法並執行,同理調用toString方法的時候,在"類"Array沒有找到則會繼續沿原型鏈向上搜索"類"Object的原型,找到toString並執行。
當執行hehe方法的時候,因爲「類」Object的原型上並無找到,搜索「類」Object的__proto__,因爲執行null,中止搜索,報錯。
注意,[1,2,3].constructor.name顯示‘Array’不是說明實例上有constructor屬性,而是正是由於實例上沒有,因此搜索到
類的原型上了,找到了constructor
怎麼建立對象,或者說怎麼模擬類。這裏我就不學高程同樣,給你們介紹7種方法了,只講我以爲必須掌握的。畢竟都es6 es7了,不少方法基本都用不到,有興趣本身看高程。
const Person = function (name) { this.name = name this.sayHi = function () { alert(this.name) } } const xm = new Person('小明') const zs = new Person('張三') zs.sayHi() //'張三' xm.sayHi() //'小明'
缺點: 每次實例化都須要複製一遍函數到實例裏面。可是不論是哪一個實例,實際上sayHi都是相同的方法,不必每次實例化的時候都複製一遍,增長額外開銷。
function specialArray() { var arr = new Array() arr.push.apply(arr,arguments) arr.sayHi = function () { alert('i am an specialArray') } return arr } var arr = new specialArray(1,2,3)
這個和在數組的原型鏈上增長方法有啥區別?
原型鏈上增長方法,全部數組均可以用。寄生構造函數模式只有被specialArray類new出來的才能用。
//共有方法掛到原型上 const Person = function () { this.name = name } Person.prototype.sayHi = function () { alert(this.name) } const xm = new Person('小明') const zs = new Person('張三') zs.sayHi() //'張三' xm.sayHi() //'小明'
缺點:基本沒啥缺點了,建立自定義類最多見的方法,動態原型模式
也只是在這種混合模式下加了層封裝,寫到了一個函數裏面,好看一點,對提升性能並無卵用。
es6的‘類’class其實就是語法糖
class Person { constructor(name) { this.name = name } say() { alert(this.name) } } const xm = new Person('小明') const zs = new Person('張三') zs.sayHi() //'張三' xm.sayHi() //'小明'
在es2015-loose模式下用bable看一下編譯
"use strict"; var Person = /*#__PURE__*/ function () { function Person(name) { this.name = name; } var _proto = Person.prototype; _proto.say = function say() { alert(this.name); }; return Person; }();
分析:嚴格模式,高級單例模式封裝了一個類,實質就是組合使用原型和構造函數
知識點:
以前在JS核心知識點梳理——數據篇裏面說了一下判斷判斷類型的四種方法,這裏藉着原型再來分析一下
只能判斷基礎類型中的非Null,不能判斷引用數據類型(由於所有爲object)它是操做符
用於測試構造函數的prototype屬性是否出如今對象的原型鏈中的任何位置 風險的話有兩個
//判斷不惟一 [1,2,3] instanceof Array //true [1,2,3] instanceof Object //true //原型鏈能夠被改寫 const a = [1,2,3] a.__proto__ = null a instanceof Array //false
仿寫一個instanceof,而且掛在Object.prototype上,讓全部對象都能用
//仿寫一個instance方法 Object.prototype.instanceof = function (obj) { let curproto = this.__proto__ while (!Object.is(curproto , null)){ if(curproto === obj.prototype){ return true } curproto = curproto.__proto__ } return false } [1,2,3].instanceof(Array) //true [1,2,3].instanceof(Object) //true [1,2,3].instanceof(Number) //false [1,2,3].instanceof(Function) //false 1..instanceof(Function) //false (1).instanceof(Number) //true
constructor 這玩意已經介紹過了,「類」的原型執行constructor指向「類」
風險的話也是來自原型的改寫
[1,2,3].constructor.name //'Array' // 注意下面兩種寫法區別 Person.protorype.xxx = function //爲原型添加方法,默認constructor仍是在原型裏 Person.protorype = { //原型都被覆蓋了,沒有constructor了,所要要手動添加,要否則constructor判斷失效 xxx:function constructor:Person }
試了下,好像這個方法也不是很準
null 能夠用object.is(xxx,null)代替
Array 能夠用Array.isArray(xxx)代替
Object.prototype.toString.call([1,2,3]) //"[object Array]" Object.prototype.toString.call(function(){}) //"[object Function]" Object.prototype.toString.call(1) //"[object Number]" Object.prototype.toString.call(null) //"[object Null]" Object.prototype.toString.call({}) //"[object Object]" Object.prototype.toString.call(undefined) //"[object Undefined]" Object.prototype.toString.call(true) // 特別注意 特別注意 特別注意"[object Object]" Object.prototype.toString.call('string') // 特別注意 特別注意 特別注意 "[object Undefined]"
對於for in 和in 都是沿着原型鏈查找屬性是否存在,能夠利用hasOwnProperty進行相關過濾
// 'in' operation test class Person { constructor (name) { this.name = name } sayHi() { console.log('Hi') } } var p1 = new Person('小明') 'name' in p1 //true 'sayHi' in p1 //true for (var i in p1) { if (p1.hasOwnProperty(i)) { console.log('ownProperty:' + i) } else { console.log('prototypeProperty: ' + i) } } //'ownProperty: name' // prototypeProperty: sayHi
參照各類資料,結合本身的理解,在儘可能不涉及到繼承的狀況下,詳細介紹了原型及其衍生應用。因爲本人技術有限,若是有說得不對的地方,但願在評論區留言。