JS的函式篇(4.3W字)

Fengmaybe發表於2019-02-23

本系列將從以下專題去總結:

1. JS基礎知識深入總結
2. 物件高階
3. 函式高階
4. 事件物件與事件機制

暫時會對以上四個專題去總結,現在開始Part3:函式高階。下圖是我這篇的大綱。

js函式學習大綱

3.1 this的使用總結

this是在函式執行的過程中自動建立的一個指向一個物件的內部指標。確切的說,this並不是一個物件,而是指向一個已經存在的物件的指標,也可以認為是this就是儲存了某個物件的地址。

this的指向不是固定的,會根據呼叫的不同,而指向不同的地方。 此章節的講解思路是這樣的:

this總結

3.1.1 搞懂this指向的預備知識

  • 對在全域性作用域中定義的變數和函式的進一步認識

    永遠記住:只要是在全域性作用域宣告的任何變數和函式預設都是作為window物件的屬性而存在的.

    理解完以上的這句話,我們在來說明一下其中的區別,這是很多人沒有關注過的。

    console.log(window.a);  //undefined
    console.log(a);   //報錯!a is not defined 
    複製程式碼

    註解:也就是在未對變數(a)進行宣告時,就會出現以上結果。首先明確一點,就是全域性變數awindow的屬性。所以,我們從這裡就可以發現,什麼時候undefined,什麼時候報錯?——那就是如果是訪問一個物件的屬性時,它沒有宣告賦值,那就是undefined;如果訪問一個變數,它沒有宣告賦值,那就是報錯。

    好,現在回頭過來,我們看全域性作用域變數和函式的認識。

    <script type="text/javascript">
          var num = 10;      //全域性作用域宣告的變數
          function sum () {  //全域性作用域宣告的函式
              alert("我是一個函式");
          }
          alert(window.num);  // 10
          window.sum();       // 我是一個函式
          // 在呼叫的時候window物件是可以省略的。
       </script>
    複製程式碼
  • 建構函式和非建構函式的澄清

    在JavaScript中建構函式和非建構函式沒有本質的區別。唯一的區別只是呼叫方式的區別。

    • 使用new 就是建構函式
    • 直接呼叫就是非建構函式

    看一個示例程式碼:

    <script type="text/javascript">
          function Person () {
              this.age = 20;
              this.sex = "男";
          }
          //作為建構函式呼叫,建立一個物件。 這個時候其實是給p新增了兩個屬性
          var p = new Person();
          alert(p.age + "  " + p.sex);
    
          //作為普通函式傳遞,其實是給 window物件新增了兩個屬性 
          //任何函式本質上都是通過某個物件來呼叫的,如果沒有直接指定就是window,也就是Window可省略
          Person();
          alert(window.age + " " + window.sex);
      </script>
    複製程式碼

3.1.2 第一個方向:全域性作用域中的this指向

全域性作用域中使用this,也就是說不在任何的函式內部使用this,那麼這個時候this就是指的 Window

<script type="text/javascript">
	//全域性作用域中的this
    //向this物件指代的物件中新增一個屬性 num, 並讓屬性的值為100
    this.num = 100;
    // 因為this就是window,所以這時是在修改屬性num的值為200
    window.num = 200;
    alert(this === window);  // true this就是指向的window物件,所以是恆等 
    alert(this.num);    //200   
    alert(window.num);  //200
</script>
複製程式碼

3.1.3 第二個方向:函式中的this

函式中this又可分為建構函式和非建構函式的this兩個概念去理解。

  • 非建構函式中的this指向

    非建構函式中this指向的就是 呼叫這個方法的那個物件

示例1:

<script type="text/javascript">
      function test() {
          alert(this == window);
          this.age = 20;
      }
      test();  //其實是 window.test();  所以這個時候test中的this指向window
  </script>
複製程式碼

示例2:

 <script type="text/javascript">
      var p = {
          age : 20,
          sex : "男",
          sayAge: function (argument) {           
              alert(this.age);
          }
      }
      p.sayAge(); //呼叫物件p的方法sayAge()  所以這個時候this指的是 p 這個物件
  </script>
複製程式碼

示例3:

  <script type="text/javascript">
      var p = {
          age : 20,
          sex : "男",
          sayAge: function (argument) {
              alert(this.age);
              alert(this === p);  //true
          }
      }
      var again = p.sayAge;   //宣告一個變數(方法),把p的方法複製給新的變數
//呼叫新的方法: 其實是window.again().  
//所以 方法中的this指代的是window物件,這個時候age屬性是undefined
// this和p也是不相等的。 
      again();    
  </script>
複製程式碼

綜上:this的指代和程式碼出現的位置無關,只和呼叫這個方法的物件有關。

  • 構造方法中的this指向

構造方法中的this指代的要未來要建立的那個物件。

示例1:

<script type="text/javascript"> 
      function Person () {
          this.age = 20;
          return this;  //作為建構函式的時候,這個行程式碼預設會新增
      }
      var p1 = new Person();  //這個時候 Person中的this就是指的p1
      var p2 = new Person();  //這是時候 Person中的this就是知道p2
  </script>
複製程式碼

多瞭解一點:其實用new呼叫建構函式的時候,建構函式內部其實有個預設的return this; 這就是為什麼this指代那個要建立的物件了。

3.1.4 第三個方向: 改變this的指向(顯式繫結)

在JavaScript中,允許更改this的指向。 通過call方法或apply方法

函式A可以成為指定任意物件的方法進行呼叫 。函式A就是函式物件,每個函式物件中都有一個方法call,通過call可以讓你指定的物件去呼叫這個函式A。

ECMAScript 規範給所有函式都定義了callapply 兩個方法。 callapply是放在Function的原型物件上的,而不是Object原型物件上!

<script type="text/javascript"> 
    var age = 20;
    function showPropertyValue (propertyName) {
        alert(this[propertyName]);
    }
    //使用call的時候,第一個參數列示showPropertyValue中的this的執行,後面的引數為向這個函式傳的值。
    //注意一點:如果第一個引數是null,則this仍然是預設的指向。
    showPropertyValue.call(null, "age");
    showPropertyValue.call(this, "age");
    showPropertyValue.call({age:50}, "age")
</script>
複製程式碼

3.1.5 call / apply / bind 的詳解

this的指向中有第三個方向就是通過call/apply去改變this的指向,這個JavaScript中一個獨特的使用形式,其他語言並沒有。那麼,我們就在這裡順帶講一下callapply 以及bind的用法。

本小節將從三個方面講解: 1:applycall的區別 2:applycall的用法 3:callbind的區別

3.1.5.1 apply 和 call 的區別

ECMAScript 規範給所有函式都定義了 callapply 兩個方法,它們的應用非常廣泛,它們的作用也是一模一樣,只是傳參的形式有區別而已。

簡單來說,假設有一個函式A,我們呼叫函式A會直接去A(),那麼如果是A()這樣直接呼叫的話,函式體A裡面的this就是window了。而我們可以通過call(或apply)去呼叫,比如:A.call().這樣子呼叫就可以指定A中的this到底是哪個物件。

call來做比對,裡面有兩個引數,引數一就是重新指定其中的this是誰,引數2是屬性名。而事實上,callapply也就是引數二的不同這個差異。

apply apply 方法傳入兩個引數:一個是作為函式上下文的物件,簡單來說,重新指定函式中的this是誰。另外一個是作為函式引數所組成的陣列,是傳入一個陣列。

var obj = {
    name : 'ya LV'
}

function func(firstName, lastName){
    console.log(firstName + ' ' + this.name + ' ' + lastName);
}

func.apply(obj, ['A', 'B']);    // A ya LV B
複製程式碼

可以看到,obj是作為函式上下文的物件,也就是說函式functhis指向了 obj這個物件。本來如果直接呼叫func(),那麼函式體中的this就是指的是window。但是現在有了引數一,就是重新指定this,這個this就是引數一的obj這個物件。引數 A 和 B 是放在陣列中傳入 func函式,分別對應 func 引數的列表元素。

call call方法第一個引數也是作為函式上下文的物件。與apply沒有任何區別。但是後面傳入的是一個引數列表,而不是單個陣列。

var obj = {
    name: 'ya LV'
}

function func(firstName, lastName) {
    console.log(firstName + ' ' + this.name + ' ' + lastName);
}

func.call(obj, 'C', 'D');       // C ya LV D
複製程式碼

對比apply我們可以看到區別,C 和 D 是作為單獨的引數傳給 func 函式,而不是放到陣列中。

對於什麼時候該用什麼方法,其實不用糾結。如果你的引數本來就存在一個陣列中,那自然就用apply,如果引數比較散亂相互之間沒什麼關聯,就用 call

補充一個使用 apply 的例子。比如求一個陣列的最大值?

明確JavaScript中沒有返回一個陣列中最大值的函式。但是,有一個函式Math.max可以返回任意多個數值型別的

引數中的最大值,Math.max函式入參並不支援陣列,只能是將多個引數逐個傳入,用逗號分隔。

這個時候如果要可以用Math.max函式,且傳引數可以是一個陣列。我們自然而然會想到所有函式都定義了callapply的方法,我們可以配合applycall來實現,又因為call傳引數並不是一個陣列。所有我們就選擇出Math.max函式加上apply就可以實現我們的目的。

原本只能這樣用,並不能直接用陣列,見示例:

let max = Math.max(1, 4, 8, 9, 0)
複製程式碼

有了 apply,就可以這麼呼叫:

let arr = [1, 4, 8, 9, 0];
let max = Math.max.apply(null, arr);
複製程式碼

在呼叫apply的時候第一個引數給了一個null,這個是因為沒有物件去呼叫這個方法,我們只需要用這個方法幫我們運算,得到返回的結果就行,所以就直接傳遞了一個null過去。

3.5.1.2 apply 和 call 的用法

applycall的用法可以分為三個:改變this的指向,借用別的物件的方法,呼叫函式。

  • 1.改變 this 指向
