前端筆記(一) 變量,執行環境與做用域,this

ECMAScript中的變量值類型

  • 基本類型 : Number, String, Boolean, Undefined, Null
  • 引用類型 : Object, Array, Function, Date, RegExp

在將一個值賦給變量時解析器必須肯定這個值是基本類型值仍是引用類型值。javascript

基本數據類型是按值訪問的, 由於能夠操做保存在變量中的實際的值。前端

引用類型則不一樣, 它的值是保存在堆內存中的對象, 而JavaScript不容許直接訪問內存中的位置。java

因此在操做對象時其實是在操做對象的引用, 即引用類型的值是按引用訪問的。瀏覽器

基本類型的特色 :閉包

1.值不會改變 2. 不能夠添加屬性和方法app

var name = "BarryAllen";

name.substring(5); //"Allen"
console.log(name) //BarryAllen  

name.identity = "Flash";
console.log(name.identity) //undefined 

name.skill = function() {
    console.log("Running very fast.")
}
name.skill(); //name.skill is not a function

引用類型的特色 :ide

1.值能夠被修改 2. 能夠添加屬性和方法函數

var obj = {};
obj.name = "BarryAllen";

var change = obj;
change.name = "OliverQueen";
console.log(obj.name); //OliverQueen

obj.identity = "Flash";
console.log(obj.identity) //Flash

obj.skill = function() {
    console.log("Running very fast.")
}
obj.skill(); //Running very fast.

從上面的代碼不難看出在進行復制變量的時候基本類型進行的是相似建立副本的操做, 而引用類型則是對指向對象的指針的複製因此在複製操做結束後兩個變量將引用同一個對象。所以改變其中一個變量就會影響到另外一個變量。this

參數的傳遞

ECMAScript中規定全部函數的參數都是按值傳遞的。prototype

function setAge(obj) {
    obj.age = 18;
    obj = {};
    obj.age = 25;
}
var person = {}
setAge(person);
console.log(person.age) //18

在函數內部從新聲明瞭對象並修改了obj.age 的值, 若參數傳遞是按引用傳遞的那麼person.age 應該輸出25, 但事實卻不是這樣。因爲此時對象是按值傳遞, 故原始的引用仍然未變。事實上在函數被執行完畢後這個新建立的局部對象就會被當即銷燬。

檢測類型
  • typeof 用於檢測基本類型
  • instanceof 在檢測引用類型時, 用於判斷它是什麼類型的對象( 由於全部引用類型的值都是Object的實例 )。
var num = 786;
var bol = true;
var name = "Violet";
console.log(typeof num +"~"+ typeof bol +"~"+ typeof name); //number~boolean~string

var arr = [];
var func = new Function();
console.log(arr instanceof Array) //true
console.log(func instanceof Function) //true

執行環境(Execution Context)與做用域

執行環境也被稱爲執行上下文, 每個執行環境中都有一個關聯的變量對象, 環境中定義的全部變量和函數都保存在這個對象中。

在Javascript中有三種代碼的執行環境 :

  • 全局執行環境 --- 默認的最外圍的執行環境, 在瀏覽器中其關聯的變量對象被認爲是window對象
  • 函數執行環境 --- 每當調用一個函數時, 一個新的執行上下文就會被建立出來
  • Eval --- 接受字符串做爲參數, 並將其做爲javascript代碼去運行, eval函數並不會建立新的做用域

每次新建立的一個執行上下文會被添加到做用域鏈的頂部,有時也稱爲執行或調用棧。瀏覽器老是運行位於做用域鏈頂部的當前執行上下文。一旦完成,當前執行上下文將從棧頂被移除而且將控制權歸還給以前的執行上下文。

下面來詳細講解一下函數執行環境的創建過程:

  • 創建階段

    • 創建arguments對象, 參數, 函數, 變量 ( 注意建立的順序 !)
    • 創建做用域鏈
    • 肯定this的值
  • 代碼執行階段

    • 變量賦值
    • 函數引用
    • 執行其餘代碼
(function (obj) {
  console.log(typeof obj); //number
  console.log(typeof foo); //function
  console.log(typeof boxer); //undefined
  var foo = "Mashics";
   function foo() {
      document.write("This is a function.");
   }
  var boxer = function() {
      document.write("I am a boxer.");
   }
})(666);

這段代碼充分說明了函數執行環境創建再到執行的過程, 即首先是參數的建立, 而後再是在函數體內去尋找函數的聲明, 最後是變量聲明。值得注意的是當javascript引擎在尋找函數聲明時首先找到了foo 這個函數, 於是以後定義的變量則不會從新覆蓋其屬性, 引擎接下來就開始查找具體代碼段裏面的變量聲明並添加到關聯變量對象的屬性中,並將其賦值爲undefined , 於是像變量提高這種經典的問題又能夠從執行環境建立過程的角度來回答並解決了。

做用域鏈與閉包

當代碼在一個環境中執行時, 會建立變量對象的一個做用域鏈, 其用途就是保證對執行環境有權訪問的全部變量和函數的有序訪問。做用域的前端永遠是當前執行代碼所在環境的變量對象, 而全局執行環境的變量對象始終是做用域鏈中的最後一個對象。在進行變量查找的時候就是經過做用域鏈一級一級的向上查找。而閉包中的一部分特性則是由做用域鏈這個重要特性決定的。

var outer = "Margin";
function foo() {
  var mider = "Padding";
    function baz() {
       var inner = "Content";
       console.log( "Gotcha! " + outer + " and " + mider + " . " );
    }
  return baz;
}
var fn = foo();
fn(); //Gotcha! Margin and Padding . 
console.log(inner); //inner is not defined.

