javascript-函式表示式

walker_jiang發表於2019-02-16

函式表示式

  • 定義:函式表示式區別於函式宣告,也是一種定義函式的方式,形似與變數賦值,這個值就是函式體,例如:

    var a = function(){}; // 函式表示式之匿名函式
    var a = function fn(){}; // 函式表示式之具名函式
    (function(){})(); // 匿名函式之立即執行函式
    // 目前知道的是這三種形式, 希望高人補充
  • 特點:
    1 . 區別於函式宣告,和普通變數一樣使用前必須宣告,不宣告在非嚴格模式下被認為是全域性的變數,在嚴格模式下報錯

遞迴

  • 定義:在一個函式中呼叫自身,遞迴必須要有結束條件階乘

    // fibonacci數列
    function fibonacci(n){
      if(n == 1 || n == 2){ // 結束條件
        return 1;
      }else{
        var num = fibonacci(n-1) + fibonacci(n-2); // 遞迴呼叫
        return num // 每一層遞迴都返回和
      }
    };
    console.log(fibonacci(6)); // 8
  • 特點:
    1 . 呼叫匿名函式表示式自身,為了便於維護,可以通過arguments.callee(指向當前函式的指標)來呼叫當前函式,這樣做的好處是當遞迴函式換名稱時不用更換內部的函式名稱

    function fibonacci(n){
      if(n == 1){
        return 1;
      }else{
        var num = arguments.callee(n-1) * n ;
        return num
      }
    };
    let a = fibonacci;
    fibonacci = null;
    console.log(a(6)); // 函式內在再次呼叫fibonacci就會報錯 Uncaught TypeError: fibonacci is not a function

一變

function fibonacci(n){
  if(n == 1){
    return 1;
  }else{
    var num = arguments.callee(n-1) * n ;
    return num
  }
};
let a = fibonacci;
fibonacci = null;
console.log(a(6)); // 720 但是在嚴格模式下回報錯 Uncaught TypeError: `caller`, `callee`, and `arguments` properties may not be accessed

二變

`use strict`;
let a = (function fibonacci(n){
  if(n == 1){
    return 1;
  }else{
    var num = fibonacci(n-1) * n ;
    return num
  }
});
let b = a;
a=null;
console.log(b(4)); // 24 ,這裡相當於包了一層,如果外邊的變數改變是不會影響函式內部呼叫的

:在嚴格模式下arguments.callee會報錯,可以通過命名函式表示式來實現

閉包

  • 閉包是指有權訪問另一個作用域中的變數的函式,形式很多,不舉例了,重點在下面
  • 特點:關於閉包的特點都得先理解執行環境、作用域、活動物件的概念

    執行環境: 函式被呼叫時會建立當前函式的執行環境,可以想象成一個物件,物件包含一個屬性,該屬性指向作用域(作用域連結串列)
    作用域,也可以看成是作用域連結串列,一個list,list的元素是指向活動物件,包括本作用域內的活動物件的指向和對上級的指向,當前函式執行完畢作用域就消失了
    活動物件,包含當前函式內的所有屬性,當沒有作用域鏈引用時活動物件被銷燬

:指向可以理解為引用,像 a=[], a就指向了記憶體中的一個陣列

{                                              
  scope: scopeList              ----|             
}                                   |
執行環境(context)                  |
[                                   |
  activeObj1: activeObjects1,     --|--|
  activeObj2: activeObjects2,     --|--|--|
  ...                               |  |  |
]                                   |  |  |
作用域鏈(scopeList)           <----|  |  |
{                                      |  |  
  var1: 1,                             |  |
  var2: 1,                             |  |
  var1: [],                            |  |
}                                      |  |
活動物件(activeObjects1)           <--|  |
{                                         |
  _var1: 1,                               |
  _var2: 1,                               |
  _var1: [],                              |
}                                         |
活動物件(activeObjects2)              <--|  

// 可以看出執行環境和作用域鏈是一對一的, 所以當執行完函式後執行環境就沒了,作用域沒有被引用了就也沒了,但是活動物件和作用域鏈是多對多的(途中只展示了一對多) ,所以就算作用域沒了,當前作用域的活動物件也可能被其它作用域引用(例如閉包),所以仍然存在於記憶體中

閉包與變數

  • 特點:閉包對外層活動物件是引用不是複製(也可以說是複製了引用),這裡寫一個親身經歷的筆試題

    var nAdd = null;
    function f(){
      let n = 99;
      nAdd = () => {
       ++n;
      }
      return () => {
       console.log(n);
      }
    };
    var f1 = f();
    var f2 = f();
    nAdd();
    f1();
    f2();
    nAdd();
    f1();
    f2();

    我認為這個題挺有意思。這裡不給答案,讀者可以自己先猜一下,然後自己跑一下和自己的猜想對對。
    我認為這個題目的關鍵在nAdd是在f函式的外層,也就是每次例項化這個f函式都會對nAdd重新賦值,重新賦值後執行環境中n會不同,多次賦值取最後一個,只要能搞清楚執行環境、作用域、活動物件的關係其實不難,不會也沒關係,一開始看到我也懵。