var obj = {
      name: 'ya LV'
  }

  function func() {
      console.log(this.name);
  }

  func.call(obj);       // ya LV
複製程式碼

這個在前一小節有講到,所以我們就簡單的再來看看。所謂“熟能生巧”,一樣東西,一個知識點,每看一次會有不同的體會,可能這次看的過程讓你有更深刻的思考,這就是進步。call方法的第一個引數是作為函式上下文的物件,這裡把 obj作為引數傳給了func,此時函式裡的this便指向了obj物件。此處func 函式裡其實相當於:

 function func() {
      console.log(obj.name);
  }
複製程式碼

另外注意下call的一些特別用法,很奇葩的this指向。稍微注意下,有點印象就好。

  function func() {
      console.log(this);
    }
    func.call();   //window
    func.call(undefined);     //window
    func.call(null);    //window
    func.call(1);  //Number {1}   這種情況會自動轉換為包裝類Number 就相當於下面一行程式碼
    func.call(new Number(1));   //Number {1}
複製程式碼
  • 2.借用別的物件的方法
 var Person1  = function () {
      this.name = 'ya LV';
  }

  var Person2 = function () {
      this.getname = function () {
          console.log(this.name);
      }
      Person1.call(this);
  }
  var person = new Person2();
  person.getname();       // ya LV
複製程式碼

從上面我們看到,Person2 例項化出來的物件 person 通過 getname 方法拿到了 Person1中的 name。因為在 Person2中,Person1.call(this) 的作用就是使用Person1 物件代替 this 物件,那麼 Person2 就有了Person1 中的所有屬性和方法了,相當於 Person2繼承了Person1的屬性和方法。 不理解的話我們再來慢慢看,我們說A.call ( 引數一)這樣的形式就是重新指定函式A中的this‘引數一’這個物件,那麼我們來看看Person2函式體中的Person1.call(this)這條語句,其中這條語句的this是指Person2這個物件。現在就是把Person1函式的this重新指向為Person2,是不是有了Person2.name='ya LV'

  • 3.呼叫函式

applycall 方法都會使函式立即執行,因此它們也可以用來呼叫函式。這個我們在這節的一開始就有說,比如A()A.call()都是呼叫函式A。

  function func() {
      console.log('ya LV');
  }
  func.call();            // ya LV
複製程式碼

3.5.1.3 call 和 bind 的區別

EcmaScript5 中擴充套件了叫 bind 的方法,在低版本的 IE 中不相容。它和call很相似,接受的引數有兩部 分,第一個引數是是作為函式上下文的物件,第二部分引數是個列表,可以接受多個引數。

它們之間的區別有以下兩點。

  1. 區別1.bind 的返回值是函式
    var name='HELLO'
    var obj = {
      name: 'ya LV'
    }

    function func() {
      console.log(this.name);
    }
    
    //將func的程式碼拷貝一份,並且永遠改變其拷貝出來的函式中的this,為bind第一個引數所指向的物件。把這     份永遠改變著this指向的函式返回給func1.
    var func1 = func.bind(obj);
   //bind方法不會立即執行,是返回一個改變上下文this的函式,要對這個函式呼叫才會執行。
    func1();  //ya LV
   //可以看到,現在這份改變this之後拷貝過來的函式,this的指向永遠是bind()繫結的那個,不管之後去call    重新指向物件,func1 都不會改變this的指向。永遠!可知,bind比call優先順序還高。
    func1.call({name:'CALL'});   //ya LV

    //又從func重新拷貝一份永遠改變this指向物件為{name:'LI SI'}這個物件的函式,返回給func2.
    var func2 = func.bind({name:'LI SI'});
    func2();   //LI SI

   //注意,這裡是拷貝一份func2(而不是func)的程式碼,而func2之前已經繫結過去永遠改變this的指向了,所以這   裡並不去改變!還是會輸出原來的最先bind的this指向物件。
    var func3 = func2.bind({name:'ZHANG SAN'});
    func3();   //LI SI

   //上面對func最初的函式進行了多次繫結,繫結後原函式 func 中的 this 並沒有被改變,依舊指向全域性物件      window。因為繫結bind的過程是拷貝程式碼的一個過程,而不是在其自身上修改。window.name = HELLO
    func();   //HELLO
複製程式碼

bind 方法不會立即執行,而是返回一個改變了上下文this後的函式。而原函式func中的 this並沒有被改變,依舊指向全域性物件 window

  1. 區別2.引數的使用
 function func(a, b, c) {
      console.log(a, b, c);
  }
  var func1 = func.bind(null,'yaLV');

  func('A', 'B', 'C');            // A B C
  func1('A', 'B', 'C');           // yaLV A B
  func1('B', 'C');                // yaLV B C
  func.call(null, 'yaLV');       // yaLV undefined undefined
複製程式碼

call 是把第二個及以後的引數作為 func方法的實參傳進去,而 func1方法的實參實則是在bind中引數的基礎上再往後排。也就是說,var func1 = func.bind(null,'yaLV'); bind現有兩個引數,第一個是指向,第二個實參是'yaLV',那麼就是先讓func中的a='yaLV',然後沒排滿就是讓func1('A', 'B', 'C'); 這個引數依次排,現在b='A'c='B' , 形參已經排完了。也就是輸出yaLV A B

在低版本瀏覽器沒有bind方法,我們也可以自己實現一個。

