詞法作用域和動態(tài)作用域
作用域:
- 作用域是指程序源代碼中定義變量的區(qū)域;作用域規(guī)定了如何查找變量,也就是確定當(dāng)前執(zhí)行代碼對變量的訪問權(quán)限窖式;
- JavaScript 采用詞法作用域(lexical scoping)港柜,也就是靜態(tài)作用域。
詞法作用域
函數(shù)的作用域在函數(shù)定義的時候就決定了胳赌。JavaScript 采用的是詞法作用域牢撼。
動態(tài)作用域
函數(shù)的作用域是在函數(shù)調(diào)用的時候才決定
例子:
var name ="logic"
function getName(){
return name;
}
function getNameWrap(){
var name = 'wind'
return getName()
}
console.log(getNameWrap()) // 輸出 logic 。因?yàn)?JavaScript 是詞法作用域疑苫,在函數(shù)定義時就確定了變量的訪問熏版。
作用域鏈
定義:當(dāng)查找變量的時候,會先從當(dāng)前上下文的變量查找捍掺,如果沒有找到撼短,就從父級(詞法層面的父級)執(zhí)行上下文的變量對象查找,一直找到全局上下文的變量對象挺勿,也就是全局對象曲横。由多個執(zhí)行上下文的變量對象構(gòu)成的鏈,就是作用域鏈不瓶。
作用:保證對執(zhí)行環(huán)境有權(quán)訪問的所有變量和函數(shù)的有序訪問
js執(zhí)行上下文和執(zhí)行上下文棧
執(zhí)行上下文
概念:當(dāng) JavaScript 代碼執(zhí)行一段可執(zhí)行代碼(executable code)時禾嫉,會創(chuàng)建對應(yīng)的執(zhí)行上下文(execution context)
執(zhí)行上下有三個重要屬性(也就是前面 3 題所說的內(nèi)容) :變量對象;作用域鏈蚊丐;this
執(zhí)行上下文棧
每個函數(shù)都會創(chuàng)建執(zhí)行上下文熙参,執(zhí)行上下文棧(Execution context stack,ECS)就是 JavaScript 引擎創(chuàng)建出來管理執(zhí)行上下文的
執(zhí)行上下文棧是有全局上下文初始化麦备,由于執(zhí)行代碼首先是全局代碼孽椰。全局上下文永遠(yuǎn)在執(zhí)行上下文棧中的最底下,只有等程序關(guān)閉才釋放凛篙。
函數(shù)聲明和函數(shù)表達(dá)式的區(qū)別
f1: 聲明式創(chuàng)建的函數(shù) f1 可以在 f1 定義之前就進(jìn)行調(diào)用弄屡;
f2: 函數(shù)表達(dá)式創(chuàng)建的函數(shù) f2 不能在 f2 被賦值之前進(jìn)行調(diào)用
出現(xiàn)這個陷阱的本質(zhì)原因體現(xiàn)在這兩種類型在Javascript function hoisting(函數(shù)提升)和運(yùn)行時機(jī)(解析時/運(yùn)行時)上的差異。
函數(shù)聲明在JS解析時進(jìn)行函數(shù)提升鞋诗,因此在同一個作用域內(nèi)膀捷,不管函數(shù)聲明在哪里定義,該函數(shù)都可以進(jìn)行調(diào)用
函數(shù)表達(dá)式的值是在JS運(yùn)行時確定削彬,并且在表達(dá)式賦值完成后全庸,該函數(shù)才能調(diào)用。
this 幾種不同場景的取值
顯示綁定
call融痛,apply壶笼,bind可以顯示的修改this的指向
隱式綁定
- 全局上下文:this 指向 window,嚴(yán)格模式下為 undefined
- 直接調(diào)用函數(shù):this 指向 window,嚴(yán)格模式下為 undefined
- 作為對象的方法調(diào)用:那個對象調(diào)用,this指向那個對象雁刷;obj.foo()覆劈。 this 指向?qū)ο?obj
- DOM 事件的綁定:onclick和addEventerListener中 this 默認(rèn)指向綁定事件的元素;
setTimeout的函數(shù)內(nèi)this是window(非嚴(yán)格模式下) - new 構(gòu)造函數(shù)綁定:構(gòu)造函數(shù)中的 this 指向?qū)嵗龑ο?/li>
- 箭頭函數(shù):
箭頭函數(shù)沒有 this, 因此也不能綁定
在箭頭函數(shù)里的 this 會指向 外層的非箭頭函數(shù)的 this。
閉包
當(dāng)函數(shù)可以記住并訪問所在的詞法作用域時责语,就產(chǎn)生了閉包炮障,即使函數(shù)是在當(dāng)前詞法作用域之外執(zhí)行(通俗一點(diǎn): 即使創(chuàng)建它的上下文已經(jīng)銷毀,它仍然存在(比如坤候,內(nèi)部函數(shù)從父函數(shù)中返回) 在代碼中引用了自由變量)
通過例子來了解閉包
例子1:
function fn1() {
var name = 'iceman';
function fn2() {
console.log(name);
}
return fn2;
}
var fn3 = fn1();
fn3();
正常來說胁赢,當(dāng)fn1函數(shù)執(zhí)行完畢之后,其作用域是會被銷毀的白筹,然后垃圾回收器會釋放那段內(nèi)存空間智末。而閉包卻很神奇的將fn1的作用域存活了下來,fn2的詞法作用域能訪問fn1的作用域徒河。fn2依然持有該作用域的引用系馆,這個引用就是閉包。
總結(jié):某個函數(shù)在定義時的詞法作用域之外的地方被調(diào)用顽照,閉包可以使該函數(shù)極限訪問定義時的詞法作用域
例子2:
function waitSomeTime(msg, time) {
setTimeout(function () {
console.log(msg)
}, time);
}
waitSomeTime('hello', 1000);
定時器中有一個匿名函數(shù)它呀,該匿名函數(shù)就有涵蓋waitSomeTime函數(shù)作用域的閉包,因此當(dāng)1秒之后棒厘,該匿名函數(shù)能輸出msg
例子3:
for (var i = 1; i <= 10; i++) {
setTimeout(function () {
console.log(i);
}, 1000);
}
// 輸出了 10個 11
i是聲明在全局作用中的下隧,定時器中的匿名函數(shù)也是執(zhí)行在全局作用域中,那當(dāng)然是每次都輸出11了淆院。
解決方案:我們可以讓i在每次迭代的時候,都產(chǎn)生一個私有的作用域土辩,在這個私有的作用域中保存當(dāng)前i的值。
for (var i = 1; i <= 10; i++) {
(function (j) {
setTimeout(function () {
console.log(j);
}, 1000);
})(i);
}
閉包的應(yīng)用場景
https://juejin.im/post/5d7747185188254dc43a610a
https://juejin.im/post/5d50c6ef6fb9a06b1d21358d
什么是堆棧溢出拷淘?什么是內(nèi)存泄漏?那些操作會造成內(nèi)存泄漏启涯?怎么樣防止內(nèi)存泄漏贬堵?
堆棧溢出:代碼執(zhí)行前都會進(jìn)行編譯和創(chuàng)建執(zhí)行上下文结洼。而管理執(zhí)行上下文的叫做執(zhí)行上下文棧黎做。棧有個特點(diǎn)就是后進(jìn)先出。同時執(zhí)行上下文棧是有大小限制的松忍。當(dāng)執(zhí)行上下文棧大小超過限制就會產(chǎn)生棧溢出錯誤蒸殿。經(jīng)常發(fā)生在遞歸中。
內(nèi)存泄漏:JavaScript是在創(chuàng)建變量(對象,字符串等)時自動進(jìn)行了分配內(nèi)存宏所,并且在不使用它們時“自動”釋放酥艳。 釋放的過程稱為垃圾回收。不在使用楣铁,沒有釋放的內(nèi)存玖雁,稱為內(nèi)存泄漏
造成內(nèi)存泄漏的原因:閉包,意外的全局變量盖腕,循環(huán)(在兩個對象彼此引用且彼此保留時赫冬,就會產(chǎn)生一個循環(huán)),脫離 DOM 的引用
示例:
// 意外全局變量
function foo(arg) {
bar = "this is a hidden global variable";
}
// 脫離 DOM 的引用
const button = document.getElementById('button');
document.body.removeChild(button);
// 此時溃列,仍舊存在一個全局的 #button 的引用,button 元素仍舊在內(nèi)存中劲厌,不能被 GC 回收。
// 閉包
(function (){
let num = 1;
return function add(a,b){
return a+b;
}
})()
如何處理循環(huán)的異步操作
先上一段代碼
function getMoney(){
var money=[100,200,300]
for( let i=0; i<money.length; i++){
compute.exec().then(()=>{
console.log(money[i])
//alert(i)
})
}
}
//compute.exec()這是個異步方法,在里面處理一些實(shí)際業(yè)務(wù)
//這時候打印出來的很可能就是300,300,300(因?yàn)楫惒絝or循環(huán)還沒有等異步操作返回Promise對象過來i值已經(jīng)改變)
正確處理思路
關(guān)鍵字async/await async告訴getMoney方法里面存在異步的操作听隐,await放在具體異步操作(方法)前面补鼻,意思是等待該異步返回Promise才會繼續(xù)后面的操作
async function getMoney(){
var money=[100,200,300]
for( let i=0; i<money.length; i++){
await compute.exec().then(()=>{
console.log(money[i])
//alert(i)
})
}
}
另外一種處理思路:使用遞歸
用遞歸來實(shí)現(xiàn)自我循環(huán)(具體循環(huán)在then里面,可以確保前面的compute.exec()的異步操作完成).then()是返回了Promise對象為resolve后才進(jìn)行的(可以了解一下Promise對象)
function getMoney(i) {
var money=[100,200,300]
compute.exec().then(() => {
if ( i < money.length ) {
console.log(money[i]);
i++;
getMoney(i);
}
});
}
getMoney(0);//開始調(diào)用