關於this物件

  • 定義:this是和執行環境繫結的
  • 特點:特點和定義息息相關,this和執行環境繫結,只要執行完畢執行環境不存在了就,例如:

    var name = `china,hebei`;
    var province = {
      name: `hebei`,
      getName: function(){
        return function(){
         return this.name;
        };
      }
    };
    console.log(province.getName()()); // china,hebei

從結果來看this指的是全域性的那個this,這個和常規理解不太一樣,按說getName屬於province這個物件,this指向province才對,想想定義就明白了,province.getName()這個執行了getName函式,並返回了一個函式體,再次執行這個函式體的時候getName()已經執行完了,執行完了執行環境當然就不存在了,this就返回給執行的環境(全域性環境),那如何改成指向province呢?

var name = `china,hebei`;
var province = {
  name: `hebei`,
  getName: function(){
    let that = this;
    return function(){
     return that.name;
    };
  }
};
console.log(province.getName()()); // hebei

很容易理解,that在getName執行完畢後並不會消失,因為它所在的活動物件還被最後返回的函式的作用域鏈引用著,所以最後輸出的就是hebei

塊級作用域

  • 定義:通過建立一個立即執行函式來模仿模組作用域的效果,普通的{}沒有塊級概念

    for(var i=0; i<2; i++){
      console.log(i);
    }
    console.log(i) // 2

塊級作用域,很簡單,通過函式作用域封裝一層即可,例如

(function(){
  for(var i=0; i<2; i++){
    console.log(i);
  }
})()
console.log(i) // Uncaught ReferenceError: i is not defined

私有變數

  • 定義:在函式定義的變數和方法都可以看成是私有變數,可以通過在函式建立閉包實現在函式外部訪問私有變數,稱之為共有方法(特權方法),例如:

    function Student(){
      var name = `jiang`;
      this.getName = function(){
        return name;
      };
    };
    var xiaoming = new Student();
    console.log(xiaoming.getName()); // jiang   只有這種特權方法可以訪問到

    特點:私有變數只能通過特權方法在函式外部被訪問
    解決的問題:增強函式的封裝性,函式作用域內得變數只能通過特權方法訪問
    帶來的問題:每個例項都會重新建立一個特權方法

靜態私有變數

  • 定義: 在私有作用域內(立即執行函式)定義函式內的私有變數和全域性的(變數沒有宣告就賦值時)匿名函式,為匿名函式新增原型方法,原型方法內訪問函式內的變數,這樣在函式外部可以可以通過變數名稱直接訪問全域性的匿名函式上的原型方法,方法內部可以訪問函式私有變數

    (function(){
      let name = `jiang`;
      student = function(){};
      student.prototype.getName = function(){
        return name;
      };
    })()
    console.log(student.prototype.getName()); // jiang
  • 解決的問題:解決了每個例項都不共享的私有變數和特權方法的問題
  • 帶來的問題:解決的問題也變成了它自身的問題,最好的方案是私有變數和靜態私有變數結合使用

模組模式

  • 定義:模組模式就是把私有變數和單例模式結合起來,在JS中通過字面物件來建立物件是最簡單的單例模式,而私有變數是函式作用域被的,方法就是定義一個變數(單例物件),然後建立一個立即執行函式返回一個字面物件,物件內部建立公共的特權方法和屬性,函式內部定義私有變數。

單例模式,例如

var a = {};
var a = {}; // 總是隻有一個a物件
var a = (function(){
  var name = `jiang`; // 私有變數
  return{ // 單例模式
    getName: function(){
      return name;
    }
  }
})()
console.log(a.getName());
  • 解決的問題:在單例內建立私有變數, 單例模式的應用場景是需要重複使用但不需要同時使用的物件,像錯誤提示彈框
  • 帶來的問題:返回的物件是沒有型別的就是不能通過instanceof確認物件型別

增強版的模組模式

  • 定義:將函式內返回的物件通過建構函式的方式宣告,然後為其新增特權方法和屬性,然後將物件返回,這樣的物件就可以通過instanceof確認其型別了

    var a = (function(){
      var name = `jiang`;
      function Student(){};
      var xiaoming = new Student();
      xiaoming.getName = function(){
        return name;
      };
      return xiaoming;
    })()

相關文章