javascript基礎知識問答-作用域和閉包

1.理解詞法作用域和動態作用域
2.理解JavaScript的作用域和作用域鏈
3.理解JavaScript的執行上下文棧,可以應用堆棧信息快速定位問題
4.this的原理以及幾種不同使用場景的取值
5.閉包的實現原理和作用,可以列舉幾個開發中閉包的實際應用
6.理解堆棧溢出和內存泄漏的原理,如何防止
7.如何處理循環的異步操作

1.理解詞法作用域和動態作用域

詞法作用域,也叫靜態作用域,它的作用域是指在詞法分析階段就確定了,不會改變。動態作用域是在運行時根據程序的流程信息來動態確定的,而不是寫代碼時進行靜態確定的。

需要明確的是,Javascript并不具有動態作用域,它只有詞法作用域,簡單明了。但是,它的 eval()withthis機制某種程度上很像動態作用域,使用上要特別注意。

2.理解JavaScript的作用域和作用域鏈

作用域是在運行代碼中的某些特定部分中的變量,函數和對象的可訪問性。作用于決定了代碼區塊中變量和其他資源的可見性。作用域最大的用處就是隔離變量,不同作用域下同名變量不會有沖突。

ES6 之前 JavaScript 沒有塊級作用域,只有全局作用域和函數作用域。ES6 的到來,為我們提供了‘塊級作用域’,可通過新增命令 let 和 const 來體現。

當代碼在一個環境中執行時,會創建變量對象的一個作用域鏈。由子級作用域返回父級作用域中尋找變量,就叫做作用域鏈。作用域鏈是保證執行環境有權訪問的所有變量和函數的有序訪問。

延長作用域鏈:
執行環境的類型只有兩種,全局和局部(函數)。但是有些語句可以在作用域鏈的前端臨時增加一個變量對象,該變量對象會在代碼執行后被移除。
具體來說就是執行這兩個語句時,作用域鏈都會得到加強。
1、try - catch 語句的catch塊;會創建一個新的變量對象,包含的是被拋出的錯誤對象的聲明。
2、with 語句。with 語句會將指定的對象添加到作用域鏈中

3.理解JavaScript的執行上下文棧,可以應用堆棧信息快速定位問題

執行上下文是評估和執行JavaScript代碼的環境的抽象概念。每當JavaScript代碼在運行的時候,它都是在執行上下文中運行。

執行棧,也就是其他編程語言中所說的“調用棧”,是一種擁有LIFO(后進先出)數據結構的棧,被用來存儲代碼運行時創建的所有執行上下文。

執行上下文總共有三種類型

  1. 全局執行上下文
  2. 函數執行上下文
  3. 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的作用鏈

如圖所示,當在函數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個假設是正確的,說明函數的作用域確實是在定義這個函數的時候就已經確定了。

閉包的應用場景

  1. 保護函數內的變量安全。以最開始的例子為例,函數a中i只有函數b才能訪問,而無法通過其他途徑訪問到,因此保護了i的安全性。

  2. 在內存中維持一個變量。依然如前例,由于閉包,函數a中i的一直存在于內存中,因此每次執行c(),都會給i自加1。

  3. 通過保護變量的安全實現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'))
}
  1. 被遺忘的定時器或者回調

var someResouce=getData();
setInterval(function(){
    var node=document.getElementById('Node');
    if(node){
        node.innerHTML=JSON.stringify(someResouce)
    }
},1000)

這樣的代碼很常見, 如果 id 為 Node 的元素從 DOM 中移除, 該定時器仍會存在, 同時, 因為回調函數中包含對 someResource 的引用, 定時器外面的 someResource 也不會被釋放。

5.子元素存在引起的內存泄露


圖片.png

黃色是指直接被 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

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,967評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,273評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,870評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,742評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,527評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,010評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,108評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,250評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,769評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,656評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,853評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,371評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,103評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,472評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,717評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,487評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,815評論 2 372