if (!Function.prototype.bind) {
   Function.prototype.bind = function () {
       var self = this,                        // 儲存原函式
       context = [].shift.call(arguments), // 儲存需要繫結的this上下文
       args = [].slice.call(arguments);    // 剩餘的引數轉為陣列
       return function () {                    // 返回一個新函式
       self.apply(context[].concat.call(args[].slice.call(arguments));
              }
          }
      }
複製程式碼

3.1.6 習題與案例

習題1

<script type="text/javascript">
     var name='window_dqs';
     var obj={
        name:'obj_dqs',
        showName:function(){
            console.log(this.name);
        }};

     function fn(){
        console.log(this);
     }
     function fn2(){
        this.name='fn_dqs';
     }
	
	//因為obj去呼叫,this就是obj
     obj.showName();    //obj_dqs  
     //因為借調,而此時借調的物件是this,而this在全域性作用域上就是指window,所以找window.name
     obj.showName.apply(this);   //window_dqs
     //因為借調的物件是一個函式物件,那麼this就是指函式物件,this.name就是函式名
     obj.showName.apply(fn2);    //fn2
</script>
複製程式碼

習題2

<script type="text/javascript">
 var name='window_dqs';
     function fn(){
        this.name='fn_dqs';
        this.showName=function(){
            console.log(this.name);
        }
        console.log(this);
     }

     function fn2(){
        this.name='fn_pps';
        this.showName=function(){
            console.log(this.name);
        }
        console.log(this);
     }

     var p=new fn();
     fn2.apply(p);
     p.showName();

     var obj={};
     fn2.apply(obj);
     obj.showName();
</script>
複製程式碼

結果是:

    var p=new fn();輸出fn { name: 'fn_dqs', showName: [Function] }
    fn2.apply(p);輸出fn { name: 'fn_pps', showName: [Function] }
    p.showName();輸出fn_pps

	var obj={};
    fn2.apply(obj);輸出Object{name: "fn_pps"showName: ƒ ()__proto__: Object··}
    obj.showName();輸出fn_pps
複製程式碼

習題3

<script type="text/javascript">
var name='window_dqs';
     var obj={
        name:'json_dqs',
        showName:function(){
            console.log(this.name);
            return function(){
                console.log(this.name);
            }
        }
     }
    var p=obj.showName();
    obj.showName()();
    p.call(obj);
</script>
複製程式碼

結果是:

json_dqs
json_dqs
window_dqs
json_dqs
複製程式碼

面試題1

程式碼片段1:

var name = "The Window";
  var object = {
    name: "My Object",
    getNameFunc: function (){
      return function (){
        return this.name;
      };
    }
  };
  console.log(object.getNameFunc());     //ƒ (){return this.name;}
  console.log(object.getNameFunc()());  //The Window
複製程式碼

程式碼片段一沒有閉包。有巢狀,但沒有用外部函式的變數或函式。是使用this的。this與呼叫方式有關。

理解:看object.getNameFunc()是物件.方法() 返回的是一個函式,這個函式還未執行。js中this是動態的,所以函式沒有執行,並不確定函式裡的this是指的是誰?那麼現在再對返回的函式加個(),也就是object.getNameFunc()(),呼叫執行,把最後一個括號和最後一個括號前當做兩個部分,前面是函式名,後面一個括號是呼叫。相當於test(),這個時候this就是window。故這樣呼叫的函式this就是指的window,故window.name=The Window.

程式碼片段二: 對於片段一我們的本意是不是想輸出My Object。那麼怎麼改造,通過that=this去操作。

var name2 = "The Window";
  var object2 = {
    name2: "My Object",
    getNameFunc: function () {
      var that = this;  //快取this
      return function () {
        return that.name2;
      };
    }
  };
  console.log(object2.getNameFunc());    //ƒ (){return that.name2;}
  console.log(object2.getNameFunc()()); //My Object
複製程式碼

程式碼片段二是有閉包的,有巢狀函式。內部函式有使用外部函式的變數that。外部和內部函式有執行。

理解:首先還是看object2.getNameFunc()返回一個函式,注意這個函式中沒有this,在呼叫object2.getNameFunc時,我們有執行一句var that = this;也就是把thisthat,這個時候this是指的是object2。再次呼叫object2.getNameFunc()()時就是執行object2.getNameFunc()返回來的函式”。that.name2=object2.name2;實質上是閉包,使用了外部函式的that變數。

程式碼片段三(對片段二的改造):

var name3 = "The Window";
  var object3 = {
    name3: "My Object",
    getNameFunc: function () {
      return function () {
        return this.name3;
      }.bind(this);
    }
  };
  console.log(object3.getNameFunc());     //ƒ (){return this.name3;}
  console.log(object3.getNameFunc()());   //My Object
複製程式碼

理解:與“程式碼片段二”一樣,只是片段二是通過that=this去改變this的值,而片段三是通過bind繫結this的值。看bind(this)這裡的this就是指這條語句object3.getNameFunc()呼叫的物件object3.所以通過這個手段去把this指向了前面的物件object3.再去呼叫返回的函式時,那麼this.name3=object3.name3

面試題2

<script>
var myObject = {
    foo: "bar",
    func: function() {
        var self = this;
        console.log(this.foo);  //bar
        console.log(self.foo);  //bar
        (function() {
          console.log(this.foo);  //undefined  此時的this是window
          console.log(self.foo);  //bar   閉包可以看到外部的區域性變數
        }());  //匿名函式自執行,是window上呼叫這個函式。
    }
};
myObject.func();

//那麼如何修改呢?使得在自執行函式中的this.foo就是我們想要的bar呢?
//提供兩種方法:
//case1:用call去指向this是誰
var myObject = {
    foo: "bar",
    func: function() {
        var self = this;
        console.log(this.foo); // bar
        console.log(self.foo); // bar
        (function() {
            console.log(this.foo);  // bar
            console.log(self.foo); // bar
        }.call(this));  
        //myObject.func();這樣呼叫func(),那麼func()中的this就是前面的物件myObject。
    }
};
myObject.func();

//case2:用bind去繫結this,但要注意bind是返回一個函式,故要bind(this)(),後一個括號表示函式呼叫。把bind(this)將拷貝一份並改變this的指向的函式執行。
var myObject = {
    foo: "bar",
    func: function() {
        var self = this;
        console.log(this.foo);  // bar
        console.log(self.foo);  // bar
        (function() {
            console.log(this.foo);  // bar
            console.log(self.foo);  // bar
        }.bind(this)());
    }
};
myObject.func();
</script>
複製程式碼

面試3 考察this的指向: 難點:陣列(類陣列)中的元素當做函式呼叫時的this指向 也就是,如果是呼叫陣列(類陣列)中的元素, 元素函式中的this是這個陣列(類陣列)。

<script>
var length = 10;
function fn(){
    console.log(this.length);
}
var obj = {
    length: 5,
    method: function (fn){  // [fn, 1, 2]  
        fn();  // 10
        arguments[0]();  // 3
    }
};
obj.method(fn, 1, 2);
/*obj.method(fn, 1, 2);傳實參fn過去,此時fn拿到函式的地址值拷貝給形參fn,在執行fn()這裡呼叫是相當window呼叫fn,this指的是window。而不是obj不要感覺是在obj裡面就是,迷惑大家的。this的指向永遠跟呼叫方式有關。
*另外,arguments[0]();呼叫時,這個時候是類陣列中的元素呼叫,那麼這時的this是類陣列本身,所以,陣列.length是不是輸出類陣列的長度。
*如果是呼叫陣列(類陣列)中的元素, 元素函式中的this是這個陣列(類陣列).為什麼呢?看以下兩個例子:
* */

//例子1:
var obj = {
    age : 100,
    foo : function (){
        console.log(this);
    }
}
var ff = obj.foo;
ff();  //window
obj.foo(); //{age: 100, foo: ƒ}
obj["foo"]();  //{age: 100, foo: ƒ}
//上面的這個例子沒有問題吧。很自然的。

//例子2:
var arr = [
    function (){
        console.log(this);
    },function (){

    }
];
var f = arr[0];
f();  //window

/*arr.0()--類似於這麼寫把,只是陣列不允許這樣的語法--*/
 arr[0]();  //輸出陣列本身:(2) [ƒ, ƒ] 。故驗證一句話:如果呼叫陣列(類陣列)中的元素時,那麼這時的this是陣列(類陣列)本身。
</script>
複製程式碼

3.2 原型與原型鏈

一個特別經典的總結:

a.b就可以看出作用域與作用域鏈,原型與原型鏈的知識。 (詳見Part1的1.1.3)

3.2.1 五張圖理解原型與原型鏈

建構函式建立物件我們先使用建構函式建立一個物件:

function Person() {
    
}
var person = new Person();
person.name = 'name';
console.log(person.name) // name
複製程式碼

在這個例子中,Person就是一個建構函式,我們使用new建立了一個例項物件person

很簡單吧,接下來進入正題:【prototype】

任何的函式都有一個屬性prototype,這個屬性的值是一個物件,這個物件就稱為這個函式的原型物件。但是一般情況,我們只關注建構函式的原型。比如:

function Person() {

}
// 雖然寫在註釋裡,但是你要注意:prototype是函式才會有的屬性
Person.prototype.name = 'name';

var person1 = new Person();  //person1是Person建構函式的例項
var person2 = new Person();  //person2是Person建構函式的例項
console.log(person1.name) // name
console.log(person2.name) // name
複製程式碼

其實,函式的prototype屬性指向了一個物件,這個物件正是呼叫該建構函式而建立的例項的原型,也就是這個例子中的person1person2的原型。例項其實是通過一個不可見的屬性[[proto]]指向的。

你可以這樣理解:每一個JavaScript物件(null除外)在建立的時候就會與之關聯另一個物件,這個物件就是我們所說的原型,每一個物件都會從原型”繼承”屬性。

讓我們用一張圖表示建構函式和例項原型之間的關係:

prototype
那麼我們該怎麼表示例項與例項原型,也就是person1 person2Person.prototype之間的關係呢,這時候我們就要講到第二個屬性:[[proto]]

當使用建構函式建立物件的時候, 新建立的物件會有一個不可見的屬性[[proto]], 他會指向建構函式的那個原型物件。事實上,每一個JavaScript物件(除了null)都具有的一個不可見屬性,叫[[proto]],這個屬性會指向該物件的原型。

為了證明這一點,我們可以在火狐或者谷歌中輸入:

function Person() {
    
}
var person1 = new Person();
console.log(person1.__proto__ === Person.prototype); //true
複製程式碼

於是我們更新下關係圖:

proto
既然例項物件和建構函式都可以指向原型,那麼原型是否有屬性指向建構函式或者例項呢?指向例項倒是沒有,因為一個建構函式可以生成多個例項,但是原型指向建構函式倒是有的,這就要講到第三個屬性:【constructor】,每個原型都有一個constructor屬性指向關聯的建構函式。

為了驗證這一點,我們可以嘗試:

function Person() {

}
console.log(Person === Person.prototype.constructor); //true
複製程式碼

所以再更新下關係圖:

原型與原型鏈詳解
綜上我們已經得出:

function Person() {

}
var person1 = new Person();
//物件的__proto__屬性: 建立物件時自動新增的, 預設值為建構函式的prototype屬性值(很重要)
console.log(person1.__proto__ == Person.prototype) // true
console.log(Person.prototype.constructor == Person) // true

// 順便學習一個ES5的方法,可以獲得物件的原型
console.log(Object.getPrototypeOf(person1) === Person.prototype) //true
複製程式碼

瞭解了建構函式、例項原型、和例項之間的關係,接下來我們講講例項和原型的關係:【例項與原型】。當讀取例項的屬性時,如果找不到,就會查詢與物件關聯的原型中的屬性,如果還查不到,就去找原型的原型,一直找到最頂層為止。

舉個例子:

function Person() {

}
//往Person物件原型中新增一個屬性
Person.prototype.name = 'name';
//建立一個person1例項物件
var person1 = new Person();
//給建立的例項物件person1新增一個屬性
person1.name = 'name of this person1';
//查詢person1.name,因為本身例項物件有,那麼就找到了自身例項物件上的屬性和屬性值
console.log(person1.name) // name of this person1
//刪除例項物件的屬性和屬性值
delete person1.name;
//查詢屬性name,在例項物件自身上找不到,通過proto指向往原型鏈上找,在原型物件中找到
console.log(person1.name) // name
複製程式碼

在這個例子中,我們設定了person1name屬性,所以我們可以讀取到為name of this person1,當我們刪除了person1name屬性時,讀取person1.name,從person1中找不到就會從person的原型也就是person.__proto__ == Person.prototype中查詢,幸運的是我們找到了為name,但是萬一還沒有找到呢?原型的原型又是什麼呢?

var obj = new Object();
obj.name = 'name'
console.log(obj.name) // name
複製程式碼

所以原型物件是通過Object建構函式生成的,結合之前所講的一句很重要的話,幾乎就是涵蓋原型與原型鏈知識的始終的一句話,那就是:例項物件的proto指向建構函式的prototype。也就是說,Person.prototype這個原型物件(例項原型)是通過Object這個建構函式new出來的,也就是Person.prototype這個原型物件是Object的例項,所以這個例項會有proto屬性指向Object建構函式的原型物件Object.prototype

這裡呢插入一句總結出來的話,逆推順推都是可行的,那就是:例項通過proto這個屬性指向其建構函式的原型物件。所以我們再更新下關係圖:

原型與原型鏈詳解
Object.prototype的原型呢?null,嗯,就是null。所以查到Object.prototype就可以停止查詢了。所以最後一張關係圖就是:
原型與原型鏈詳解

那【原型鏈】是啥 ? 那就是由proto這個屬性進行查詢的一個方向這就是一條原型鏈。圖中由相互關聯的原型組成的鏈狀結構就是原型鏈,也就是藍色的這條線,都是通過proto屬性進行查詢的。

​ 那麼訪問一個物件的屬性時,怎麼通過原型鏈去查詢屬性或方法呢 ? 先在自身屬性中查詢,找到返回。如果沒有, 再沿著proto這條鏈向上查詢, 找到返回。如果最終沒找到, 返回undefined

3.2.2 從程式碼中看原型與原型鏈

原型與原型鏈詳解
解析上圖:首先n ,s 都是全域性變數,然後通過物件字面量的方法去建立了一個物件。然後有一個建構函式(之所以是建構函式,是因為後面程式碼有new的操作),這個建構函式就會有函式宣告提前,當建構函式宣告時,就會去在記憶體中建立一個person的函式物件,這個函式物件裡只有prototype屬性,去指向person的函式原型物件。要注意,現在還沒有去執行裡面的程式碼,只是函式宣告時建立了一個person的函式物件。後面就是new的例項物件,新new出來的例項物件P2 P3就會在記憶體中分配一塊記憶體去把地址值給它,現在才會去執行建構函式中的程式碼。所以只有P2 P3才會 有name age speak屬性和方法。這些新new出來的例項物件就會有一個不可見的屬性proto,去指向這個原型物件。而最終這個person的函式原型物件會有指向一個object的原型物件,再上去其實就是null。這就一層一層往上走就是原型鏈,因為原型鏈,我們才會有繼承的特性。

​ 注幾點:

  1. 從上圖的圖示中可以看到,建立 P2 P3 例項物件雖然使用的是 Person 建構函式,但是物件建立出來之後,這個P2 P3 例項物件其實已經與 Person 建構函式(函式物件)沒有任何關係了,P2 P3 例項物件的[[ proto ]] 屬性指向的是 Person 建構函式的原型物件。
  2. 如果使用 new Person() 建立多個物件,則多個物件都會同時指向 Person 建構函式的原型物件。
  3. 我們可以手動給這個原型物件新增屬性和方法,那麼P2 P3 ····這些例項物件就會共享這些在原型中新增的屬性和方法。也就是說,原型物件相當於公共的區域,所有的同一類的例項都可以去訪問到原型物件。
  4. 如果我們訪問P2例項物件 中的一個屬性 gender ,如果在P2 物件中找到,則直接返回。如果 P2 物件中沒有找到,則直接去P2物件的 [[proto]] 屬性指向的原型物件中查詢,如果查詢到則返回。(如果原型中也沒有找到,則繼續向上找原型的原型---原型鏈)。
  5. 讀取物件的屬性值時: 會自動到原型鏈中查詢
  6. 設定物件的屬性值時: 不會查詢原型鏈, 如果當前物件中沒有此屬性, 直接新增此屬性並設定其值。比如通過P2物件只能讀取原型中的屬性 name的值,並不能修改原型中的屬性name 的值。 P2.gender= "male" ; 並不是修改了原型中的值,而是在 P2物件中給新增了一個屬性 gender
  7. 方法一般定義在原型中, 屬性一般通過建構函式定義在物件本身上。

另外看看,原型與原型鏈的三點關注:

  1. 函式的顯示原型指向的物件預設是空Object例項物件(但Object不滿足)
console.log(Fn.prototype instanceof Object) // true
console.log(Object.prototype instanceof Object) // false
console.log(Function.prototype instanceof Object) // true
複製程式碼
  1. 所有函式都是Function的例項(包含Function)
console.log(Function.__proto__===Function.prototype) //true
複製程式碼
  1. Object的原型物件是原型鏈盡頭
console.log(Object.prototype.__proto__) // null
複製程式碼

3.2.3 探索instanceof

instanceof是如何判斷的?

  • 表示式: A instanceof B

  • 如果B建構函式的原型物件(B.prototype)在A例項物件的原型鏈(A.proto.proto·····沿著原型鏈)上, 返回true, 否則返回false。(見下圖)

    instanceof

  • 也就是說A例項物件的原型鏈上可能會有很多物件,只要B建構函式的原型物件有一個是在其原型鏈上的物件即可返回true

  • 反過來說也一樣,例項物件A是否可以通過proto屬性(沿著原型鏈,A.proto.proto·····)找到B.prototype(B的原型物件),找到返回true,沒找到返回false.

  • 注1:對例項物件的說明,事實上,例項物件有兩種。一種是我們經常說的new出來的例項物件(比如建構函式Person , new出來p1 p2...,這些都是例項物件),另外一種就是函式,函式本身也是例項,是Function new出來的。但我們一般說的例項物件就是指new出來的類似於p1 p2這些的例項物件。

Function是通過new自己產生的例項(Function.proto===Function.prototype)

案例1:

  //一個建構函式Foo
  function Foo() {  }
  //一個f1例項物件
  var f1 = new Foo()
  //翻譯:f1是Foo的例項物件嗎?
  //還記得我說過,一個例項物件通過proto指向其建構函式的原型物件上。
  //深入翻譯:f1這個例項物件通過proto指向是否可以找到Foo.prototype上呢?
  console.log(f1 instanceof Foo) // true
  //這行程式碼可以得出,沿著proto只找了一層就找到了。
  console.log(f1.__proto__ === Foo.prototype);   // true
  
  //翻譯:f1是Object的例項物件嗎?
  //深入翻譯:f1這個例項物件通過proto指向是否可以找到Object.prototype上呢?
  console.log(f1 instanceof Object) // true
  //這兩行程式碼可以得出,沿著proto找了兩層才找到。事實上,f1.__proto__找到了Foo.prototype(Foo建構函式原型上),再次去.__proto__,找到了Object的原型物件上。見下圖。
  console.log(f1.__proto__ === Object.prototype);  // false
  console.log(f1.__proto__.__proto__ === Object.prototype);  // true
複製程式碼

js經典原型與原型鏈的圖
案例2:

//這個案例的實質還是那句話:一個例項物件通過proto屬性指向其建構函式的原型物件上。
//翻譯:例項物件Object是否可以通過proto屬性(沿著原型鏈)找到Function.prototype(Function的原型物件)
  console.log(Object instanceof Function) // true
//以上結果的輸出可以看到下圖,Object.__proto__直接找到一層就是Function.prototype.(Object created by Function)可知Object建構函式是由Function建立出來的,也就是說,Object這個例項是new Function出來的。

  console.log(Object instanceof Object) // true
//很有意思。上面我們已經知道Object這個例項是new Function出來的。也就是Object.proto指向Function.prototype。有意思的是,Function的原型物件又是Object原型物件的一個例項,也就是Function.prototype.proto 指向  Object.prototype .很有意思吧,見下圖很更清楚這個“走向”。


  console.log(Function instanceof Function) // true
//由這個可知,可以驗證我們的結論:Function是通過new自己產生的例項。                                   	 Function.proto===Function.prototype

  console.log(Function instanceof Object) // true
//Function.proto.proto===Function.prototype (找了兩層)

  //定義了一個Foo建構函式。由下圖可知,Foo.proto.proto.proto===null  
  function Foo() {}
  console.log(Object instanceof  Foo) // false
//這條語句要驗證的是,Object是否可以通過其原型鏈找到Foo.prototype。
// Object.proto.proto.proto=null  並不會找到Foo.prototype。所以,返回FALSE。
複製程式碼

js經典原型與原型鏈的圖
看上圖,再引申出一個問題:函式是物件。那你覺得函式包含的大?還是物件大呢? 如上圖,物件是由函式創造的。(Object created by Function) 也就是說,物件是new Function得到的。 繼續翻譯,物件是例項 Function是建構函式。 繼續翻譯,物件這個例項有不可見屬性proto指向Function建構函式的原型物件(Function.prototype)。 故,函式與物件的關係是:函式更大,它包含物件。 這個我個人覺得很重要,務必理解透。

3.2.4 一些概念的梳理

  • 所有函式都有一個特別的屬性:
    • prototype : 顯式原型屬性
  • 所有例項物件都有一個特別的屬性:
    • __proto__ : 隱式原型屬性
  • 顯式原型與隱式原型的關係
    • 函式的prototype: 定義函式時被自動賦值, 值預設為{}, 即用為原型物件
    • 例項物件的__proto__: 在建立例項物件時被自動新增, 並賦值為建構函式的prototype值
    • 原型物件即為當前例項物件的父物件
  • 原型鏈
    • 所有的例項物件都有__proto__屬性, 它指向的就是原型物件
    • 這樣通過__proto__屬性就形成了一個鏈的結構---->原型鏈
    • 當查詢物件內部的屬性/方法時, js引擎自動沿著這個原型鏈查詢
    • 當給物件屬性賦值時不會使用原型鏈, 而只是在當前物件中進行操作

3.2.5 習題與案例

面試1:

/*阿里面試題*/function Person(){
    ②getAge = function (){
        console.log(10)
    }
    ③return this;
}

④Person.getAge = function (){
    console.log(20);
}

⑤Person.prototype.getAge = function (){
    console.log(30);
}

⑥var getAge = function (){
    console.log(40)
}

⑦function getAge(){
    console.log(50)
}


Q1:Person.getAge() // 20
Q2:getAge() // 40
Q3:Person().getAge() // 10
Q4:getAge() // 10
Q5:new Person().getAge() // 30
Q6:new Person.getAge(); // 20
複製程式碼

整體程式碼塊①定義了建構函式Person ②是在建構函式中有一個未宣告的變數,這個變數是引用變數,內容為地址值。指向一個函式物件。又因為,未使用嚴格模式下,在函式中不使用var宣告的變狼都會成為全域性變數。(注意這裡不是屬性,是全域性變數)同時也要注意,這裡②和③的語句在解析到這裡後並沒有執行。執行的話就要看有沒有new(作為建構函式使用),或者有沒有加()呼叫(作為普通函式使用)。 ③返回一個this。這個this是誰現在還不知道。需要明白js中的this是動態的,所以根據上一節this的總結才定位到this到底是誰。 ④Person.getAge是典型的“物件.屬性(方法)”的形式,所以它是給Person函式物件上新增一個getAge的方法。等同於:

  function Person.getAge(){
    console.log(20);
}
複製程式碼

函式名其實就是變數名。 ⑤在建構函式的原型中新增了getAge的方法 ⑥這裡也是給一個全域性變數賦值一個地址值,使其指向一個函式物件。注意,這裡var的變數會宣告提前。與程式碼塊②區別,這裡當解析完後,getAge已經指向一個函式物件啦。可以看做:

function getAge(){
    console.log(40)
}
複製程式碼

⑦定義一個函式,函式也會宣告提前。在棧記憶體有getAge,內容值為一個地址值,指向一個函式物件。

Q1:物件.屬性方法()。程式碼塊④產生的結果。 Q2:呼叫函式,全域性作用域裡的。那只有程式碼塊⑥產生結果。 Q3:Person().getAge()。先看前面一部分Person(),把Person當做一個普通函式呼叫,執行Person函式體對全域性變數getAge進行定義並重新指向,也就是Person()執行了程式碼塊②而覆蓋了程式碼塊⑥的操作。又返回this,根據Person()這種呼叫方式,可知this就是window。所以就是“window.gerAge()”,因被覆蓋了,所以這行程式碼執行結果是程式碼塊②產生。 Q4:getAge()相當於window.getAge(); 還是上一個語句的結果,程式碼塊②產生結果。 Q5:new Person()先看這部分,就是new出來一個例項,你可以想成p1,那麼p1.getAge(); p1是一個Person的例項,p1中有不可見的[[proto]]屬性,指向Person的原型物件。那麼p1.getAge (),現在p1本身找,找不到就沿著原型鏈(proto指向鏈)去找,好找到了原型對向中有,因為程式碼塊⑤產生作用。 Q6:new Person.getAge(); 可以把Person.getAge看成一個物件,去new它,是不是類似於我們平常var p1=new Person();這樣的操作,所以我們把Person.getAge看做一個建構函式去new它。由上面對程式碼塊④的理解,可以看做那樣的函式,所以結果就是程式碼塊④產生的結果。

面試題2:

function A () {

  }
  A.prototype.n = 1;
  var b = new A();
  A.prototype = {
    n: 2,
    m: 3
  };
  var c = new A();
  console.log(b.n, b.m, c.n, c.m);  //1 undefined 2 3
//見下圖:
複製程式碼

js經典原型與原型鏈的圖
面試題3:連續賦值問題

//與上題的區別在於如何理解a.x的執行順序
<script>
var a = {n: 1};
var b = a;
a.x = a = {n: 2};  //先定義a.x再去從右往左賦值操作。
console.log(a.x);  // undefined  物件.屬性 找不到 是返回undefined  變數找不到則報錯!
console.log(b);  // {n :1, x : {n : 2}}
</script>
//見下圖分析
複製程式碼

在記憶體結構

面試題4:

//建構函式F
  function F (){};
  Object.prototype.a = function(){
    console.log('a()')
  };
  Function.prototype.b = function(){
    console.log('b()')
  };
  //new一個例項物件f
  var f = new F();

  f.a();  //a()
  f.b();  //報錯,找不到
  F.a();  //a()
  F.b();  //b()
複製程式碼

2.3 執行上下文與執行上下文棧

2.3.1 變數提升與函式提升

變數宣告提升

  • 通過var定義(宣告)的變數, 在定義語句之前就可以訪問到
  • 值: undefined
  • 注:未使用var關鍵字宣告變數時,該變數不會宣告提升。
console.log(c); //報錯,c is not defined.
    console.log(b); //undefined
    var b=0;
    c=4;
    console.log(c); //4 意外的全域性變數->在ES5的嚴格模式下就會報錯。
    console.log(b); //0  
複製程式碼

函式宣告提升

  • 通過function宣告的函式, 在之前就可以直接呼叫
  • 值: 函式定義(物件)
  • 注1:函式宣告(Function Declaration)和函式表示式(Function Expression)是有微妙的區別,要明確他們兩是Javascript兩種型別的函式定義,兩個概念上是並列的。也就是說定義函式的方式有兩種:一種是函式宣告,另一種就是函式表示式。
  • 注2:函式表示式並不會宣告提升。

先有變數提升, 再有函式提升

案例一:

var a = 3;
  function fn () {
    console.log(a);  //undefined
    var a = 4
  }
  fn();

//上面這段程式碼相當於
 var a = 3;
  function fn () {
      var a;
      console.log(a);  //undefined
      a = 4
  }
  fn();
複製程式碼

案例二:

  console.log(b) //undefined  變數提升
  fn2() //可呼叫  函式提升
  fn3() //不能呼叫,會報錯。  fn3是一個函式表示式,並不會函式提升,實際上他是變數提升。

  var b = 3
  function fn2() {
    console.log('fn2()')
  }
  var fn3 = function () {
    console.log('fn3()')
  }
複製程式碼

問題: 變數提升和函式提升是如何產生的? An:因為存在全域性執行上下文和函式執行上下文的預處理過程。所以我們就來學習下一節的執行上下文。

2.3.2 執行上下文

程式碼分類(位置)

  • 全域性程式碼
  • 函式(區域性)程式碼

執行上下文分為全域性執行上下文和函式執行上下文

全域性執行上下文

  • 步驟1:在執行全域性程式碼前將window確定為全域性執行上下文物件(虛擬的)
  • 步驟2:對全域性資料進行預處理(收集資料)
    • var定義的全域性變數==>值為undefined, 並新增為window的屬性
    • function宣告的全域性函式==>賦值(fun), 新增為window的方法
    • this==>賦值(window)
  • 步驟3:開始執行全域性程式碼
     //全域性執行上下文
     console.log(a1);  //undefined
     console.log(a2);  //undefined
     a2();   //也會報錯,a2不是一個函式
     console.log(a3);  //ƒ a3() {console.log('a3()')}
     console.log(a4)   //報錯,a4沒有定義
     console.log(this); //window

     var a1 = 3;
    //函式表示式,實際上是變數提升。而不是函式提升。
     var a2 = function () {
       console.log('a2()')
     };
     function a3() {
       console.log('a3()')
     }
     a4 = 4;
複製程式碼

函式執行上下文

  • 步驟1:在呼叫函式, 準備執行函式體之前, 建立對應的函式執行上下文物件(虛擬的, 存在於棧中。棧會分為全域性變數棧和區域性變數棧,區域性變數棧可以理解為是一個封閉的記憶體空間。雖然我們編寫的程式碼無法訪問這個物件,但解析器在處理資料時會在後臺使用它。)
  • 步驟2:對區域性資料進行預處理(收集資料)
    • 形參變數==>賦值(實參)==>新增為執行上下文的屬性
    • arguments==>賦值(實參列表), 新增為執行上下文的屬性
    • var定義的區域性變數==>undefined, 新增為執行上下文的屬性
    • function宣告的函式 ==>賦值(fun), 新增為執行上下文的方法
    • this==>賦值(呼叫函式的物件)
  • 步驟3:開始執行函式體程式碼
 //函式執行上下文
 function fn(a1) {
    console.log(a1);  //2  實參對形參賦值
    console.log(a2);  //undefined  函式內部區域性變數宣告提升
    a3();     //a3()  可呼叫  函式提升
    console.log(arguments);  //類陣列[2,3]
    console.log(this);  //window

    var a2=3;
    function a3() {
      console.log("a3()");
    }
  }
  fn(2,3);  //執行,不執行不會產生函式執行上下文
複製程式碼

全域性執行上下文和函式執行上下文的生命週期
全域性 : 準備執行全域性程式碼前產生, 當頁面重新整理/關閉頁面時死亡
函式 : 呼叫函式時產生, 函式執行完時死亡

2.3.3 執行上下文棧

  • 執行上下文棧流程理解:
  1. 在全域性程式碼執行前, JS引擎就會建立一個棧來儲存管理所有的執行上下文物件
  2. 在全域性執行上下文(window)確定後, 將其新增到棧中(壓棧)
  3. 在函式執行上下文建立後, 將其新增到棧中(壓棧)
  4. 在當前函式執行完後,將棧頂的物件移除(出棧)
  5. 當所有的程式碼執行完後, 棧中只剩下window
<script type="text/javascript">
                            //1. 進入全域性執行上下文
  var a = 10;
  var bar = function (x) {
    var b = 5;
    foo(x + b)              //3. 進入foo執行上下文
  };
  var foo = function (y) {
    var c = 5;
    console.log(a + c + y)
  };
  bar(10);   //2. 進入bar函式執行上下文(注:函式執行上下文物件在函式呼叫時產生,而不是函式宣告時產生)
</script>
複製程式碼

以上這種情況整個過程產生了3個執行上下文 呼叫一次函式產生一個執行上下文 如果在上面程式碼最後一行的bar(10),再呼叫一次bar(10),那麼就會產生5個上下文。 因為第一個bar(10)產生一個函式上下文 在bar函式中呼叫foo,又產生一個函式執行上下文。 那麼現在又呼叫bar(10),與上面一個樣會產生兩個上下文,加起來4個函式執行上下文。 最後加上window的全域性變數上下文,一共五個執行上下文。

遞迴
執行上下文
圖註解1:這個過程有點像遞迴函式的回溯思想。在棧中,先有window的全域性上下文,然後執行bar()會把bar函式執行上下文壓入棧中。bar中呼叫foo,把foo函式執行上下文壓入棧中,foo函式執行完畢,釋放,便會把foo函式執行上下文pop(推出來)。逐漸bar執行完畢,popbar函式執行上下文,最後只剩下window上下文。

註解2:假設一個情況:f1()函式中會呼叫f2()f3()函式。那麼在當前時刻棧中可最多達到幾個上下文? An: 當f1()執行,會先呼叫f2(),呼叫完後,f2()已經完成了使命,它的生命週期就結束了,所以棧 就會釋放掉他,在執行f3(),所以棧中也就最多三個上下文。f3() f1() window.

註解3:假設另一個情況:f1()函式中會呼叫f2(), f2()中會呼叫f3()函式。那麼在當前時刻棧中可最多達到幾個上下文 ? An: 當f1()執行,會先呼叫f2(),執行f2()時要呼叫f3(),所以,棧中可達到4個上下文。f3() f2() f1() window .

2.3.4 習題與案例

面試題1:執行上下文棧

  1. 依次輸出什麼?
  2. 整個過程中產生了幾個執行上下文?
<script type="text/javascript">
  console.log('global begin: '+ i); //undefined 變數提升
  var i = 1;
  foo(1);
  function foo(i) {
    if (i == 4) {
      return;
    }
    console.log('foo() begin:' + i);
    foo(i + 1);
    console.log('foo() end:' + i);
  }
  console.log('global end: ' + i)  //1 全域性變數i,其他的函式中的i當執行結束後就銷燬了。
複製程式碼

執行結果:

&emsp;global begin: undefined
  foo() begin:1
  foo() begin:2
&emsp;foo() begin:3
&emsp;foo() end:3
&emsp;foo() end:2
&emsp;foo() end:1
&emsp;global end: 1
複製程式碼

一共產生5個上下文: 分析見下圖,我畫的很清楚了。這張圖畫了12min。主要就是入棧出棧,在出棧前,回溯原來的那個函式,那個函式執行上下文還在,如果還有 沒有執行完的語句 會在這個時候執行。當剩餘的語句已經執行完了,那麼這個函式的執行上下文生命週期結束,釋放出棧。想想我們遞迴呼叫去求階乘的例子,思想是一樣的。

遞迴思想

面試題2:變數提升和函式提升(執行上下文)

function fn(a){
    console.log(a);  // 輸出function a的原始碼,a此時是函式a
    var a = 2;
    function a(){

    }
    console.log(a);  // 2
}
fn(1);
複製程式碼

考察點: 宣告提前 難點: 函式優先

呼叫一開始, 就會先建立一個區域性變數a, (因為a是形參), 然後把實參的值1賦值給aa= 1 幾乎在同一時刻,那麼一瞬間,開始處理函式內變數提升和函式提升 此時,a因為函式提升已經變成了a = function(){} 以上這些過程都是函式執行上下文的預處理過程 接下來,才是正式執行內部函式的程式碼。 console.log(a); 此時輸出的就是function原始碼ƒ a(){} 結尾的輸出語句便輸出a = 2

測試題1: [考查知識點]先預處理變數, 後預處理函式

function a() {} //函式提升
  var a;  //變數提升
  //先預處理變數, 後預處理函式。也就是,函式提升會覆蓋變數提升。
  console.log(typeof a); //function
複製程式碼

測試題2:[考查知識點] 變數預處理, in操作符 (在window上能不能找到b,不管有沒有值)

 if (!(b in window)) {
    var b = 1;  
    //在ES6之前沒有塊級作用域,所以這個變數b 相當於window的全域性變數
  }
  console.log(b); //undefined
複製程式碼

測試題3: [考查知識點]預處理, 順序執行 這個題筆者認為出的相當好。混亂讀者的視角。當然再次強調,面試題是專門命題出來考查的,實際開發上可能有些不會這麼用。但主要作用就是深入理解。

var c = 1;
  function c(c) {
    console.log(c);
    var c = 3;
  }
  c(2); //報錯。 c is not a function

  //這個題包含了變數和函式宣告提升的問題,就是等價於以下的程式碼:
  var c;  //變數提升
  function c(c) {  //函式提升,覆蓋變數提升
    console.log(c);
    var c = 3;   //函式內部的區域性變數(在棧記憶體的封閉記憶體空間裡,外面看不到)
  }
  c=1;//開始真正執行程式碼var c = 1
  console.log(c);
  c(2);  //c is not a function  c是一個變數,值為number型別的數值.怎麼可以執行?
複製程式碼

2.4 作用域與作用域鏈

2.4.1 作用域

1.理解:

  • 作用域:就是一塊"地盤",一塊程式碼區域, 在編碼時就確定了, 不會再變化(見下圖解)定義函式變數時觸發了作用域。執行結束完成作用域生命週期結束。
  • 作用域鏈:多個巢狀的作用域形成的由內向外的結構, 用於查詢變數(見下圖解)

2.分類:

  • 全域性作用域
  • 函式作用域
  • js沒有塊作用域(但在ES6有了!)

3.作用

  • 作用域: 隔離變數, 可以在不同作用域去定義同名的變數,不會造成衝突。不同作用域下同名變數不會有衝突。例如,在全域性中有一個變數b,那麼在函式體中能不能有變數b,當然可以,這就是分隔變數。
  • 作用域鏈: 查詢變數

給個案例:

var a = 10,
    b = 20
  function fn(x) {
    var a = 100,
      c = 300;
    console.log('fn()', a, b, c, x)
    function bar(x) {
      var a = 1000,
        d = 400
      console.log('bar()', a, b, c, d, x)
    }

    bar(100)
    bar(200)
  }
  fn(10);
複製程式碼

輸出結果:

fn() 100 20 300 10
bar() 1000 20 300 400 100
bar() 1000 20 300 400 200
複製程式碼

4.作用域的圖解如下:

作用域的圖解

2.4.2 作用域與執行上下文

1.區別1

  • 全域性作用域之外,每個函式都會建立自己的作用域,作用域在函式定義時就已經確定了。而不是在函式呼叫時。
  • 全域性執行上下文是在全域性作用域確定之後, js程式碼馬上執行之前建立。
  • 函式執行上下文是在呼叫函式時, 函式體程式碼執行之前建立。

2.區別2

  • 作用域是靜態的, 只要函式定義好了就一直存在, 且不會再變化。
  • 執行上下文是動態的, 呼叫函式時建立, 函式呼叫結束時就會自動釋放(不是通過垃圾回收機制回收)。

3.聯絡

  • 執行上下文(物件)是從屬於所在的作用域
  • 全域性上下文環境==>全域性作用域
  • 函式上下文環境==>對應的函式作用域

4.作用域與執行上下文圖解如下:

作用域與執行上下文圖解

2.4.3 作用域鏈

1.理解

  • 多個上下級關係的作用域形成的鏈, 它的方向是從下向上的(從內到外)
  • 查詢變數時就是沿著作用域鏈來查詢的

2.查詢一個變數的查詢規則

  • a.在當前作用域下的執行上下文中查詢對應的屬性, 如果有直接返回, 否則進入b
  • b.在上一級作用域的執行上下文中查詢對應的屬性, 如果有直接返回, 否則進入c
  • c.再次執行2的相同操作, 直到全域性作用域, 如果還找不到就丟擲找不到的異常

3.作用域鏈的圖解如下:

作用域鏈的圖解

2.4.4 習題與案例

面試題1:作用域

<script type="text/javascript">
  var x = 10;
  function fn() {
    console.log(x);  //10
  }
  function show(f) {
    var x = 20;
    f();
  }
  show(fn);
</script>
複製程式碼

記住: 作用域是程式碼一編寫就確定下來了,不會改變。產生多少個作用域?n+1. n就是多少個函式,1就是指的是window。查詢變數就是沿著作用域查詢,而作用域是一開始就確定了,與哪裡呼叫一點關係都沒有。 見圖解:

作用域
面試題:考察作用域與作用域鏈上的查詢

<script type="text/javascript">
  var fn = function () {
    console.log(fn) //output: ƒ () {console.log(fn)}
  }
  fn()

  var obj = {
    fn2: function () {
     console.log(fn2) //報錯,fn2 is not defined 
     console.log(this.fn2)//輸出fn2這個函式物件  
    } 
  }
  obj.fn2()
</script>
複製程式碼

報錯原因:因為首先在這個匿名函式作用域找,找不到去上一層全域性找,沒找到,報錯。找fn2是沿著作用域查詢的! 輸出fn2這個函式物件的原因:如果要找到obj屬性fn2,則用this.fn2(),讓其用this這個指標指向obj,在obj這個物件中找fn2屬性。

面試題3:考察連續賦值隱含的含義

(function(){
    var a = b = 3;
})();

console.log(a);  // 報錯 a is not defined
console.log(b);  // 3
複製程式碼

理解:首先,賦值從右向左看。b = 3因為沒var 所以相當於在全域性作用域中新增b,賦值為3。現在。看前面var a=的部分,a有var,那麼a就是區域性變數放在棧記憶體的封閉記憶體空間上。var a=bb是變數,是基本資料型別的變數。它的記憶體中的內容值就是基本資料型別值,故拷貝一份給a。區域性變數中a=3

匿名函式自執行,會有一個函式執行上下文物件。當函式執行完成,就會把執行上下文棧彈出這個上下文物件。就再也訪問不到。

所以,在函式自執行結束後,再執行輸出a的語句。a壓根就找不到,根本沒定義。b因為是全域性變數還是可以找到滴。

注意擴充套件,這裡只有在非嚴格模式下,才會把b當做全域性變數。若在ES5中嚴格模式下,則會報錯。

面試4:考慮返回值問題

function foo1(){
    return {
        bar: "hello"
    }
}

function foo2(){
    return
    {
        bar: "hello"
    }
}

console.log(foo1()); // 返回一個物件 {bar:'hello'}
console.log(foo2());  // undefined
複製程式碼

解釋:因為foo2函式return後面少了分號,在js引擎解析時,編譯原理的知識可知,在詞法分析會return後面預設加上分號,所以,後面那個物件壓根不執行,壓根不搭理。所以啊,當return時返回的就是undefined

面試5:函式表示式的作用域範圍

<script>
console.log(!eval(function f() {})); //false
var y = 1;
if (function f(){}){
    y += typeof f;
}
console.log(y);  // 1undefined
</script>
複製程式碼

2.5 閉包

2.5.1 引入閉包

Code 1:

<button>測試1</button>
<button>測試2</button>
<button>測試3</button>
<script type="text/javascript">
  var btns = document.getElementsByTagName('button')
  //遍歷加監聽
  for (var i = 0,length=btns.length; i < length; i++) {
    var btn = btns[i]
    btn.onclick = function () {
      alert('第'+(i+1)+'個')
    }
  }
</script>
複製程式碼

輸出結果:不管點選哪個button,都是輸出“第4個”。因為for迴圈一下就執行完了,可是btn.onclick是要等到使用者事件觸發的,故這個時候i3.永遠輸出“第4個”。 一些細節問題:在這個過程中,產生了多少個i?一個i,i是全域性變數啊。 事件模型的處理: 當事件被觸發時,該事件就會對此互動進行響應,從而將一個新的作業(回撥函式)新增到作業佇列中的尾部,這就是js關於非同步程式設計最基本的形式。 事件可以很好的工作於簡單的互動,但將多個分離的非同步呼叫串聯在一起就會很麻煩,因為你必須要追蹤到每個事件的事件物件(例如上面的btn).此外你還要確保所有的事件處理程式都能在事件第一次觸發之前被繫結完畢。例如,若btnonclick被繫結前點選,那麼就不會有任何的事情發生。因此,雖然在響應使用者互動或類似的低頻功能時,事件很有用,但它面對更復雜的需求時仍然不夠靈活。 所以,從這個例子不僅僅是對遍歷加監聽/閉包等理解。從這裡也可以說明事件物件和事件機制的問題。所以,在ES6中會有promise和非同步函式進行更多更復雜需求上的操作。

Code 2 通過物件.屬性儲存i

<button>測試1</button>
<button>測試2</button>
<button>測試3</button>
<script type="text/javascript">
  var btns = document.getElementsByTagName('button')
  //遍歷加監聽
  for (var i = 0,length=btns.length; i < length; i++) {
    var btn = btns[i]
    //將btn所對應的下標儲存在btn上(解決方式1)
    btn.index = i
    btn.onclick = function () {
      alert('第'+(this.index+1)+'個')
    }
  }
</script>
複製程式碼

這個時候就是我們想要的結果,點哪個i ,button就輸出第幾個。

Code 3 通過ES6的塊級作用域 let

<button>測試1</button>
<button>測試2</button>
<button>測試3</button>
<script type="text/javascript">
  var btns = document.getElementsByTagName('button')
  //遍歷加監聽
  for (let i = 0,length=btns.length; i < length; i++) {  //(解決方式二)
    var btn = btns[i]  
    btn.onclick = function () {
      alert('第'+(i+1)+'個')
    }
  }
</script>
複製程式碼

在ES6中引入塊級作用域,使用let即可。

Code 4 利用閉包解決

<button>測試1</button>
<button>測試2</button>
<button>測試3</button>
<script type="text/javascript">
  var btns = document.getElementsByTagName('button')
  //利用閉包
  for (var i = 0,length=btns.length; i < length; i++) {  //這裡的i是全域性變數
    (function (j) {  //這裡的j是區域性變數
      var btn = btns[j]
      btn.onclick = function () { 
        alert('第'+(j+1)+'個')   //這裡的j是區域性變數
      }
    })(i); //這裡的i是全域性變數
  }
</script>
複製程式碼

for迴圈裡有兩個函式,btn.click這個匿名函式就是一個閉包。它訪問了外部函式的變數。 產生幾個閉包?3個閉包(外部函式執行幾次就產生幾個閉包)。每個閉包都有變數j,分別儲存著j=0,j=1,j=2的值。故可以實現這樣的效果。之前之所以出問題,是因為都是用著全域性變數的i,同一個i值。 閉包有沒有被釋放?沒有,一直存在。我們知道閉包釋放,那就是讓指向內部函式的引用變數為null即可。但是此時btn.onclick一直引用這內部函式(匿名函式),故其閉包不會被釋放。 閉包應不應該被釋放?不應該。因為一個頁面的一個button是要一直存在的,頁面顯示過程中,button要一直關聯著這個閉包。才能讓每點選button1alert(第1個)這樣的結果。不可能讓button點選了一次就失效吧。那麼假設要釋放這些閉包,那就讓btn.onclick=null即可。 閉包的作用?延長區域性變數j的生命週期。

2.5.2 理解閉包(Closure)

1.如何產生閉包?

  • 當一個巢狀的內部(子)函式引用了巢狀的外部(父)函式的變數(函式)時, 就產生了閉包。
  • 注1:若外部函式有變數b,而內部函式中沒有引用b,則不會產生閉包。

2.閉包到底是什麼?
閉包是指有權訪問另一個函式作用域中的變數的函式。
可以理解為:
包含了那個區域性變數的容器(不一定是物件,類似物件)
他被內部函式物件引用著
怎麼判斷閉包存在否?最終就是判斷函式物件有沒有被垃圾回收機制。

  • 使chrome`可以除錯檢視閉包的存在
  • 理解一: 閉包是巢狀的內部函式(絕大部分人)
  • 理解二: 包含被引用變數(函式)的物件(極少數人)
  • 若理解二請注意: 閉包存在於巢狀的內部函式中。
  • 口語:首先明白閉包的本質上是一個物件,儲存在內部函式中的物件,這個物件儲存著被引用的變數。
  • 在後臺執行環境中,閉包的作用域包含著它自己的作用域、包含函式的作用域和全域性作用域。

3.產生閉包的條件?

  • 函式巢狀
  • 內部函式引用了外部函式的資料(變數/函式)
  • 執行外部函式(內部函式可以不執行)

案例1:

 function fn1 () {
      var a = 2
      var b = 'abc'
      function fn2 () { //執行函式定義就會產生閉包(不用呼叫內部函式)
        console.log(a)  //引用了外部函式變數,若裡面沒有引用任何的外部函式變數(函式)則不會產生閉包
      }
      // fn2() 內部函式可以不執行,也會產生閉包。只要執行了內部函式的定義就行。但若是函式表示式呢?
    }
    fn1(); //外部函式要執行哦,否則不會產生閉包
複製程式碼

案例2:

 function fun1() {
      var a = 3
      var fun2 = function () {
        console.log(a)
      }
    }
    fun1()
  //這樣子通過函式表示式定義函式,若沒有在裡面呼叫內部函式,則不會產生閉包。
複製程式碼

案例3:

  function fun1() {
      var a = 3
      var fun2 = function () {
        console.log(a)
      }
      fun2()
    }
    fun1()
  //這樣子通過函式表示式定義函式,但在裡面呼叫了內部函式,那麼這個情況是可以產生閉包的。
複製程式碼

函式表示式不同於函式宣告。函式宣告要求有名字,但函式表示式不需要。沒有名字的函式表示式也叫做匿名函式(anonymous function),匿名函式有時候也叫拉姆達函式,匿名函式的 ·name· 屬性是空字串。

以上這些案例,只是輔助理解。並沒有實際上的應用,下面就來說說閉包實際可以應用的地方。

2.5.3 常見的閉包

  1. 將函式作為另一個函式的返回值
  2. 將函式作為實參傳遞給另一個函式呼叫

案例1:將函式作為另一個函式的返回值

 function fn1() {
    var a = 2
    function fn2() {
      a++
      console.log(a)
    }
    return fn2
  }
  var f = fn1()
  f() // 3
  f() // 4
複製程式碼

深入理解: 問題一:有沒有產生閉包? An:條件一,函式的巢狀。外部函式fn1,內部函式fn2,條件一滿足;條件二,內部函式引用了外部函式的資料(變數/函式)。a就是外部函式的資料變數。條件二滿足。條件三,執行外部函式。var f = fn1()其中的fn1()是不是執行了,外部函式執行了。賦值給f變數是因為外部函式fn1在執行後返回一個函式,用全域性變數f來儲存其地址值。條件三滿足。綜上所述,產生了閉包。

問題二:產生了幾個閉包? 產生了一個閉包。我們根據上一節的知識可知:執行函式定義就會產生閉包。那麼執行函式定義是不是隻要執行外部函式即可,因為外部函式一執行,就會有函式上下文物件,就會函式宣告提前,也就是執行了函式定義。那麼,這個時候執行了幾次外部函式?是不是一次。執行了一次外部函式,也就是宣告函式提前了一次,執行函式定義這個操作做了一次,故只產生了一個閉包。也可得出結論,外部函式執行幾次,就產生幾個閉包。跟內部函式執行幾次沒有關係(前提,在可以生成閉包的情況下)

問題三:呼叫內部函式為什麼可以讀出a的最新值? 從結果可以知道,f() ,f()是不是在呼叫了兩次內部函式,從輸出的結果看,a每次輸出最新值。這就可以知道,在執行內部函式的時候,a並沒有消失。記住這點,這就是閉包的本質作用。

問題四:那麼如果我現在在以上程式碼最後(分別輸出3,4語句後面)繼續加入

  var h =fn1();
  h(); 
  f(); 
複製程式碼

這個時候會輸出什麼? An:h()會輸出3. f()會輸出5. 因為:var h = fn1()又執行了一次,h接收返回值函式物件(內部函式),也就是在這個時候產生了新的一個閉包。當呼叫內部函式時,h(),就會有新的函式上下文物件產生,a值就會從初始值開始記錄。當呼叫f()時,這個時候還是在上一個閉包的狀態下,那個作用域並沒有消失,故還在原先的基礎上改變a值。

案例2. 將函式的實參傳遞給另一個函式呼叫(★★★)

 function showDelay(msg, time) {
    setTimeout(function () {
      alert(msg)
    }, time)
  }
  showDelay('my name is ly', 2000)
複製程式碼

這個例子說明了,我們要使用閉包不一定要return出去。只要這個函式物件被引用著就行。return的話那我再外面用變數接收一下就引用著了。但是我使用定時器,定時器模組式在瀏覽器分執行緒執行著的,定時器這個回撥函式就是定時器模組儲存管理著。

深入理解: 問題一:有沒有產生閉包? An:條件一,函式的巢狀。外部函式showDelay ,內部函式定時器的回撥函式,條件一滿足;條件二,內部函式引用了外部函式的資料(變數/函式)。msg就是外部函式的資料變數,而在回撥函式中用了。注意,time不是哦,time還是在外部函式用的,在內部函式中並沒有用到外部函式的time變數。是因為msg變數才滿足條件二。條件三,執行外部函式。showDelay('my name is ly', 2000)執行了,外部函式執行了。但是注意回撥函式沒有宣告提升,故還要等2000ms後觸發進行呼叫回撥函式。這個時候內部函式才執行。條件三滿足(這個類似於函式表示式情況,如果是函式表示式,那麼不僅僅要外部函式要執行,內部函式表示式定義的函式也要有執行,只有這樣才會出現閉包。如果是函式宣告定義的函式,那麼就會有函式執行上下文去建立,函式提升,故在執行函式定義的時候就會出現閉包)。綜上所述,產生了閉包。

2.5.4 閉包的作用

1.使用函式內部的區域性變數在函式執行完後, 仍然存活在記憶體中(延長了區域性變數的生命週期) 本來,區域性變數的生命週期是不是函式開始執行到執行完畢,區域性變數就自動銷燬啦。但是,通過閉包可以延長區域性變數的生命週期,函式內部的區域性變數可以在函式執行完成後繼續存活在記憶體中。那就是通過閉包,具體怎樣的內部機制見下。

2.讓函式外部可以操作(讀寫)到函式內部的資料(變數/函式) 本來,函式內部是可以通過作用域鏈去由內向外去找資料(變數/函式),是可以訪問到函式外部的。但是反過來是不行的,函式外部能訪問到內部的資料(變數/函式)嗎?

 function fun1() {
    var a='hello world'
  }
  console.log(a); //報錯 a is not defined  
  //函式外部不能訪問函式內部的資料
複製程式碼

所以,函式外部不能訪問函式內部的資料。但是,可以通過閉包去訪問到函式內部的資料。具體的內部機制又是怎樣的呢?見下。

//詳解閉包的作用(重要)
function fn1() {
    var a = 2
    function fn2() {
      a++
      console.log(a)
    }
    function fn3() {
      a--
      console.log(a)
    }
    return fn3
  }
  
  var f = fn1()
  f() // 1
  f() // 0
複製程式碼

問題1. 函式fn1()執行完後, 函式內部宣告的區域性變數是否還存在? An: 一般是不存在, 存在於閉包中的變數才可能存在。像fn2 fn3變數就自動銷燬了。因為函式內的區域性變數的生命週期就是函式開始執行到執行完畢的過程。那像fn2這個函式物件也會被垃圾回收機制回收,因為沒有變數去引用(指向)fn2函式物件。但是fn3這個物件還在,根本原因是因為語句 var f = fn1(); fn( )執行完畢返回一個fn3的地址值並且賦值給全域性變數f,那麼全域性變數f就會指向他,所以,這個fn3這個函式物件不會被垃圾回收機制回收。 但我在回答這個問題時,是說存在於閉包中的變數才可能存在。為什麼可能呢?因為如果我把語句var f = fn1(); 改成fn1(),這個時候還是沒有變數去引用,所以這時還是會被回收的。見下圖。

在這裡插入圖片描述
那麼我們發現,閉包一直會存在嗎?這就是我們下一節要講的閉包的生命週期。提前說一下,也就是fn3函式物件一直會有引用,閉包就會存在。這時我只要將f=null,這個時候fn3函式物件就沒有被f引用,所以會被垃圾回收機制回收,故此時這個閉包死亡。

問題2:在函式外部能直接訪問函式內部的區域性變數嗎? An: 不能, 但我們可以通過閉包讓外部操作它.

2.5.5 閉包的生命週期

  1. 產生: 在巢狀內部函式定義執行完時就產生了(不是在呼叫)--->針對的是用函式宣告的內部巢狀函式。
  2. 死亡: 在巢狀的內部函式成為垃圾物件時。
function fn1() {
    //此時閉包就已經產生了(函式提升, 內部函式物件已經建立了)
    var a = 2
    function fn2 () {
      a++
      console.log(a)
    }
    return fn2
  }
  var f = fn1()
  f() // 3
  f() // 4
  f = null //閉包死亡(包含閉包的函式物件成為垃圾物件)
複製程式碼

2.5.6 閉包的應用

閉包應用:

  • 模組化: 封裝一些資料以及運算元據的函式, 向外暴露一些行為。(本節具體講)從這可以引出四大模組化思想。
  • 迴圈遍歷加監聽(在2.5.1引入閉包章節有講)
  • JS框架(jQuery)大量使用了閉包

閉包的應用之一:定義JS模組

  1. 要具有特定功能的js檔案

  2. 將所有的資料和功能都封裝在一個函式內部(私有的)(函式內部會有作用域與作用域鏈的概念,函式內部的資料就是私有的,外部訪問不到。)

  3. 只向外暴露一個包含n個方法的物件(暴露多個行為)或函式(暴露一個行為)

  4. 模組的使用者, 只需要通過模組暴露的物件呼叫方法來實現對應的功能

自定義JS模組一:

function myModule() {
  //私有資料
  var msg = 'Hello world';
  //運算元據的函式
  function doSomething() {
    console.log('doSomething() '+msg.toUpperCase())
  }
  function doOtherthing () {
    console.log('doOtherthing() '+msg.toLowerCase())
  }

  //向外暴露物件(給外部使用的方法)
  return {
    doSomething: doSomething,
    doOtherthing: doOtherthing
  }
}

//怎麼使用?在html頁面中
  var module = myModule()
  module.doSomething()
  module.doOtherthing()
複製程式碼

自定義JS模組二:

(function () {
  //私有資料
  var msg = 'My atguigu'
  //運算元據的函式
  function doSomething() {
    console.log('doSomething() '+msg.toUpperCase())
  }
  function doOtherthing () {
    console.log('doOtherthing() '+msg.toLowerCase())
  }

  //向外暴露物件(給外部使用的方法)
  window.myModule2 = {
    doSomething: doSomething,
    doOtherthing: doOtherthing
  }
})();

//怎麼使用?在html頁面中
  myModule2.doSomething()
  myModule2.doOtherthing()
//這種自定義模組相對而言更好,因為不需要先呼叫外部函式,直接使用 myModule2.doSomething()更加方便。
複製程式碼

2.5.7 閉包的缺點和解決

1.缺點

  • 函式執行完後, 函式內的區域性變數沒有釋放, 佔用記憶體時間會變長(是優點亦是缺點)
  • 容易造成記憶體洩露(注意和記憶體溢位的區別,見1.5.3節)

2.解決

  • 能不用閉包就不用
  • 及時釋放
<script type="text/javascript">
  function fn1() {
    var arr = new Array[100000]
    function fn2() {
      console.log(arr.length)
    }
    return fn2
  }
  var f = fn1()
  f()
  //這裡是有閉包的,arr一直沒有釋放,很佔記憶體。
  //如何解決?很簡單。
  f = null //讓內部函式成為垃圾物件-->回收閉包

</script>
複製程式碼
  • 理解:

    • 當巢狀的內部函式引用了外部函式的變數時就產生了閉包
    • 通過chrome工具得知: 閉包本質是內部函式中的一個物件, 這個物件中包含引用的變數屬性
  • 作用:

    • 延長區域性變數的生命週期
    • 讓函式外部能操作內部的區域性變數
  • 寫一個閉包程式

  function fn1() {
    var a = 2;
    function fn2() {
      a++;
      console.log(a);
    }
    return fn2;
  }
  var f = fn1();
  f();
  f();
複製程式碼
  • 閉包應用:

    • 模組化: 封裝一些資料以及運算元據的函式, 向外暴露一些行為
    • 迴圈遍歷加監聽
    • JS框架(jQuery)大量使用了閉包
  • 缺點:

    • 變數佔用記憶體的時間可能會過長
    • 可能導致記憶體洩露
    • 解決:
      • 及時釋放 :f = null; //讓內部函式物件成為垃圾物件

2.6.9 習題與案例

面試1:考察閉包

function foo(){
    var m = 1;
    return function (){
        m++;
        return m;
    }
}

var f = foo();  //這會形成一個閉包  (呼叫一次外部函式)
var f1 = foo();  //這會形成一個閉包 (呼叫一次外部函式)
/*不同閉包有不同作用域。同一個閉包可以訪問其最新的值。--這句話知識一個表面現象,結合上面的案例去發現深入的步驟,這個過程是如何執行的?*/
console.log(f()); // 2
console.log(f1()); // 2
console.log(f()); // 3
console.log(f()); // 4
複製程式碼

面試2:閉包相關知識

<script>
function fun(n, o){
    console.log(o);  //實則就是輸出閉包中的變數值,n是閉包引用的變數。延長的是n的變數生命週期。
    return {
        fun: function (m){
            return fun(m, n);
        }
    }
}
/*注意以上程式碼段是有閉包的,return fun(m,n)中的n是用到了外部函式的變數n*/

//測試一:
var a = fun(0);  // undefined
a.fun(1); // 0  執行這裡實則是產生了新的閉包,但沒有變數去指向這個內部函式產生的閉包故馬上就消失啦。
a.fun(2); // 0
a.fun(3); // 0
/*最後三行語句一直用的閉包是fun(0)產生的閉包*/

//測試二: 
var b = fun(0).fun(1).fun(2).fun(3); //undefined 0 1 2
/*產生了四個閉包,也就是外部函式fun(n,o)呼叫過4次。*/

// 測試三:
var c = fun(0).fun(1); // undefined 0
c.fun(2)  // 1
c.fun(3)  // 1
/*最後兩行語句一直用的閉包是fun(0).fun(1)產生的閉包,故其語句*/

</script>
複製程式碼

面試3:寫一個函式, 使下面的兩種呼叫方式都正確

console.log(sum(2,3));   // Outputs 5
console.log(sum(2)(3));  // Outputs 5
複製程式碼

答案:

<script>
function sum(){
    if(arguments.length == 2){
        return arguments[0] + arguments[1];
    }else if(arguments.length == 1){
        var first = arguments[0];
        return function (a){
            return first + a;
        }
    }
}
</script>
複製程式碼

JS的函式篇(4.3W字)
此文件為呂涯原創,可任意轉載,但請保留原連結,標明出處。
文章只在CSDN和掘金第一時間釋出:
CSDN主頁:https://blog.csdn.net/LY_code
掘金主頁:https://juejin.im/user/5b220d93e51d4558e03cb948
若有錯誤,及時提出,一起學習,共同進步。謝謝。 ???

相關文章