前端基礎-你真的懂函數嗎?

前言

衆所周知,在前端開發領域中,函數是一等公民,因而可知函數的重要性,本文旨在介紹函數中的一些特性與方法,對函數有更好的認知前端

正文

1.箭頭函數

ECMAScript 6 新增了使用胖箭頭(=>)語法定義函數表達式的能力。很大程度上,箭頭函數實例化的函數對象與正式的函數表達式建立的函數對象行爲是相同的。任何可使用函數表達式的地方,均可以使用箭頭函數:es6

let arrowSum = (a, b) => { 
 return a + b; 
}; 
let functionExpressionSum = function(a, b) { 
 return a + b; 
}; 
console.log(arrowSum(5, 8)); // 13 
console.log(functionExpressionSum(5, 8)); // 13

使用箭頭函數須知:面試

  • 箭頭函數的函數體若是不用大括號括起來會隱式返回這行代碼的值
  • 箭頭函數不能使用 argumentssupernew.target,也不能用做構造函數
  • 箭頭函數沒有 prototype 屬性

2.函數聲明與函數表達式

JavaScript 引擎在任何代碼執行以前,會先讀取函數聲明,並在執行上下文中生成函數定義。而函數表達式必須等到代碼執行到它那一行,纔會在執行上下文中生成函數定義。windows

// 沒問題 
console.log(sum(10, 10)); 
function sum(num1, num2) { 
 return num1 + num2; 
}

以上代碼能夠正常運行,由於函數聲明會在任何代碼執行以前先被讀取並添加到執行上下文。這個過程叫做函數聲明提高(function declaration hoisting)。在執行代碼時,JavaScript 引擎會先執行一遍掃描,把發現的函數聲明提高到源代碼樹的頂部。所以即便函數定義出如今調用它們的代碼以後,引擎也會把函數聲明提高到頂部。若是把前面代碼中的函數聲明改成等價的函數表達式,那麼執行的時候就會出錯:數組

// 會出錯
console.log(sum(10, 10)); 
let sum = function(num1, num2) { 
 return num1 + num2; 
};

上述代碼的報錯有一些同窗可能認爲是let致使的暫時性死區。其實緣由並不出在這裏,這是由於這個函數定義包含在一個變量初始化語句中,而不是函數聲明中。這意味着代碼若是沒有執行到let的那一行,那麼執行上下文中就沒有函數的定義。你們能夠本身嘗試一下,就算是用var來定義,也是同樣會出錯。瀏覽器

3.函數內部

在 ECMAScript 5 中,函數內部存在兩個特殊的對象:argumentsthis。ECMAScript 6 又新增了 new.target 屬性。閉包

arguments函數

它是一個類數組對象,包含調用函數時傳入的全部參數。這個對象只有以 function 關鍵字定義函數(相對於使用箭頭語法建立函數)時纔會有。但 arguments 對象其實還有一個 callee 屬性,是一個指向 arguments 對象所在函數的指針。this

function factorial(num) { 
 if (num <= 1) { 
 return 1; 
 } else { 
 return num * factorial(num - 1); 
 } 
}

// 上述代碼能夠運用arguments來進行解耦
function factorial(num) { 
 if (num <= 1) { 
 return 1; 
 } else { 
 return num * arguments.callee(num - 1); 
 } 
}

這個重寫以後的 factorial()函數已經用 arguments.callee 代替了以前硬編碼的 factorial。這意味着不管函數叫什麼名稱,均可以引用正確的函數。編碼

arguments.callee 的解耦示例
let trueFactorial = factorial; 
factorial = function() { 
 return 0; 
}; 
console.log(trueFactorial(5)); // 120 
console.log(factorial(5)); // 0

這裏 factorial 函數在賦值給trueFactorial後被重寫了 那麼咱們若是在遞歸中不使用arguments.callee 那麼顯然trueFactorial(5)的運行結果也是0,可是咱們解耦以後,新的變量仍是能夠正常的進行

