【進階2-1期】深入淺出圖解作用域鏈和閉包

(關注福利,關注本公眾號回復[資料]領取優質前端視頻,包括Vue、React、Node源碼和實戰、面試指導)

本周開始前端進階的第二期,本周的主題是作用域閉包,今天是第6天。

本計劃一共28期,每期重點攻克一個面試重難點,如果你還不了解本進階計劃,點擊查看前端進階的破冰之旅

如果覺得本系列不錯,歡迎轉發,您的支持就是我堅持的最大動力。

本期推薦文章

從作用域鏈談閉包 ,由于微信不能訪問外鏈,點擊閱讀原文就可以啦。

推薦理由

這是一篇譯文,深入淺出圖解作用域鏈,一步步深入介紹閉包。看完本文對于相關知識的邏輯會清晰很多。

閱讀筆記

紅寶書(p178)上對于閉包的定義:閉包是指有權訪問另外一個函數作用域中的變量的函數
關鍵在于下面兩點:

  • 是一個函數
  • 能訪問另外一個函數作用域中的變量

對于閉包有下面三個特性:

  • 1、閉包可以訪問當前函數以外的變量
function getOuter(){
  var date = '815';
  function getDate(str){
    console.log(str + date);  //訪問外部的date
  }
  return getDate('今天是:'); //"今天是:815"
}
getOuter();
  • 2、即使外部函數已經返回,閉包仍能訪問外部函數定義的變量
function getOuter(){
  var date = '815';
  function getDate(str){
    console.log(str + date);  //訪問外部的date
  }
  return getDate;     //外部函數返回
}
var today = getOuter();
today('今天是:');   //"今天是:815"
today('明天不是:');   //"明天不是:815"
  • 3、閉包可以更新外部變量的值
function updateCount(){
  var count = 0;
  function getCount(val){
    count = val;
    console.log(count);
  }
  return getCount;     //外部函數返回
}
var count = updateCount();
count(815); //815
count(816); //816

作用域鏈

Javascript中有一個執行上下文(execution context)的概念,它定義了變量或函數有權訪問的其它數據,決定了他們各自的行為。每個執行環境都有一個與之關聯的變量對象,環境中定義的所有變量和函數都保存在這個對象中。

詳情查看 【進階1-2期】JavaScript深入之執行上下文棧和變量對象

作用域鏈:當訪問一個變量時,解釋器會首先在當前作用域查找標示符,如果沒有找到,就去父作用域找,直到找到該變量的標示符或者不在父作用域中,這就是作用域鏈。

作用域鏈和原型繼承查找時的區別:如果去查找一個普通對象的屬性,但是在當前對象和其原型中都找不到時,會返回undefined;但查找的屬性在作用域鏈中不存在的話就會拋出ReferenceError

作用域鏈的頂端是全局對象,在全局環境中定義的變量就會綁定到全局對象中。

全局環境

無嵌套的函數
// my_script.js
"use strict";

var foo = 1;
var bar = 2;

function myFunc() {
  
  var a = 1;
  var b = 2;
  var foo = 3;
  console.log("inside myFunc");
  
}

console.log("outside");
myFunc();

定義時:當myFunc被定義的時候,myFunc的標識符(identifier)就被加到了全局對象中,這個標識符所引用的是一個函數對象(myFunc function object)。

內部屬性[[scope]]指向當前的作用域對象,也就是函數的標識符被創建的時候,我們所能夠直接訪問的那個作用域對象(即全局對象)。

image

myFunc所引用的函數對象,其本身不僅僅含有函數的代碼,并且還含有指向其被創建的時候的作用域對象。

調用時:當myFunc函數被調用的時候,一個新的作用域對象被創建了。新的作用域對象中包含myFunc函數所定義的本地變量,以及其參數(arguments)。這個新的作用域對象的父作用域對象就是在運行myFunc時能直接訪問的那個作用域對象(即全局對象)。

image
有嵌套的函數

