JS的函數篇(4.3W字)

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

1. JS基礎知識深刻總結
2. 對象高級
3. 函數高級
4. 事件對象與事件機制html

暫時會對以上四個專題去總結,如今開始Part3:函數高級。下圖是我這篇的大綱。 java

js函數學習大綱

3.1 this的使用總結

this是在函數執行的過程當中自動建立的一個指向一個對象的內部指針。確切的說,this並非一個對象,而是指向一個已經存在的對象的指針,也能夠認爲是this就是存儲了某個對象的地址。面試

this的指向不是固定的,會根據調用的不一樣,而指向不一樣的地方。 此章節的講解思路是這樣的: chrome

this總結

3.1.1 搞懂this指向的預備知識

  • 對在全局做用域中定義的變量和函數的進一步認識編程

    永遠記住:只要是在全局做用域聲明的任何變量和函數默認都是做爲window對象的屬性而存在的.json

    理解完以上的這句話,咱們在來講明一下其中的區別,這是不少人沒有關注過的。數組

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

    註解:也就是在未對變量(a)進行聲明時,就會出現以上結果。首先明確一點,就是全局變量awindow的屬性。因此,咱們從這裏就能夠發現,何時undefined,何時報錯?——那就是若是是訪問一個對象的屬性時,它沒有聲明賦值,那就是undefined;若是訪問一個變量,它沒有聲明賦值,那就是報錯。promise

    好,如今回頭過來,咱們看全局做用域變量和函數的認識。瀏覽器

    <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>
複製代碼

此文檔爲呂涯原創,可任意轉載,但請保留原連接,標明出處。 文章只在CSDN和掘金第一時間發佈: CSDN主頁:https://blog.csdn.net/LY_code 掘金主頁:https://juejin.im/user/5b220d93e51d4558e03cb948 如有錯誤,及時提出,一塊兒學習,共同進步。謝謝。 😝😝😝

相關文章
相關標籤/搜索