this

函數內部另外一個特殊的對象是 this,它在標準函數和箭頭函數中有不一樣的行爲。

在標準函數中,this 引用的是把函數當成方法調用的上下文對象,這時候一般稱其爲 this 值(在網頁的全局上下文中調用函數時,this 指向 windows)。

在箭頭函數中,this引用的是定義箭頭函數的上下文。

caller

這個屬性引用的是調用當前函數的函數,或者若是是在全局做用域中調用的則爲 null。

function outer() { 
 inner(); 
} 
function inner() { 
 console.log(inner.caller); 
} 
outer();

以上代碼會顯示 outer()函數的源代碼。這是由於 ourter()調用了 inner(),inner.caller指向 outer()。若是要下降耦合度,則能夠經過 arguments.callee.caller 來引用一樣的值:

function outer() { 
 inner(); 
} 
function inner() { 
 console.log(arguments.callee.caller); 
} 
outer();

new.target

ECMAScript 中的函數始終能夠做爲構造函數實例化一個新對象,也能夠做爲普通函數被調用。ECMAScript 6 新增了檢測函數是否使用 new 關鍵字調用的 new.target 屬性。若是函數是正常調用的,則 new.target 的值是 undefined;若是是使用 new 關鍵字調用的,則 new.target 將引用被調用的構造函數。

function King() { 
 if (!new.target) { 
 throw 'King must be instantiated using "new"' 
 } 
 console.log('King instantiated using "new"'); 
} 
new King(); // King instantiated using "new" 
King(); // Error: King must be instantiated using "new"

這裏能夠作一些延申,還有沒有其餘辦法來判斷函數是否經過new來調用的呢?

可使用 instanceof 來判斷。咱們知道在new的時候發生了哪些操做?用以下代碼表示:

var p = new Foo()
// 實際上執行的是
// 僞代碼
var o = new Object(); // 或 var o = {}
o.__proto__ = Foo.prototype
Foo.call(o)
return o

上述僞代碼在MDN是這麼說的:

  1. 一個繼承自 Foo.prototype 的新對象被建立。
  2. 使用指定的參數調用構造函數 Foo,並將 this 綁定到新建立的對象。new Foo 等同於 new Foo(),也就是沒有指定參數列表,Foo 不帶任何參數調用的狀況。
  3. 由構造函數返回的對象就是 new 表達式的結果。若是構造函數沒有顯式返回一個對象,則使用步驟1建立的對象。(通常狀況下,構造函數不返回值,可是用戶能夠選擇主動返回對象,來覆蓋正常的對象建立步驟)

new 的操做說完了 如今咱們看一下 instanceof,MDN上是這麼說的:instanceof 運算符用於檢測構造函數的 prototype 屬性是否出如今某個實例對象的原型鏈上。

也就是說,A的N個__proto__ 全等於 B.prototype,那麼A instanceof B返回true,如今知識點已經介紹完畢,能夠開始上代碼了

function Person() {
        if (this instanceof Person) {
          console.log("經過new 建立");
          return this;
        } else {
          console.log("函數調用");
        }
      }
      const p = new Person(); // 經過new建立
      Person(); // 函數調用

解析:咱們知道new構造函數的this指向實例,那麼上述代碼不可貴出如下結論this.__proto__ === Person.prototype。因此這樣就能夠判斷函數是經過new仍是函數調用

這裏咱們其實還能夠將 this instanceof Person 改寫爲 this instanceof arguments.callee

4.閉包

終於說到了閉包,閉包這玩意真的是面試必問,因此掌握仍是頗有必要的

閉包指的是那些引用了另外一個函數做用域中變量的函數,一般是在嵌套函數中實現的。

function foo() {
    var a = 20;
    var b = 30;

    function bar() {
        return a + b;
    }
  return bar;
}

