標(biāo)簽(空格分隔): JAVASCRIPT DEEP
本文總結(jié)js里不太好理解的幾個(gè)概念:prototype, _proto_, new, Object.create, instanceof, typeof炫狱。
對象和實(shí)例
先來說明實(shí)例和對象,簡單來說對象就是一個(gè)概念(抽象的)撬腾,實(shí)例就是物體(實(shí)體)。
下面直接上代碼:
// 為了區(qū)分對象和原型匀泊,下面所有的對象統(tǒng)統(tǒng)用大寫
function A(name,age){this.name=name; this.age=age;}
a = new A("test", 10);
console.log(a.name, a.age); // => "test" 10
a就是一個(gè)對象,通過new
這個(gè)關(guān)鍵字把實(shí)例變成了一個(gè)實(shí)例痢畜。new
后面再說虫几,這里先只考慮prototype
黑低。
在js里赘艳,幾乎一切都是實(shí)例,而并非一切都是對象克握,可以簡單地認(rèn)為有prototype
這個(gè)屬性的都是對象蕾管。
這里只是粗略地說法,比如你強(qiáng)行給一個(gè)變量設(shè)置一個(gè)
prototype
屬性菩暗,其仍然不是一個(gè)對象娇掏,其仍然不能執(zhí)行new操作。只能做99%的情況下勋眯,如果有prototype
的屬性婴梧,就可以認(rèn)為它是一個(gè)對象下梢。
對象可以被實(shí)例化,對象可以被繼承塞蹭。對象本身也是原型孽江,其只不過是被更高層的對象實(shí)例化出來的而已。
說到底prototype
也無非就是對象的一個(gè)屬性而已番电。這是一個(gè)特殊的屬性岗屏,我們可以簡單理解為該屬性里放著對象A專門給其實(shí)例和后代的實(shí)例準(zhǔn)備的內(nèi)容。反之漱办,只有prototype
里面的內(nèi)容才會(huì)繼承給實(shí)例和子對象这刷,A本身的方法并不會(huì)被繼承。對于一個(gè)對象娩井,其顯式地通過A.prototype.someFunc
來調(diào)用prototype
屬性里的內(nèi)容暇屋,而對于一個(gè)實(shí)例,則可以直接采用a.someFunc
的方式調(diào)用其中的內(nèi)容洞辣。
另外咐刨,為了方便開發(fā)者訪問實(shí)例的對象的prototype
屬性,很多瀏覽器都實(shí)現(xiàn)了__proto__
這個(gè)好用的關(guān)鍵字扬霜。在js中定鸟,任何內(nèi)容都有__proto__
這一屬性。之前說了js里幾乎一切都是實(shí)例著瓶,不過也有例外联予,比如數(shù)字、字符串等基本類型材原。雖然在使用instanceof
的時(shí)候可能會(huì)返回false,但是這些基礎(chǔ)變量也都有__proto
屬性沸久,對應(yīng)到了合適的類型上,這么做可能主要是為了使用方便吧华糖。需要注意的是麦向,__proto
并非js標(biāo)準(zhǔn)瘟裸,有些場景下可能會(huì)報(bào)錯(cuò)客叉。
下面我們來具體看一下prototype
屬性的特點(diǎn)。代碼如下:
function A(){};
A.prototype.test = () => console.log("A.prototype.test");
let a = new A();
A.prototype.test(); // => A.prototype.test
//A.test(); // => Error...
A.test = () => console.log("A.test");
A.test() // => A.test
a.test(); // A.prototype.test
a.__proto__.test(); // A.prototype.test
a.test = () => console.log("a.test");
a.test(); // a.test
a.__proto__.test(); // A.prototype.test
可以看到话告,對于對象A兼搏,它是無法直接通過A.test
訪問到·prototype
里的內(nèi)容的。對于實(shí)例a沙郭,如果有test則直接使用自己的test佛呻,如果沒有找到則會(huì)去其對象的prototype
(即__proto__
)里找。(當(dāng)然如果還沒找到會(huì)繼續(xù)找其父對象的prototype
)
到現(xiàn)在為止病线,基本可以理清prototype
的含義了吓著,基本就是一個(gè)特殊屬性鲤嫡,主要是服務(wù)于對象和實(shí)例之間的聯(lián)系以及對象之間的繼承關(guān)系。
繼承關(guān)系
上面分析了prototype
屬性的作用绑莺,但是只分析了其在對象和實(shí)例之間的紐帶作用暖眼。而它更主要的作用是用于實(shí)現(xiàn)繼承,js里的繼承關(guān)系就是基于prototype
實(shí)現(xiàn)的纺裁。盡管現(xiàn)在已經(jīng)引入了class诫肠,extends等這些關(guān)鍵字,不過這僅僅是語法糖而已欺缘,實(shí)際還是通過prototype
等關(guān)鍵字實(shí)現(xiàn)的栋豫。
開始之前,我們先確認(rèn)一下怎么才算繼承谚殊。
既然要繼承丧鸯,子對象肯定要有父對象所有的屬性,而且子對象實(shí)例化出來的變量應(yīng)該也是父對象的實(shí)例络凿。
具體的實(shí)現(xiàn)方法可以參考廖雪峰的JS入門教程骡送,這里給出另一種寫法,本質(zhì)是一樣的絮记。
function A(){}
function B(props){
A.call(this,props);
// ...
}
B.prototype = Object.create(A.prototype);
// 修復(fù)
B.prototype.constructor = B;
為啥非要通過F=>new F來轉(zhuǎn)換一遍呢摔踱?
直接復(fù)制過去了PrimaryStudent
和Student
就沒區(qū)別了啊,你想給PrimaryStudent
加一個(gè)新方法怨愤,你會(huì)發(fā)現(xiàn)Student
也有了該方法派敷。
為啥非要修復(fù)最后一個(gè)constructor?
事實(shí)上撰洗,這一步即便不執(zhí)行篮愉,在大部分場景下也不會(huì)出問題。為什么一定要保證prototype
里的constructor
指向?qū)ο蟊旧砟夭畹迹窟@類似C++里的多態(tài)试躏,具體分析可以參考下面的鏈接Stack Overflow關(guān)于prototype.constructor的討論。大致就是在基類中操作的某些通用函數(shù)需要知道處理誰设褐。
這樣做完以后颠蕴,所有從B實(shí)例化出去的變量都能夠直接使用A的方法,且都是A的實(shí)例化助析。
b = new B()
b.__proto__ === B.prototype // true
b.__protot__.__proto__ === A.prototype // true
b instanceof B // true
b instanceof A // true
如下:
console.log(A.prototype);// => {constructor: f}
//constructor是個(gè)函數(shù)犀被?
A.prototype.constructor === A // true wtf?
我們發(fā)現(xiàn)對象的prototype
屬性有一個(gè)constructor
屬性等于對象本身。
一等公民——Function
很多文章里都會(huì)說外冀,函數(shù)是js里的一等公民寡键,之前也就簡單理解為函數(shù)比較重要罷了,不過實(shí)際拿prototype
和__proto__
試了一下發(fā)現(xiàn)雪隧,Function
確實(shí)比較特殊西轩。
具體看下面代碼:
Object.__proto__ === Function.prototype; // true
Function.__proto__ === Function.prototype; // true
Function.__proto__ === Object.__proto__; // true
Function.__proto__.__proto__ === Object.prototype; //true
Function instanceof Object; // true
Object instanceof Function; // true
可以簡單總結(jié)為
- Function和Object都是Function的實(shí)例员舵。
- Function繼承自O(shè)bject。
- Function是Object類型藕畔。
- Object是Function類型固灵。
這么設(shè)計(jì)肯定有原因的,至于具體原因這里就不深入探討了劫流,不過確實(shí)只有Function有這些屬性巫玻。其他內(nèi)置的類型,例如Number,Date之類的都沒有這些等式。
從這里可以看到遥金,函數(shù)在js里確實(shí)比較特殊,這也是為什么經(jīng)常會(huì)看到函數(shù)被用作橋梁而其他的類型就不會(huì)诗力。
對象擴(kuò)展
有了prototype
的加持,我們可以隨意地對一個(gè)現(xiàn)有對象進(jìn)行擴(kuò)展我抠,比如下面這段代碼就是給Date加了一個(gè)format
方法苇本,功能與moment.js
里的format
類似。
Date.prototype["Format"] = function(fmt) {
fmt = fmt || "YYYY-MM-dd hh:mm:ss";
let o = {
"M+": this.getMonth() + 1, //月份
"d+": this.getDate(), //日
"h+": this.getHours(), //小時(shí)
"m+": this.getMinutes(), //分
"s+": this.getSeconds(), //秒
"q+": Math.floor((this.getMonth() + 3) / 3), //季度
S: this.getMilliseconds() //毫秒
};
if (/(y+)/.test(fmt))
fmt = fmt.replace(
RegExp.$1,
(this.getFullYear() + "").substr(4 - RegExp.$1.length)
);
for (let k in o)
if (new RegExp("(" + k + ")").test(fmt))
fmt = fmt.replace(
RegExp.$1,
RegExp.$1.length == 1
? o[k]
: ("00" + o[k]).substr(("" + o[k]).length)
);
return fmt;
}
}
有了這個(gè)擴(kuò)展菜拓,我們就可以寫出下面的代碼了:
let d = new Date();
console.log(d.format("yyyy-MM-dd:hh"));
// => 2017-09-09:10
console.log(Date.format("yyyy-MM-dd:hh"));
// TypeError
類似地瓣窄,我們同樣可以對對象本身進(jìn)行擴(kuò)展:
JSON["safeParse"] = function(
text,
reviver
) {
try {
return JSON.parse(text, reviver);
} catch (e) {
return null;
}
}
}
// parse
JSON.parse("{x:")
// => null
上面兩個(gè)例子可以看到,我們需要分清應(yīng)用的場景纳鼎,需要針對需求進(jìn)行擴(kuò)展俺夕。
這么擴(kuò)展的代價(jià)是什么?
這種擴(kuò)展方式很簡單粗暴贱鄙,不過這也會(huì)給我們帶來一些副作用(雖然大部分情況下都不會(huì)涉及)劝贸。
先看一下下面的代碼:
// somewhere unkown
Object.prototype.hello = () => console.log("hello");
// doing sth
let a = {};
a.name = "test";
a.age = "21";
a.roler = "adc";
for(let k in a) {
console.log(a[k].toString());
}
// => test
// => 21
// => adc
// => () => console.log("hello");
這里調(diào)用toString()
主要是為了清晰地看到問題所在————我們擴(kuò)展的對象會(huì)被in
操作符所遍歷,這個(gè)在數(shù)組上也會(huì)有類似的問題(不過數(shù)組對象可以使用of
來避免這個(gè)問題逗宁,這也是為什么大部分教程里都會(huì)建議使用of
來遍歷數(shù)組的原因)映九。前面知道所有的對象都來自Object
,所以對Object
的擴(kuò)展會(huì)影響到所有的in
關(guān)鍵字!!!
那怎么解決呢瞎颗?
為了避免這個(gè)問題件甥,后面有了hasOwnProperty
這個(gè)函數(shù)。我們只需要簡單地加一行代碼就行了言缤。
for(let k in a) {
if(!a.hasOwnProperty(k))continue;
console.log(a[k].toString());
}
在in
關(guān)鍵字存在的地方務(wù)必都加上這一判斷嚼蚀。如果沒有加這個(gè)關(guān)鍵字禁灼,即便現(xiàn)在程序沒有出錯(cuò)管挟,但是后續(xù)開發(fā)中一旦有人對關(guān)聯(lián)的對象做了擴(kuò)展,這塊代碼就有可能出錯(cuò)或者輸出跟預(yù)期不符弄捕,可以設(shè)想把上面的hello
函數(shù)換成Object.prototype.hello = "nevermore"
,這個(gè)時(shí)候程序不會(huì)報(bào)錯(cuò)僻孝,只是輸出錯(cuò)了导帝。這個(gè)時(shí)候就看可能出現(xiàn)一些詭異的bug,項(xiàng)目大的時(shí)候很難調(diào)試穿铆。
從上面我們可以看到您单,利用prototype
進(jìn)行擴(kuò)展會(huì)給系統(tǒng)帶來不小的隱患,尤其是對基礎(chǔ)對象(Object, Funtiong, Array)進(jìn)行擴(kuò)展的時(shí)候荞雏,因?yàn)槲覀冇肋h(yuǎn)也沒法保證其他人的代碼都是安全的虐秦。
怎么解決這一隱患呢?
為了解決這一問題凤优,又有了defineProperty
這一函數(shù)悦陋。借用這個(gè)函數(shù)我們可以更加安全地?cái)U(kuò)展對象,這里簡單地介紹一下筑辨,更加詳細(xì)的內(nèi)容請參考MDN文檔
該函數(shù)原型如下:Object.defineProperty(obj, prop, descriptor)
- obj: 我們需要擴(kuò)展的對象俺驶,例如:Date.prototype
- prop: 我們需要擴(kuò)展的函數(shù)(或變量)名稱,例如:"format"
- descriptor: 設(shè)置這一新屬性的特征棍辕。例如:
{enumerable: false, value: () => console.log("hello")}
表示不允許該屬性被枚舉到暮现,即不會(huì)被in
之類的迭代器遍歷到,且數(shù)值設(shè)置為一個(gè)函數(shù)楚昭。這個(gè)參數(shù)有下面6個(gè)配置項(xiàng)栖袋。- configurable: 是否允許修改或者刪除屬性特征。
- enumerable: 是否允許迭代器遍歷
- value: 屬性的值抚太。
- writable: 是否支持修改屬性值栋荸。這里的修改和configurable的修改管理的內(nèi)容不一樣,這里是確定屬性值是否允許修改凭舶,而上面是確認(rèn)配置項(xiàng)是否允許修改晌块。如果配置項(xiàng)允許修改我們可以再次調(diào)用
defineProperty
來把writable
修改成true
然后再修改該字段內(nèi)容也是可以的。 - get 見set
- set get/set是一對
getter
和setter
帅霜,它們是一套和value/writable互斥的配置匆背,不能同時(shí)設(shè)置,否則會(huì)報(bào)錯(cuò)身冀。其中get
跟getter
完全一致钝尸,就是相當(dāng)于吧該屬性變成了一個(gè)getter
。setter
則會(huì)在該屬性被修改的時(shí)候被調(diào)用搂根。
關(guān)于上面講到的get/set可以參考下面的代碼:
let _am_ = 0;
Object.defineProperty(Object.prototype, "game", {
get: function() { return Date.now() },
set: function(nV) { _am_ = nV + 1; },
});
let a = {};
setInterval(()=>(a.game = a.game) && console.log(`game: ${a.game}`) || console.log(`_am_: ${_am_}`), 1000);
// => game: 1504956090106
// => _am_: 1504956090090
// => game: 1504956091110
// => _am_: 1504956091111
這里就展示get=>set的工作過程珍促,每一次出現(xiàn)a.game=?
的操作的時(shí)候就會(huì)觸發(fā)set,可以嘗試把上面的賦值操作去掉剩愧,就會(huì)發(fā)現(xiàn)set不會(huì)被觸發(fā)(這個(gè)時(shí)候a.game還是會(huì)變化的)猪叙。
判斷對象
我們經(jīng)常會(huì)遇到需要判斷參數(shù)的類型的場景,比如我們有一個(gè)下面的函數(shù):
// 判斷請求的token的格式
handler = rgx => req => rgx.test(req.header("token"));
handler(()=>{});
// Error
為了安全起見,這個(gè)時(shí)候我們勢必希望能夠判斷rgx的類型穴翩。在使用nodejs做服務(wù)的時(shí)候犬第,類型判斷就是最討厭的問題之一。雖然js動(dòng)態(tài)語言的性質(zhì)導(dǎo)致了這一問題不可能完全解決芒帕,不過js還是提供了一些手段來做基本的類型判斷歉嗓。
typeof: typeof關(guān)鍵字會(huì)返回實(shí)例的對象,代碼如下:
typeof function(){} // => "function"
typeof 0 // => "number"
typeof null // => "object"
typeof undefined // => "undefined"
typoef "" // => "string"
typeof new RegExp(/^\d{11}$/) // => "object"
基本的類型typeof就可以確定了背蟆,不過上面的正則返回的是更底層的"object"鉴分。
instanceof: 該關(guān)鍵字就是用于判斷類型的。
/\d+/g instanceof RegExp // => true
/\d+/g instanceof Object // => true
關(guān)于instanceof的用法跟其他語言里基本一樣带膀,這里不再贅述冠场。
除了上面兩種寫法,還有下面這種寫法:
let typeOf = Object.prototype.toString;
typeOf.call(/^\d{11}$/) // => [object RegExp]
typeOf.apply(/^\d{11}$/) // => [object RegExp]
采用這種方法就可以找到一個(gè)對象更細(xì)致的類型了本砰,具體哪些就不列了碴裙,有興趣自己去嘗試吧。關(guān)于call和apply的用法可以參考下面鏈接理解call和apply