當函數返回沒有被引用的時候,就會被垃圾回收器回收。但是對于閉包,即使外部函數返回了,函數對象仍會引用它被創建時的作用域對象。

"use strict";
function createCounter(initial) {
  var counter = initial;
  
  function increment(value) {
    counter += value;
  }
  
  function get() {
    return counter;
  }
  
  return {
    increment: increment,
    get: get
  };
}

var myCounter = createCounter(100);
console.log(myCounter.get());   // 返回 100

myCounter.increment(5);
console.log(myCounter.get());   // 返回 105

當調用 createCounter(100) 時,內嵌函數increment和get都有指向createCounter(100) scope的引用。假設createCounter(100)沒有任何返回值,那么createCounter(100) scope不再被引用,于是就可以被垃圾回收。

image

但是createCounter(100)實際上是有返回值的,并且返回值被存儲在了myCounter中,所以對象之間的引用關系如下圖:

image

即使createCounter(100)已經返回,但是其作用域仍在,并且只能被內聯函數訪問。可以通過調用myCounter.increment() 或 myCounter.get()來直接訪問createCounter(100)的作用域。

當myCounter.increment() 或 myCounter.get()被調用時,新的作用域對象會被創建,并且該作用域對象的父作用域對象會是當前可以直接訪問的作用域對象。

調用get()時,當執行到return counter時,在get()所在的作用域并沒有找到對應的標示符,就會沿著作用域鏈往上找,直到找到變量counter,然后返回該變量。

image

單獨調用increment(5)時,參數value保存在當前的作用域對象。當函數要訪問counter時,沒有找到,于是沿著作用域鏈向上查找,在createCounter(100)的作用域找到了對應的標示符,increment()就會修改counter的值。除此之外,沒有其他方式來修改這個變量。閉包的強大也在于此,能夠存貯私有數據。

image

創建兩個函數:myCounter1myCounter2

//my_script.js
"use strict";
function createCounter(initial) {
  /* ... see the code from previous example ... */
}

//-- create counter objects
var myCounter1 = createCounter(100);
var myCounter2 = createCounter(200);

關系圖如下


image

myCounter1.increment和myCounter2.increment的函數對象擁有著一樣的代碼以及一樣的屬性值(name,length等等),但是它們的[[scope]]指向的是不一樣的作用域對象。

參考

從作用域鏈談閉包

往期文章查看

每周計劃安排

每周面試重難點計劃如下,如有修改會通知大家。每周一期,為期半年,準備明年跳槽的小伙伴們可以把本公眾號[置頂]了。

  • 【進階1期】 調用堆棧
  • 【進階2期】 作用域閉包
  • 【進階3期】 this全面解析
  • 【進階4期】 深淺拷貝原理
  • 【進階5期】 原型Prototype
  • 【進階6期】 高階函數
  • 【進階7期】 事件機制
  • 【進階8期】 Event Loop原理
  • 【進階9期】 Promise原理
  • 【進階10期】Async/Await原理
  • 【進階11期】防抖/節流原理
  • 【進階12期】模塊化詳解
  • 【進階13期】ES6重難點
  • 【進階14期】計算機網絡概述
  • 【進階15期】瀏覽器渲染原理
  • 【進階16期】webpack配置
  • 【進階17期】webpack原理
  • 【進階18期】前端監控
  • 【進階19期】跨域和安全
  • 【進階20期】性能優化
  • 【進階21期】VirtualDom原理
  • 【進階22期】Diff算法
  • 【進階23期】MVVM雙向綁定
  • 【進階24期】Vuex原理
  • 【進階25期】Redux原理
  • 【進階26期】路由原理
  • 【進階27期】VueRouter源碼解析
  • 【進階28期】ReactRouter源碼解析

交流

本人Github鏈接如下,歡迎各位Star

http://github.com/yygmind/blog

我是木易楊,網易高級前端工程師,跟著我每周重點攻克一個前端面試重難點。接下來讓我帶你走進高級前端的世界,在進階的路上,共勉!

如果你想加群討論每期面試知識點,公眾號回復[加群]即可


?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容