上述代碼中,因爲foo函數內部的bar函數使用了foo函數內部的變量,而且bar函數return把變量return了出去,這樣閉包就產生了,這使得咱們能夠在外部拿到這些變量。

const b = foo();
b() // 50

foo函數在調用的時候建立了一個執行上下文,能夠在此上下文中使用a,b變量,理論上說,在foo調用結束,函數內部的變量會v8引擎的垃圾回收機制經過特定的標記回收。可是在這裏,因爲閉包的產生,a,b變量並不會被回收,這就致使咱們在全局上下文(或其餘執行上下文)中能夠訪問到函數內部的變量。

我以前看到了一個說法:

不管什麼時候聲明新函數並將其賦值給變量,都要存儲函數定義和閉包,閉包包含在函數建立時做用域中的全部變量,相似於揹包,函數定義附帶一個小揹包,它的包中存儲了函數定義時做用域中的全部變量

以此引伸出一個經典面試題

for (var i = 1; i <= 5; i++) {
  setTimeout(function timer() {
    console.log(i);
  }, i * 1000);
}

怎樣可使得上述代碼的輸出變爲1,2,3,4,5?

使用es6咱們能夠很簡單的作出解答:將var換成let。

那麼咱們使用剛剛學到的閉包知識怎麼來解答呢?代碼以下:

for (var i = 1; i <= 5; i++) {
(function (i) {
  setTimeout(function timer() {
    console.log(i);
  }, i * 1000);
})(i)
}

根據上面的說法,將閉包當作一個揹包,揹包中包含定義時的變量,每次循環時,將i值保存在一個閉包中,當setTimeout中定義的操做執行時,則訪問對應閉包保存的i值,便可解決。

5.當即調用的函數表達式(IIFE)

以下就是當即調用函數表達式

(function() { 
 // 塊級做用域 
})();

使用 IIFE 能夠模擬塊級做用域,即在一個函數表達式內部聲明變量,而後當即調用這個函數。這樣位於函數體做用域的變量就像是在塊級做用域中同樣。

// IIFE 
(function () { 
 for (var i = 0; i < count; i++) { 
 console.log(i); 
 } 
})(); 
console.log(i); // 拋出錯誤

ES6的塊級做用域:

// 內嵌塊級做用域 
{ 
 let i; 
 for (i = 0; i < count; i++) { 
 console.log(i); 
 } 
} 
console.log(i); // 拋出錯誤
// 循環的塊級做用域
for (let i = 0; i < count; i++) { 
 console.log(i); 
} 
console.log(i); // 拋出錯誤

IIFE的另外一個做用就是上文中的解決settimeout的輸出問題

附錄知識點

關於instanceof

Function instanceof Object;//true
Object instanceof Function;//true

上述代碼你們能夠嘗試在瀏覽器中跑一下,很是的神奇,那麼這是什麼緣由呢?

借用大佬的一張圖

image.png

那麼由此就能夠獲得

//構造器Function的構造器是它自身
Function.constructor=== Function;//true

//構造器Object的構造器是Function(由此可知全部構造器的constructor都指向Function)
Object.constructor === Function;//true



//構造器Function的__proto__是一個特殊的匿名函數function() {}
console.log(Function.__proto__);//function() {}

//這個特殊的匿名函數的__proto__指向Object的prototype原型。
Function.__proto__.__proto__ === Object.prototype//true

//Object的__proto__指向Function的prototype,也就是上面中所述的特殊匿名函數
Object.__proto__ === Function.prototype;//true
Function.prototype === Function.__proto__;//true

結論:

  1. 全部的構造器的constructor都指向Function
  2. Function的prototype指向一個特殊匿名函數,而這個特殊匿名函數的__proto__指向Object.prototype

結尾

本文主要參考 《JavaScript 高級程序設計 第四版》 因爲做者水平有限,若有錯誤,敬請與我聯繫,謝謝您的閱讀!

相關文章
相關標籤/搜索