1.什么是閉包 用它的優(yōu)點(diǎn)和缺點(diǎn)专酗?
??通俗來講映琳,閉包就是子函數(shù)可以調(diào)用父函數(shù)的變量铜涉,但是這種說法不嚴(yán)謹(jǐn)溪北,更嚴(yán)格一點(diǎn)的定義是:函數(shù)對(duì)象可以通過作用域鏈相互聯(lián)系起來桐绒,函數(shù)體內(nèi)部的變量可以保存在函數(shù)作用域范圍內(nèi)
,它的優(yōu)點(diǎn)是:函數(shù)嵌套函數(shù)之拨,有利于封裝茉继,命名變量不沖突,而且子函數(shù)可以調(diào)用父函數(shù)的參數(shù)和變量蚀乔,不過它也有缺點(diǎn)烁竭,那就是:變量和參數(shù)不會(huì)被瀏覽器的垃圾回收機(jī)制回收,降低程序性能乙墙,在IE下還可能會(huì)導(dǎo)致內(nèi)存泄漏颖变,這個(gè)問題可以用手動(dòng)清空引用的方式,也就是設(shè)置Null來解決听想。
??注意:這里面試官可能會(huì)接著閉包繼續(xù)往下問腥刹,比如引起閉包的原因(涉及到作用域鏈,詳情請(qǐng)移步這里)汉买,看程序說出結(jié)果(提前刷題)衔峰,閉包中的this指向問題(高三182頁),以及內(nèi)存泄漏和垃圾回收機(jī)制(第11題)等蛙粘。
2.怎么解決回調(diào)函數(shù)嵌套的問題垫卤?以及底層工作原理?手寫一個(gè)promise
??回調(diào)函數(shù)就是指將一個(gè)函數(shù)作為參數(shù)出牧,傳遞給另一個(gè)函數(shù)穴肘,等那個(gè)函數(shù)執(zhí)行完以后,再執(zhí)行傳進(jìn)去的這個(gè)函數(shù)舔痕。(這句與問題無關(guān)评抚,可以不提,僅做筆記)
??首先ES6中的Promise就是專門為解決為解決回調(diào)函數(shù)問題而設(shè)計(jì)的伯复,它是一個(gè)對(duì)象慨代,包含三個(gè)狀態(tài)和兩個(gè)方法,當(dāng)狀態(tài)改變的時(shí)候啸如,就調(diào)用相應(yīng)的方法侍匙,然后通過.then()
來接收并執(zhí)行后續(xù)操作,還有Promise.race()
叮雳、Promise.all()
等一系列方法想暗,它可以規(guī)范化回調(diào),并且解決信任問題债鸡。
??但是Promise
也存在缺陷江滨,例如它無法中斷執(zhí)行,而且不設(shè)置回調(diào)函數(shù)的話厌均,就無法拋出異常唬滑,如果層級(jí)很深的話,依舊會(huì)存在嵌套問題棺弊,所以解決回調(diào)地獄晶密,還可以使用Generator
。
??Generator
中的鏈?zhǔn)秸?qǐng)求模她,可以隨時(shí)控制請(qǐng)求的執(zhí)行和中斷稻艰,比如讓正在執(zhí)行的A先暫停,轉(zhuǎn)去執(zhí)行B侈净,然后再返回來繼續(xù)執(zhí)行A尊勿。它需要在Funtion
和函數(shù)名之間加個(gè)*
號(hào)僧凤,在異步操作需要暫停的地方,用yield
注明元扔,然后使用next()
繼續(xù)下一步躯保,直到碰到下一個(gè)yield
或者return
語句,也可以使用for...of
語句來自動(dòng)遍歷澎语,用throw()
方法來獲取捕獲到的異常途事。(Generator
的缺點(diǎn)沒找到)
??最后還有一種async/ await
方法,它其實(shí)就是Generator
的語法糖擅羞,將原本的*
替換成async
尸变,寫在函數(shù)前面,然后把await
寫在async
函數(shù)里的减俏,相當(dāng)于告訴函數(shù)“先回去等會(huì)兒”召烂,等await
后面的異步操作執(zhí)行完畢后,再繼續(xù)過來執(zhí)行垄懂,async
返回的是一個(gè)Promise
對(duì)象骑晶,可以用then()
方法來繼續(xù)后面的操作,參數(shù)就是return
出來的內(nèi)容草慧,而且相比之前的方法桶蛔,async/ await
有更好的語義和適用性。
??(注意:await
后面緊跟著的一般是一個(gè)Promise
對(duì)象漫谷,如果不是仔雷,會(huì)強(qiáng)制轉(zhuǎn)換成一個(gè)立即resolve
的Promise
對(duì)象,同時(shí)要熟悉它的錯(cuò)誤機(jī)制舔示,這個(gè)比較重要)
??手寫Promise
代碼如下:
function myPromise(obj){
this.state = "pending"
this.value = undefined
this.error = undefined
this.resolveCallbackArr = []
this.rejectCallbackArr = []
var that = this
var resolve = function(val){
that.value = val
that.state = "fulfilled"
that.resolveCallbackArr.forEach(function(fn){
fn()
})
}
var reject = function(err){
that.error = err
that.state = "rejected"
that.rejectCallbackArr.forEach(function(fn){
fn()
})
}
obj(resolve,reject)
}
myPromise.prototype = {
then: function(onResolve,onReject){
var that = this
var promise2 = new myPromise(function(resolve,reject){
if(that.state == 'fulfilled'){
onResolve(that.value)
}
if(that.state == 'rejected'){
onReject(that.error)
}
if(that.state == 'pending'){
that.resolveCallbackArr.push(function(){
var x = onResolve(that.value)
console.log(x)
rePromise(x,resolve,reject)
})
that.rejectCallbackArr.push(function(){
onReject(that.error)
})
}
})
return promise2
}
}
function rePromise(x,resolve,reject){
if(typeof x == 'object'){
x.then(function(res){
resolve(res)
},function(err){
reject(err)
})
}else{
resolve(x)
}
}
??測(cè)試代碼:
var promise = new myPromise(function(resolve,reject){
setTimeout(function(){
resolve(200)
},1000)
// reject(500)
})
promise.then(function(res){
alert(res)
return new myPromise(function(resolve,reject){
setTimeout(function(){
resolve('200-2')
},1000)
})
},function(err){
alert(err)
}).then(function(res){
alert(res)
return '200-3'
},function(err){
alert(err)
}).then(function(res){
alert(res)
},function(err){
alert(err)
})
3.從輸入一個(gè)URL到頁面加載的過程碟婆?
??我理解的一共是有八個(gè)步驟:
??1. 首先輸入url地址,敲擊回車
??2. 這個(gè)時(shí)候?yàn)g覽器會(huì)先在緩存里查找有沒有對(duì)應(yīng)的IP地址
??3. 如果沒有的話惕稻,開始檢查本地hosts文件里是否有網(wǎng)址映射關(guān)系竖共,如果沒有,就在DNS解析器緩存里俺祠,以遞歸的方式進(jìn)行查找公给,如果還是沒有,則繼續(xù)向上蜘渣,在本地DNS服務(wù)器里淌铐,以根域服務(wù)器 => 頂級(jí)域 => 第二層域 => 子域的順序進(jìn)行迭代查詢,最后找到域名對(duì)應(yīng)的IP地址
??4. 然后客戶端就可以和對(duì)應(yīng)IP的服務(wù)器之間進(jìn)行連接了蔫缸,這里涉及到TCP的三次握手腿准,詳細(xì)過程如下所示(重在理解):
??5. 建立連接后,客戶端開始想服務(wù)器發(fā)送
http
請(qǐng)求拾碌,包括起始行吐葱、請(qǐng)求頭和請(qǐng)求主體??6. 服務(wù)端接收到請(qǐng)求后街望,對(duì)數(shù)據(jù)進(jìn)行處理,然后以
http
的Response
對(duì)象格式返回給客戶端弟跑,包括狀態(tài)碼它匕、響應(yīng)頭和響應(yīng)報(bào)文??7. 之后瀏覽器開始處理接收到的頁面文檔,首先將
HTML
解析成DOM
樹窖认,然后將CSS
解析成樣式結(jié)構(gòu)體,最后將兩者結(jié)合生成render tree
??(注1:這里可能會(huì)問具體的解析過程告希,簡(jiǎn)單來說就是
CSS
不會(huì)阻塞解析扑浸,但會(huì)阻塞渲染render tres
,而JS
則會(huì)阻塞瀏覽器解析HTML
文檔)??(注2:這里還可能涉及到
回流和重繪
的問題)??8. 最后數(shù)據(jù)傳輸完成燕偶,可以關(guān)閉連接喝噪,這就涉及到了TCP的四次揮手(可以只提一嘴,不必展開細(xì)說)指么,詳細(xì)過程如下所示(重在理解):
4.清除浮動(dòng)幾種方式酝惧?原理和適用場(chǎng)景
??清除浮動(dòng)常見的有兩種方式:
??1. 使用clear:both;
屬性清除浮動(dòng),也就是使用偽元素伯诬,在包含浮動(dòng)的父元素(例如class="clearfix"
)加入如下代碼:
//非IE瀏覽器下:
.clearfix:after{
content: '';
display: block;
height: 0;
clear: both;
}
//IE瀏覽器下:
.clearfix {
*zoom: 1
}
??即可清除浮動(dòng)晚唇,其原理是在被清除浮動(dòng)的元素的上邊和下面添加足夠的垂直外邊距
??2. 用overflow: hidden;
觸發(fā)父元素變成BFC,代碼如下:
.clearfix {
overflow: hidden;
}
??這樣也可以清除浮動(dòng)盗似,其原理是利用BFC哩陕,讓浮動(dòng)元素也參與父元素高度的計(jì)算
??(注意:這里面試官可能會(huì)問BFC
的定義、原理及相關(guān)內(nèi)容赫舒,這樣就可以順帶扯到margin
的父子拖拽問題)
5.為什么有跨域悍及?簡(jiǎn)述幾種跨域方式
??引起跨域問題的是瀏覽器的同源策略,(有這一句夠了接癌,也可以繼續(xù)往下補(bǔ)充)瀏覽器為了防止XSS
和CSFR
的攻擊心赶,于是設(shè)置了同源策略,它規(guī)定頁面的腳本只能訪問位于同一個(gè)源下的數(shù)據(jù)缺猛,所謂同源是指協(xié)議缨叫、域名、端口號(hào)三者相同枯夜,否則瀏覽器就報(bào)錯(cuò)弯汰。
??解決跨域的方式有很多:
??1. 首先可以使用CORS,也就是跨域資源共享湖雹,它由Server
來進(jìn)行設(shè)置咏闪,客戶端在正式通信前,會(huì)先發(fā)送一次“預(yù)檢”請(qǐng)求摔吏,如果請(qǐng)求的域名在后臺(tái)的許可名單之中鸽嫂,會(huì)返回一個(gè)肯定答復(fù)纵装,瀏覽器就可以正式發(fā)送請(qǐng)求了。它有簡(jiǎn)單請(qǐng)求和非簡(jiǎn)單請(qǐng)求兩種据某。
??2. 還有可以通過nginx
反向代理來進(jìn)行跨域
??3. 還可以開啟谷歌瀏覽器的DeBug
模式橡娄,在本地開發(fā)時(shí)進(jìn)行跨域,這也是我在項(xiàng)目中最常用到的方式癣籽,因?yàn)楣镜捻?xiàng)目在正式上線后挽唉,都位于同一域名下,不會(huì)存在跨域問題的
??4. 最后我還了解一種JSONP
方法筷狼,它的核心原理是利用了所有具有src
屬性的HTML
標(biāo)簽可以跨域訪問腳本這一特性瓶籽,具體來說,就是
???a. 先在客戶端注冊(cè)一個(gè)回調(diào)函數(shù)埂材,比如callback
???b. 然后動(dòng)態(tài)的創(chuàng)建一個(gè)script
標(biāo)簽塑顺,將其src
值設(shè)置為請(qǐng)求地址,同時(shí)在后面添加參數(shù)和回調(diào)函數(shù)名俏险,也就是callback
???c. 之后服務(wù)端對(duì)這個(gè)請(qǐng)求進(jìn)行處理严拒,并返回callback(data)
,data
是服務(wù)端返回給前端的數(shù)據(jù)
???d. 最后客戶端接收到返回的這段js腳本竖独,因?yàn)橹白?cè)過callback
這個(gè)函數(shù)裤唠,所以會(huì)立即執(zhí)行函數(shù)體,這樣就完成了跨域
??不過JSONP
也有一定的局限性莹痢,就是它只支持get
請(qǐng)求(注意:這里可能會(huì)引導(dǎo)面試官往get
巧骚、post
請(qǐng)求,或者http
協(xié)議上問)格二,而且同樣也需要Server
的支持劈彪。
??嗯...我知道的跨域方式就只有這么多(羞澀~)
6.判斷數(shù)據(jù)類型幾種方式?及bug和解決方法
??JS中一共有七種數(shù)據(jù)類型顶猜,一種是引用類型——Object
沧奴,還有六種基本數(shù)據(jù)類型,分別是Number
长窄、String
滔吠、Boolean
、Null
挠日、Undifined
,以及ES6新增的Symbol
??所有的數(shù)據(jù)類型都可以用typeof(var)
來檢測(cè)疮绷,它返回的是一個(gè)字符串,但是對(duì)于復(fù)雜數(shù)據(jù)類型來說嚣潜,不管是數(shù)組Array
冬骚、日期Date
,或者是普通對(duì)象,這種方法返回的都是Object
只冻,無法更詳細(xì)的區(qū)分庇麦,所以可以用(var) instranceof (type)
方法來判斷,但是這種方法在iframe
下會(huì)產(chǎn)生bug
喜德,而且這種方法也無法準(zhǔn)確判斷Function
和Object
的類型山橄,因?yàn)榧瓤梢哉f函數(shù)是個(gè)構(gòu)造方法,也可以說方法是一個(gè)對(duì)象舍悯,萬物皆對(duì)象嘛~所以區(qū)分它們兩個(gè)的時(shí)候航棱,應(yīng)該使用(var).Constructor == (type)
,也就是構(gòu)造器方法萌衬,不過這種方法在類繼承的時(shí)候同樣有可能產(chǎn)生bug
丧诺。
??最后還有一種萬能的方法,可以判斷所有數(shù)據(jù)類型奄薇,而且也沒有任何Bug
[內(nèi)心竊喜臉~],就是用Object
原型對(duì)象上的toString
來判斷抗愁,具體寫法是Object.prototype.toString.call(var) == '[Object (type)]'
馁蒂,這樣無論什么類型,都可以準(zhǔn)確知道它的類型了蜘腌。
7.手寫一個(gè)webpack配置(package.json沫屡、webpack.config.js)
8.排序和去重的算法至少能各自手寫兩種(要求性能最高的)
9.Generator、async的用法和區(qū)別(自個(gè)封裝npm)
??可以和上述第二題合并解答撮珠,一同添加至知識(shí)體系中沮脖,這里不再重復(fù)贅述
??npm什么的等會(huì)兒再說啦!P炯薄勺届!
10.手寫3種以上Es5的面向?qū)ο罄^承,以及說說class類繼承的this指向問題娶耍?
??我知道的面向?qū)ο罄^承一共有六種方式免姿,其中最常用的是組合繼承方式,它涉及到了原型繼承和構(gòu)造函數(shù)繼承榕酒。
??1. 原型鏈繼承 這種方式其實(shí)就是通過原型和原型鏈的方式胚膊,讓一個(gè)原型對(duì)象和另一個(gè)類型的實(shí)例相等,來實(shí)現(xiàn)繼承想鹰,寫法如下:
function Father(){} //定義父類
Father.prototype.say = function(){ //父類方法
alert("我是父類的方法")
}
function Son(name,age){ //定義子類紊婉,有name和age屬性
this.name = name
this.age = age
}
Son.prototype = new Father() //實(shí)現(xiàn)繼承
Son.prototype.sayName = function(){ //子類方法
alert(this.name)
}
//后面可以實(shí)例化對(duì)象,如:
var son = new Son("亞當(dāng)",23)
son.say() //我是父類的方法
son.sayName() //亞當(dāng)
??這種方法有很多缺陷辑舷,比如1. 無法確定實(shí)例和原型的關(guān)系喻犁,2. 用字面量添加方法時(shí)會(huì)重寫原型連,導(dǎo)致繼承無效,3. 而且如果屬性中存在引用類型的話株汉,多個(gè)實(shí)例訪問時(shí)都會(huì)指向同一塊內(nèi)存地址筐乳,相互之間存在影響,4. 創(chuàng)建子類的實(shí)例時(shí)也無法向父類中傳遞參數(shù)乔妈。
??2. 構(gòu)造函數(shù)繼承 這種方式是通過call()
或者apply()
調(diào)用父類的構(gòu)造函數(shù)蝙云,代碼如下:
function Father(name){ //定義子類,有name屬性
this.name = name
this.sayName = function(){
alert(this.name)
}
}
function Son(age){ //定義子類路召,有age屬性
Father.call(this,"亞當(dāng)")
this.age = age
}
var son = new Son()
son.sayName() //亞當(dāng)
??這種方法可以再構(gòu)造時(shí)向父類傳遞參數(shù)勃刨,也可以正常使用引用類型的數(shù)據(jù),但是它將所有方法都放在了構(gòu)造函數(shù)里股淡,每次實(shí)例化都會(huì)創(chuàng)建一個(gè)一模一樣的函數(shù)身隐,復(fù)用性太差,而且父類里面定義的方法唯灵,在子類中也不可見贾铝,所以基本不會(huì)單獨(dú)使用
??3.組合繼承 組合的意思其實(shí)就是將前兩種方法結(jié)合起來,把每個(gè)實(shí)例獨(dú)有的屬性放在構(gòu)造函數(shù)里埠帕,將共享的方法放在原型鏈中垢揩,這樣每個(gè)對(duì)象既有各自獨(dú)立的屬性,又有可以共享的方法敛瓷,代碼如下:
function Father(name){
this.name = name
}
Father.prototype.sayName = function(){
alert(this.name)
}
function Son(age){
Father.call(this,"亞當(dāng)")
this.age = age
}
Son.prototype = new Father()
Son.prototype.constructor = Son
Son.prototype.sayAge = function(){
alert(this.age)
}
??組合繼承是使用最廣泛的一種方式叁巨,不過它同樣存在不足之處,那就是無論什么情況下呐籽,都會(huì)調(diào)用兩次父類的構(gòu)造函數(shù)锋勺,一次是在創(chuàng)建子類通過原型繼承父類的時(shí)候,另一次是在子類型構(gòu)造函數(shù)內(nèi)部狡蝶,用call()
或者apply()
調(diào)用父類到時(shí)候庶橱,這樣就創(chuàng)建了兩次同名的屬性,只不過后面這次把前面的屬性覆蓋了而已贪惹。
??所以基于這種缺陷悬包,后來就又有了寄生式組合繼承的方法,它可以彌補(bǔ)組合繼承的不足馍乙,而這個(gè)方法又涉及到寄生式繼承和原型式繼承布近,不過后面這幾種方法不太常用,就不詳細(xì)說了丝格。
??(注意:如果只了解前三種方法撑瞧,說到組合繼承就可以了,也不要說組合繼承的不足显蝌,否則引出后面的幾種方式無異于給自己挖坑)
??簡(jiǎn)單說一下后幾種预伺,1. 原型式繼承就是Object.creat()
方法订咸,可以傳入兩個(gè)參數(shù),存在引用類型方面的缺陷酬诀;2. 寄生式繼承用于封裝函數(shù)的繼承過程脏嚷,在內(nèi)部可以進(jìn)行某些自定義處理,需要先用到原型式繼承瞒御,它無法做到函數(shù)復(fù)用父叙;3. 寄生式組合繼承,其原理是通過構(gòu)造函數(shù)來繼承屬性肴裙,通過原型鏈的混成形式來繼承方法趾唱,簡(jiǎn)單來說,就是不用每次都調(diào)用父類型的構(gòu)造函數(shù)蜻懦,而是先拷貝一個(gè)原型的副本下來甜癞,之后繼承的都是這個(gè)副本,這樣就可以只調(diào)用父類型一次宛乃,實(shí)現(xiàn)繼承悠咱,不過這種方式過于繁瑣,所以除非必要征炼,一般還是使用組合繼承的方式析既。
??(注:ES6的類繼承,以及相關(guān)的this
指向問題還沒寫柒室,之后再補(bǔ))
11.說說垃圾回收機(jī)制,還有內(nèi)存泄漏什時(shí)候會(huì)出現(xiàn)以及解決方法
??JS的垃圾回收機(jī)制有兩種逗宜,第一種是標(biāo)記清除雄右,這也是目前主流的垃圾收集算法,它的思想是給當(dāng)前不使用的值加上標(biāo)記纺讲,然后再回收它們的內(nèi)存擂仍;還有一種引用計(jì)數(shù)方法,這種算法的思想是根據(jù)跟蹤記錄所有值被引用的次數(shù)熬甚,引用數(shù)為0時(shí)逢渔,即收回內(nèi)存,JS引擎目前都不再使用這種算法乡括,但是IE中訪問非原生JS對(duì)象肃廓,比如DOM
元素時(shí)悲幅,這種算法仍然可能導(dǎo)致問題易结。
??內(nèi)存泄漏是在IE瀏覽器下晦溪,使用閉包操作DOM元素的時(shí)候產(chǎn)生的同眯,具體來說杰妓,如果閉包的作用域中保存著一個(gè)HTML元素突诬,它會(huì)創(chuàng)建一個(gè)循環(huán)引用顿苇,導(dǎo)致該元素的引用數(shù)至少也是1界酒,永遠(yuǎn)無法被銷毀。
??內(nèi)存泄漏可以通過手動(dòng)消除引用的方式來解決绘迁,就是先將調(diào)用的DOM元素保存在一個(gè)副本中合溺,在閉包中只引用這個(gè)副本來解除循環(huán)引用,然后再將包含DOM對(duì)象的變量設(shè)置為null
缀台,這樣就能解除DOM對(duì)象的引用棠赛,確保能正常回收其占用的內(nèi)存将硝。
12.用定時(shí)器寫時(shí)鐘 解決誤差的問題
??定時(shí)器存在誤差恭朗,這個(gè)涉及到了瀏覽器的線程問題,每個(gè)瀏覽器都包含一個(gè)JS處理線程依疼,和一個(gè)定時(shí)觸發(fā)線程痰腮,JS腳本在單線程的執(zhí)行過程中如果遇到了定時(shí)器,會(huì)先將其放入一個(gè)任務(wù)隊(duì)列律罢,等待JS線程處理完當(dāng)前操作之后膀值,再轉(zhuǎn)過來執(zhí)行隊(duì)列里的任務(wù),所以這一小段時(shí)間误辑,便是產(chǎn)生誤差的原因沧踏。
??所以在使用定時(shí)器,比如做時(shí)鐘效果的時(shí)候巾钉,可以用當(dāng)前時(shí)間 - 開始時(shí)間翘狱,來取得中間的時(shí)間差,然后根據(jù)時(shí)間差來動(dòng)態(tài)計(jì)算指針劃過的刻度砰苍,這樣就可以減少因?yàn)閳?zhí)行過程所帶來的誤差值潦匈。