1.理解詞法作用域和動態作用域
2.理解JavaScript的作用域和作用域鏈
3.理解JavaScript的執行上下文棧,可以應用堆棧信息快速定位問題
4.this的原理以及幾種不同使用場景的取值
5.閉包的實現原理和作用,可以列舉幾個開發中閉包的實際應用
6.理解堆棧溢出和內存泄漏的原理,如何防止
7.如何處理循環的異步操作
1.理解詞法作用域和動態作用域
詞法作用域,也叫靜態作用域,它的作用域是指在詞法分析階段就確定了,不會改變。動態作用域是在運行時根據程序的流程信息來動態確定的,而不是寫代碼時進行靜態確定的。
需要明確的是,Javascript并不具有動態作用域,它只有詞法作用域,簡單明了。但是,它的 eval()
、with
、this
機制某種程度上很像動態作用域,使用上要特別注意。
2.理解JavaScript的作用域和作用域鏈
作用域是在運行代碼中的某些特定部分中的變量,函數和對象的可訪問性。作用于決定了代碼區塊中變量和其他資源的可見性。作用域最大的用處就是隔離變量,不同作用域下同名變量不會有沖突。
ES6 之前 JavaScript 沒有塊級作用域,只有全局作用域和函數作用域。ES6 的到來,為我們提供了‘塊級作用域’,可通過新增命令 let 和 const 來體現。
當代碼在一個環境中執行時,會創建變量對象的一個作用域鏈。由子級作用域返回父級作用域中尋找變量,就叫做作用域鏈。作用域鏈是保證執行環境有權訪問的所有變量和函數的有序訪問。
延長作用域鏈:
執行環境的類型只有兩種,全局和局部(函數)。但是有些語句可以在作用域鏈的前端臨時增加一個變量對象,該變量對象會在代碼執行后被移除。
具體來說就是執行這兩個語句時,作用域鏈都會得到加強。
1、try - catch 語句的catch塊;會創建一個新的變量對象,包含的是被拋出的錯誤對象的聲明。
2、with 語句。with 語句會將指定的對象添加到作用域鏈中
3.理解JavaScript的執行上下文棧,可以應用堆棧信息快速定位問題
執行上下文是評估和執行JavaScript代碼的環境的抽象概念。每當JavaScript代碼在運行的時候,它都是在執行上下文中運行。
執行棧,也就是其他編程語言中所說的“調用棧”,是一種擁有LIFO(后進先出)數據結構的棧,被用來存儲代碼運行時創建的所有執行上下文。
執行上下文總共有三種類型
- 全局執行上下文
- 函數執行上下文
- Eval函數執行上下文
執行上下文的聲明周期包含三個階段創建階段
→執行階段
→回收階段
創建階段
- 創建變量對象
- 創建作用域鏈
- 確定this 指向
4.this的原理以及幾種不同使用場景的取值
場景一:構造函數
所謂構造函數就是用來new對象的函數。其實嚴格來說,所有的函數都可以new一個對象,但是有些函數的定義是為了new一個對象,而有些函數則不是。另外注意,構造函數的函數名第一個字母大寫(規則約定)。例如:Object、Array、Function等
function Foo(){
this.name='Apple',
this.type='fruit';
console.log(this);//Foo{name:'Apple',type:'fruit'}
}
var f1=new Foo();
console.log(f1.name,f1.type)//Apple,fruit
以上代碼中,如果函數作為構造函數用,那么其中的this就代表它即將new出來的對象。
注意:
以上僅限new Foo()的情況,即Foo函數作為構造函數的情況。如果直接調用Foo函數,而不是new Foo(),情況就大不一樣了。
function Foo(){
this.name='Apple',
this.type='fruit';
console.log(this);//Window{top:Window,window:Window……}
}
Foo();
這種情況下this是window。
在構造函數的prototype中,this代表什么。
function Fn(){
this.name='Apple',
this.type='fruit';
}
Fn.prototype.getName=function(){
console.log(this.name)
}
var f1=new Fn();
f1.getName();//Apple
如上代碼,在Fn.prototype.getName函數中,this指向的是f1對象。因此可以通過this.name獲取f1.name的值。
其實,不僅僅是構造函數的prototype,即便是在整個原型鏈中,this代表的也都是當前對象的值。
場景二:函數作為對象的一個屬性
如果函數作為對象的一個屬性時,并且作為對象的一個屬性被調用時,函數中的this指向該對象。
var obj={
x:10,
fn:function(){
console.log(this);//Object{x:10,fn:function}
}
}
obj.fn();
以上代碼中,fn不僅作為一個對象的一個屬性,而且的確是作為對象的一個屬性被調用。結果this就是obj對象。
var obj={
x:10,
fn:function(){
console.log(this); //Window{top:Window,window:Window……}
}
}
var fn1=obj.fn;
fn1();
以上代碼,如果fn函數被復制到了另一個變量中,并沒有作為obj的一個屬性被調用,那么this的值就是window,this.x就是undefined。
場景三:函數用call或者apply調用
當一個函數被call和apply調用時,this的值就取傳入的對象的值。
var obj = {
x:10
}
var fn = function(){
console.log(this); //Object{x:10}
console.log(this.x); //10
}
fn.call(obj);
場景四:全局&調用普通函數
在全局環境下,this永遠是window。
console.log(this === window);//true
普通函數在調用時,其中的this也都是window。
var x= 10;
var fn = function (){
console.log(this);//Window{top:Window;window:Window……}
console.log(this.x);//10
}
fn()
下面情況要注意:
var obj = {
x:10,
fn:function(){
function f(){
console.log(this)//Window{top:Window;window:Window……}
console.log(this.x);//undefined
}
f();
}
}
obj.fn();
函數f雖然是在obj.fn內部定義的,但是它仍然是一個普通的函數,this仍然指向window。
5.閉包的實現原理和作用,可以列舉幾個開發中閉包的實際應用
閉包是一個擁有許多變量和綁定了這些變量的環境表達式(通常是一個函數),因而這些變量也是該表達式的一部分。換句話說,JavaScript中所有的function都是一個閉包。
一般來說,嵌套的function所產生的閉包更為強大。
function a() {
var i = 0;
function b() { alert(++i); } //函數b嵌套在函數a內部;
return b;//函數a返回函數b。
}
var c = a();
c();
這樣在執行完var c=a()后,變量c實際上是指向了函數b,再執行c()后就會彈出一個窗口顯示i的值(第一次為1)。這段代碼其實就創建了一個閉包,為什么?因為函數a外的變量c引用了函數a內的函數b,就是說:
當函數a的內部函數b被函數a外的一個變量引用的時候,就創建了一個閉包.
所謂“閉包”,就是在構造函數體內定義另外的函數作為目標對象的方法函數,而這個對象的方法函數反過來引用外層函數體中的臨時變量。這使得只要目標 對象在生存期內始終能保持其方法,就能間接保持原構造函數體當時用到的臨時變量值。盡管最開始的構造函數調用已經結束,臨時變量的名稱也都消失了,但在目 標對象的方法內卻始終能引用到該變量的值,而且該值只能通這種方法來訪問。即使再次調用相同的構造函數,但只會生成新對象和方法,新的臨時變量只是對應新 的值,和上次那次調用的是各自獨立的。
簡而言之,閉包的作用就是在a執行完并返回后,閉包使得Javascript的垃圾回收機制GC不會收回a所占用的資源,因為a的內部函數b的執行需要依賴a中的變量。這是對閉包作用的非常直白的描述,不專業也不嚴謹,但大概意思就是這樣,理解閉包需要循序漸進的過程。
當定義函數a的時候,js解釋器會將函數a的作用域鏈(scope chain)
設置為定義a時a所在的“環境”,如果a是一個全局函數,則scope chain
中只有window對象。
當執行函數a的時候,a會進入相應的執行環境(excution context)
。
在創建執行環境的過程中,首先會為a添加一個scope
屬性,即a的作用域
,其值就為第1步中的scope chain。即a.scope=a的作用域鏈
。
然后執行環境會創建一個活動對象(call object)。活動對象也是一個擁有屬性的對象,但它不具有原型而且不能通過JavaScript代碼直接訪問。創建完活動對象后,把活動對象添加到a的作用域鏈的最頂端。此時a的作用域鏈包含了兩個對象:a的活動對象和window對象。
下一步是在活動對象上添加一個arguments屬性,它保存著調用函數a時所傳遞的參數。
最后把所有函數a的形參和內部的函數b的引用也添加到a的活動對象上。在這一步中,完成了函數b的的定義,因此如同第3步,函數b的作用域鏈被設置為b所被定義的環境,即a的作用域。
到此,整個函數a從定義到執行的步驟就完成了。此時a返回函數b的引用給c,又函數b的作用域鏈包含了對函數a的活動對象的引用,也就是說b可以訪問到a中定義的所有變量和函數。函數b被c引用,函數b又依賴函數a,因此函數a在返回后不會被GC回收。
當函數b執行的時候亦會像以上步驟一樣。因此,執行時b的作用域鏈包含了3個對象:b的活動對象、a的活動對象和window對象,如下圖所示:
如圖所示,當在函數b中訪問一個變量的時候,搜索順序是:
先搜索自身的活動對象,如果存在則返回,如果不存在將繼續搜索函數a的活動對象,依次查找,直到找到為止。
如果函數b存在prototype原型對象,則在查找完自身的活動對象后先查找自身的原型對象,再繼續查找。這就是Javascript中的變量查找機制。
如果整個作用域鏈上都無法找到,則返回undefined。
小結,本段中提到了兩個重要的詞語:函數的定義與執行。文中提到函數的作用域是在定義函數時候就已經確定,而不是在執行的時候確定。用一段代碼來說明這個問題
function f(x) {
var g = function () { return x; }
return g;
}
var h = f(1);
alert(h());
這段代碼中變量h指向了f中的那個匿名函數(由g返回)。
假設函數h的作用域是在執行alert(h())確定的,那么此時h的作用域鏈是:h的活動對象->alert的活動對象->window對象。
假設函數h的作用域是在定義時確定的,就是說h指向的那個匿名函數在定義的時候就已經確定了作用域。那么在執行的時候,h的作用域鏈為:h的活動對象->f的活動對象->window對象。
如果第一種假設成立,那輸出值就是undefined;如果第二種假設成立,輸出值則為1。
運行結果證明了第2個假設是正確的,說明函數的作用域確實是在定義這個函數的時候就已經確定了。
閉包的應用場景
保護函數內的變量安全。以最開始的例子為例,函數a中i只有函數b才能訪問,而無法通過其他途徑訪問到,因此保護了i的安全性。
在內存中維持一個變量。依然如前例,由于閉包,函數a中i的一直存在于內存中,因此每次執行c(),都會給i自加1。
通過保護變量的安全實現JS私有屬性和私有方法(不能被外部訪問)
私有屬性和方法在Constructor外是無法被訪問的
function Constructor(...) {
var that = this;
var membername = value;
function membername(...) {...}
}
以上3點是閉包最基本的應用場景,很多經典案例都源于此。
在Javascript中,如果一個對象不再被引用,那么這個對象就會被GC回收。如果兩個對象互相引用,而不再被第3者所引用,那么這兩個互相引用的對象也會被回收。因為函數a被b引用,b又被a外的c引用,這就是為什么函數a執行后不會被回收的原因。
var聲明的變量由于不存在塊級作用域所以可以在全局環境中調用,而let聲明的變量由于存在塊級作用域所以不能在全局環境中調用。
var a=[];
for(var i=0;i<10;i++){
a[i]=function(){
console.log(i);
};
}
a[6](); //10
var b=[];
for(let i=0;i<10;i++){
b[i]=function(){
console.log(i);
};
}
b[6]();//6
6.理解堆棧溢出和內存泄漏的原理,如何防止
內存泄露:指一塊被分配的內存既不能使用,又不能回收,直到瀏覽器進程結束
JS的回收機制
JavaScript垃圾回收的機制很簡單:找出不再使用的變量,然后釋放掉其占用的內存,但是這個過程不是實時的,因為其開銷比較大,所以垃圾回收系統(GC)會按照固定的時間間隔,周期性的執行。
到底哪個變量是沒有用的?所以垃圾收集器必須跟蹤到底哪個變量沒用,對于不再有用的變量打上標記,以備將來收回其內存。用于標記的無用變量的策略可能因實現而有所區別,通常情況下有兩種實現方式:標記清除
和引用計數
。引用計數不太常用,標記清除較為常用
標記清除
js中最常用的垃圾回收方式就是標記清除。當變量進入環境時,例如,在函數中聲明一個變量,就將這個變量標記為“進入環境”。從邏輯上講,永遠不能釋放進入環境的變量所占用的內存,因為只要執行流進入相應的環境,就可能會用到它們。而當變量離開環境時,則將其標記為“離開環境”。
function test(){
var a=10;//被標記,進入環境
var b=20;//被標記,進入環境
}
test();//執行完畢之后a、b又被標記離開環境,被回收
引用計數
引用計數的含義是跟蹤記錄每個值被引用的次數。當聲明了一個變量并將一個引用類型值(function object array)賦給該變量時,則這個值的引用次數就是1。如果同一個值又被賦給另一個變量,則該值的引用次數加1。相反,如果包含對這個值引用的變量又取得了另外一個值,則這個值的引用次數減1。當這個值的引用次數變成0時,則說明沒有辦法再訪問這個值了,因而就可以將其占用的內存空間回收回來。這樣,當垃圾回收器下次再運行時,它就會釋放那些引用次數為0的值所占用的內存。
function test(){
var a={};//a的引用次數為0
var b=a;//a的引用次數加1,為1
var c=a;//a的引用次數加1,為2
var b={};//a的引用次數減1,為1
}
哪些情況會造成內存泄露
1.意外的全局變量引起的內存泄露
function leak(){
leak="xxx";//leak成為一個全局變量,不會被回收
}
2.閉包引起的泄露
function bindEvent(){
var obj=document.createElement("XXX");
obj.οnclick=function(){
//Even if it's a empty function
}
}
閉包可以維持函數內局部變量,使其得不到釋放。 上例定義事件回調時,由于是函數內定義函數,并且內部函數--事件回調的引用外暴了,形成了閉包。
解決之道,將事件處理函數定義在外部,解除閉包,或者在定義事件處理函數的外部函數中,刪除對dom的引用。
//將事件處理函數定義在外部
function onclickHandler(){
//do something
}
function bindEvent(){
var obj=document.createElement("XXX");
obj.οnclick=onclickHandler;
}
//在定義事件處理函數的外部函數中,刪除對dom的引用
function bindEvent(){
var obj=document.createElement("XXX");
obj.οnclick=function(){
//Even if it's a empty function
}
obj=null;
}
3.沒有清理的DOM元素引用
var elements={
button: document.getElementById("button"),
image: document.getElementById("image"),
text: document.getElementById("text")
};
function doStuff(){
image.src="http://some.url/image";
button.click():
console.log(text.innerHTML)
}
function removeButton(){
document.body.removeChild(document.getElementById('button'))
}
- 被遺忘的定時器或者回調
var someResouce=getData();
setInterval(function(){
var node=document.getElementById('Node');
if(node){
node.innerHTML=JSON.stringify(someResouce)
}
},1000)
這樣的代碼很常見, 如果 id 為 Node 的元素從 DOM 中移除, 該定時器仍會存在, 同時, 因為回調函數中包含對 someResource 的引用, 定時器外面的 someResource 也不會被釋放。
5.子元素存在引起的內存泄露
黃色是指直接被 js變量所引用,在內存里,紅色是指間接被 js變量所引用,如上圖,refB 被 refA 間接引用,導致即使 refB 變量被清空,也是不會被回收的子元素 refB 由于 parentNode 的間接引用,只要它不被刪除,它所有的父元素(圖中紅色部分)都不會被刪除。
怎樣避免內存泄露
1)減少不必要的全局變量,或者生命周期較長的對象,及時對無用的數據進行垃圾回收;
2)注意程序邏輯,避免“死循環”之類的 ;
3)避免創建過多的對象 原則:不用了的東西要及時歸還。
7.如何處理循環的異步操作
1.不需要等待結果的異步循環
async function processArray(array) {
array.forEach(async (item) => {
await func(item);
})
console.log('Done!');
}
function delay() {
return new Promise(resolve => setTimeout(resolve, 300));
}
async function delayedLog(item) {
// notice that we can await a function
// that returns a promise
await delay();
console.log(item);
}
async function processArray(array) {
array.forEach(async (item) => {
await delayedLog(item);
})
console.log('Done!');
}
processArray([1, 2, 3]);
結果輸出為
Done!
1
2
3
如果不需要等結果這樣寫是ok的,但是在大多數案例里這不是個很好的邏輯。
2.線性處理數組
要等待結果,我們應該返回到老式的 for 循環,但這一次為了更好的可讀性我們可以使用現代寫法 for..of
。
sync function processArray(array) {
for (const item of array) {
await delayedLog(item);
}
console.log('Done!');
}
結果輸出:
1
2
3
Done!
該代碼將依次處理每一項。但是我們可以使用并行運行。
3.并行處理數組
async function processArray(array) {
// map array to promises
const promises = array.map(delayedLog);
// wait until all promises are resolved
await Promise.all(promises);
console.log('Done!');
}
這段代碼將并行運行許多delayLog 任務。但是對于非常大的數組要小心(并行的任務太多對CPU或內存來說可能比較吃力)。
也不要混淆“并行”與真正的線程和并行。該代碼不能保證真正的并行執行。這取決于您的 item函數(在本演示中是delayedLog)。網絡請求、webworker 和其他一些任務可以并行執行。
參考鏈接:
http://www.lxweimin.com/p/70b38c7ab69c
http://www.lxweimin.com/p/2c3c8890dff0
http://www.frontopen.com/1702.html
https://www.cnblogs.com/wangfupeng1988/p/3988422.html
https://blog.csdn.net/michael8512/article/details/77888000
http://www.lxweimin.com/p/9dd1014f7f1c