引言
最近又攀登了一下JS三座大山中的第二座。登山過程很酸爽,一路發現了許多之前沒曾注意到的美景。本着獨樂樂不如衆樂樂的原則,這裏和大家分享一下。
JS的面試對象
有些人認爲 JavaScript 不是真正的面向對象的語言,比如它沒有像許多面向對象的語言一樣有用於創建class類的聲明(在 ES2015/ES6 中引入了 class 關鍵字,但那只是語法糖,JavaScript 仍然是基於原型的)
。JavaScript 用一種稱爲構建函數的特殊函數來定義對象和它們的特徵。不像“經典”的面向對象的語言,從構建函數創建的新實例的特徵並非全盤複製,而是通過一個叫做原形鏈的參考鏈鏈接過去的。同理,原型鏈也是實現繼承的主要方式(
ES6的extends只是語法糖
)。
原型、原型鏈
一直在猶豫,到底是先講創建對象的方法還是先講原型。爲了後面保證講創建對象方法的連貫性,這裏還是先講講原型吧,
這裏爲了權威,直接就摘抄MD
N的定義了
JavaScript 常被描述爲一種基於原型的語言 (prototype-based language)
——每個對象擁有一個原型對象
,對象以其原型爲模板、從原型繼承方法和屬性。原型對象也可能擁有原型,並從中繼承方法和屬性,一層一層、以此類推。這種關係常被稱爲原型鏈 (prototype chain)
,它解釋了爲何一個對象會擁有定義在其他對象中的屬性和方法。準確地說,這些屬性和方法定義在Object的構造器函數(constructor functions)之上的prototype屬性上,而非對象實例本身。
這個__proto__屬性有什麼用呢?在傳統的 OOP 中,首先定義“類”,此後創建對象實例時,類中定義的所有屬性和方法都被複制到實例中。在 JavaScript 中並不如此複製,而是在對象實例和它的構造器之間建立一個鏈接(它是__proto__屬性,是從構造函數的prototype屬性派生的),之後通過上溯原型鏈,在構造器中找到這些屬性和方法。
簡單的說,就是實例對象能通過自己的__proto__屬性去訪問“類”
原型(prototype)上的方法和屬性,類如果也是個實例,就會不斷往上層類的原型去訪問,直到找到
補充:
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方法
上圖是我畫的一個原型鏈圖
[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的類
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世界裏的關係圖
知識點:
- Object.getPrototypeOf(Function) === Function.prototype // Function是Function的實例,沒毛病
- Object.getPrototypeOf(Object.prototype)
- 任何方法上都有prototype屬性以及__proto__屬性
任何對象上都有__proto__屬性 - Function.__proto__.__proto__===Object.prototype
- Object.getPrototypeOf(Object)===Function.prototype
- 最高級應該就是Function.prototype了,因爲5
判斷類型的方法
之前在JS核心知識點梳理——數據篇裏面說了一下判斷判斷類型的四種方法,這裏藉着原型再來分析一下
1. typeof:
只能判斷基礎類型中的非Null,不能判斷引用數據類型(因爲全部爲object)它是操作符
2. instanceof:
用於測試構造函數的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
3. constructor:
constructor 這玩意已經介紹過了,“類”的原型執行constructor指向“類”
風險的話也是來自原型的改寫
[1,2,3].constructor.name //'Array'
// 注意下面兩種寫法區別
Person.protorype.xxx = function //爲原型添加方法,默認constructor還是在原型裏
Person.protorype = { //原型都被覆蓋了,沒有constructor了,所要要手動添加,要不然constructor判斷失效
xxx:function
constructor:Person
}
4.Object.prototype.toString.call(xxx)
試了下,好像這個方法也不是很準
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]"
總結
參照各種資料,結合自己的理解,在儘量不涉及到繼承的情況下,詳細介紹了原型及其衍生應用。由於本人技術有限,如果有說得不對的地方,希望在評論區留言。