這段代碼是一個簡單的閉包, 但它卻說明了做用域鏈中最重要的特性:

!!即內部環境能夠經過做用域鏈訪問全部外部環境, 但外部環境不能訪問內部環境中的任何變量和函數 !!

PS : 另外再解釋一下幾個容易使人混淆或者說是難懂的概念。

  • 變量對象與活動對象
    • 變量對象在執行環境的創建階段被建立, 在未進入執行階段以前其中的屬性不能被訪問, 而當其進入執行階段後變量對象變爲活動對象, 接下來就能夠進行執行階段中的步驟了。
  • 做用域與做用域鏈
    • 做用域與執行環境是兩個徹底不一樣的概念, javascript代碼執行的過程其實有兩個階段即代碼編譯階段和代碼執行階段, 做用域是在編譯階段建立的一套規則, 用來管理引擎如何在當前做用域以及嵌套的子做用域中根據標識符名稱進行變量查找, 而執行上下文的建立則是在代碼執行階段進行的。做用域鏈是由一系列變量對象組成, 咱們能夠在這個單向通道中, 查詢變量對象中的標識符, 這樣就能夠訪問到上一層做用域中的變量了。

this詳解

在理解this的綁定過程以前, 必需要理解調用棧和調用位置這兩個概念, 由於this的綁定徹底取決於從調用棧中分析出的調用位置。而調用位置就在當前正在執行的函數的前一個調用中。

  1. 調用棧:爲了達到當前執行位置所調用的全部函數。
  2. 調用位置:函數在代碼中被調用的位置( 而不是聲明的位置 )。
function head() {
   //當前調用棧爲head
   console.log("first");
   body(); //body的調用位置 --> head
}
function body() {
   //當前調用棧爲head -> body
   console.log("second");
   footer(); //footer的調用位置 --> body
}
function footer() {
  //當前的調用棧爲head -> body -> footer
   console.log("third");
}
head();  //head的調用位置 --> 全局做用域
this綁定規則:
  1. 默認綁定
  2. 隱式綁定
  3. 顯示綁定
  4. new綁定
  • 默認綁定

當函數獨立調用, 即直接使用不帶任何修飾的函數引用進行調用時this使用默認綁定, 此時this指向全局對象。

var a = 2;
function foo() {
   console.log( this.a );
}
foo(); // 2
  • 隱式綁定

當函數引用有上下文對象時, 隱式綁定規則會把函數調用中的this綁定到這個上下文對象。

var obj = {
   a : 2,
   foo : foo
}
function foo() {
   console.log( this.a );
}
obj.foo(); //2

由於調用foo()時this被綁定到obj, 所以這裏的this 至關於obj

隱式丟失

當隱式綁定的函數被顯式或者隱式賦值時會丟失綁定對象, 從而把this綁定到全局對象上或者undefined上。而在回調函數中的this綁定會丟失也正是由於參數傳遞其實就是一種隱式賦值。

var a = "Global";
var obj = {
   a : 2,
   foo : foo
}
function foo() {
   console.log( this.a );
}
var bar = obj.foo;
bar(); //Global ->顯示賦值

function doFoo(fn) {
   fn();
}
doFoo( obj.foo ); //Global ->隱式賦值

setTimeout(obj.foo, 1000); //Global ->內置函數中的隱式賦值,相似於下面這段代碼

function setTimeout(fn, delay) {
  //等待delay毫秒
  fn();
}
  • 顯式綁定

經過Function.prototype 中的call , apply , bind 來直接指定this的綁定對象。

call和apply都是當即執行的函數, 而且接受參數的形式不一樣。

bind則是建立一個新的包裝函數而且返回, 而不是執行。

var obj = {
   a : 2
}
function foo() {
   console.log( this.a );
}
var bar = function() {
   foo.call(obj);
}
bar(); //2  -->硬綁定

function calculate(b, c) {
   console.log(this.a, b, c);
   return (this.a * b) + c;
}
var excute = function() {
   return calculate.apply(obj, arguments); //apply方法可接受參數並將變量傳遞到下層函數
}
excute(5,10); //2 5 10 20

var baz = calculate.bind(obj); //bind方法將this綁定到obj對象上
baz(8,5); //2 8 5 21
  • new綁定

在JavaScript中構造函數只是一些使用new操做符時被調用的普通函數, 即發生構造函數調用時會執行如下操做:

  1. 建立一個全新的對象
  2. 新對象被執行[[Prototype]]鏈接
  3. 新對象會綁定到函數調用的this
  4. 若函數沒有返回對象, new表達式中的函數調用會自動返回這個新對象
function Foo(a) {
    this.a = a;
}
var bar = new Foo(6);
console.log( bar.a ); //6
  • 優先級

new綁定 > 顯式綁定 > 隱式綁定 > 默認綁定


  • this與箭頭函數

ES6中的箭頭函數並不使用this的四種標準原則,它是根據外層( 函數或者全局 )做用域來決定this。

先來看下一種常見的this綁定丟失情景:

function foo() {
   setTimeout(function() {
     console.log( this.a );
   },1000);
}
var obj = {
   a : 2
}
foo.call(obj); //undefined

這裏因爲setTimeout中發生的隱式丟失於是this應用了默認規則, 於是輸出undefined 。那麼如何將this綁定到咱們想要的obj對象上呢?

var obj = {
   a : 2
}
function foo() {
   setTimeout( () => {
      console.log( this.a );
   },1000);
}
foo.call(obj) //2

顯然箭頭函數中的this 在詞法上繼承了foo , 於是它會捕獲調用時foo的this, 即this被綁定到obj對象上。


參考書籍: 《JavaScript高級程序設計》《你不知道的JavaScript》(上)

相關文章
相關標籤/搜索