進擊JavaScript核心 --- (2)函數和預解析機制

1、函數java

每一個函數都是 Function類型的實例,也具備屬性和方法。因爲函數也是一個對象,所以函數名實際上也是一個指向函數對象的指針,不會與某個函數綁定

一、函數的定義方式
(1)、函數聲明
function add(a, b) {
  return a + b;
}
函數聲明提高:在執行代碼以前,會先讀取函數聲明,也就是說,能夠把函數聲明放在調用它的代碼以後
fn();   // 1
function fn() {console.log(1)}
 
(2)、函數表達式
var add = function(a, b) {
  return a + b;
};
 
函數表達式看起來像是常規的變量賦值,因爲其function關鍵字後面沒有指定函數名,所以是一個匿名函數
函數表達式必須先賦值,不具有函數聲明提高的特性
fn();   // Uncaught TypeError: fn is not a function
var fn = function(){console.log(1)};
 
因爲函數聲明提高這一特性,致使在某些狀況下會出現意想不到的結果,例如:
var flag = true;
if(flag) {
  function fn() {
    console.log('flag 爲true')
  }
} else{
  function fn() {
    console.log('flag 爲false')
  }
}
fn();
// chrome, firefox, ie11  輸出 flag 爲true
// ie10及如下 輸出 flag 爲false
 
本意是想flag爲true時輸出 'flag 爲true', flag爲false時輸出 'flag 爲false',爲什麼結果卻不盡相同呢?究其緣由就在於函數聲明提高,執行代碼時首先讀取函數聲明,而 if...else...代碼塊同屬於全局做用域,所以後面的同名函數會覆蓋前面的函數,最終函數fn就只剩下一個 function fn(){console.log('flag 爲false')}
 
因爲函數聲明提高致使的這一結果使人大爲意外,所以,js引擎會嘗試修正錯誤,將其轉換爲合理狀態,但不一樣瀏覽器版本的作法並不一致
此時,函數表達式就能夠解決這個問題
var flag = true;
var fn;

if(flag) {
  fn = function() {
    console.log('flag 爲true');
  }
} else{
  fn = function() {
    console.log('flag 爲false');
  }
}
fn()

//chrome, firefox, ie7-11 均輸出 flag 爲true
 
其實這個也很好理解,js預解析時,fn和flag均被初始化爲undefined,而後代碼從上到下逐行執行,首先給flag賦值爲true,進入if語句,爲fn賦值爲 function fn(){console.log('flag 爲true')}

關於函數表達式,還有一種寫法, 命名函數表達式
var add = function f(a, b) {
  console.log(a + b);
}
add(1,2);     // 3
f(1,2);       // Uncaught ReferenceError: f is not defined

var add = function f(a, b) {
  console.log(f);
}
console.log(add);
add(3, 5);


// ƒ f(a, b) {
//   console.log(f);
// }
因而可知,命名函數f也是指向函數的指針,只在函數做用域內部可用

(3)、Function構造函數
var add = new Function('a', 'b', 'return a + b');
 
不推薦這種寫法,由於這種語句會致使解析兩次代碼,第一次是解析js代碼,第二次解析傳入構造函數中的字符串,從而影響性能
 
二、沒有重載
在java中,方法具備重載的特性,即一個類中能夠定義有相同名字,但參數不一樣的多個方法,調用時,會根據不一樣的參數選擇不一樣的方法
public void add(int a, int b) {
  System.out.println(a + b);
}

public void add(int a, int b, int c) {
  System.out.println(a * b * c);
}

// 調用時,會根據傳入參數的不一樣,而選擇不一樣的方法,例如傳入兩個參數,就會調用第一個add方法
 
而js則沒有函數重載的概念
function add(a, b) {
  console.log(a + b);
}

function add(a, b, c) {
  c = c || 2;
  console.log(a * b * c);
}

add(1, 2);   // 4  (直接調用最後一個同名的函數,並無重載)
 
因爲函數名能夠理解成一個指向函數對象的指針,所以當出現同名函數時,指針就會指向最後一個出現的同名函數,就不存在重載了(以下圖所示)
三、調用匿名函數
對於函數聲明和函數表達式,調用函數的方式就是在函數名(或變量名)後加一對圓括號
function fn() {
  console.log('hello')
}
fn()

// hello

 

既然fn是一個函數指針,指代函數的代碼段,那可否直接在代碼段後面加一對圓括號呢?chrome

function fn() {
  console.log('hello')
}()

// Uncaught SyntaxError: Unexpected token )

var fn = function() {
  console.log('hello')
}()

// hello

 

分別對函數聲明和函數表達式執行這一假設,結果出人意料。另外,前面也提到函數聲明存在函數聲明提高,函數表達式不存在,若是在函數聲明前加一個合法的JS標識符呢?
console.log(fn);   // ƒ fn() {console.log('hello');}
function fn() {
  console.log('hello');
}

// 在function關鍵字前面加一個合法的字符,結果就把fn當作一個未定義的變量了
console.log(fn);   // Uncaught ReferenceError: fn is not defined
+function fn() {
  console.log('hello');
}

 

基於此能夠大膽猜想,只要是function關鍵字開頭的代碼段,js引擎就會將其聲明提早,因此函數聲明後加一對圓括號會認爲是語法錯誤。結合函數表達式後面直接加圓括號調用函數成功的狀況,作出以下嘗試:
+function() {
  console.log('hello')
}()

-function() {
  console.log('hello')
}()

*function() {
  console.log('hello')
}()

/function() {
  console.log('hello')
}()

%function() {
  console.log('hello')
}()

// hello
// hello
// hello
// hello
// hello

 

居然所有成功了,只是這些一元運算符在此處並沒有實際意義,看起來使人費解。換成空格吧,又會被js引擎給直接跳過,達不到目的,所以能夠用括號包裹起來
(function() {
  console.log('hello');
})();

(function() {
  console.log('hello');
}());

// hello
// hello
不管怎麼包,均可以成功調用匿名函數了,咱們也不用再困惑調用匿名函數時,圓括號該怎麼加了
 
四、遞歸調用
遞歸函數是在一個函數經過名字調用自身的狀況下構成的
 
一個經典的例子就是計算階乘
// 3! = 3*2*1
// 4! = 4*3*2*1 = 4*3!

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

console.log(factorial(5))   // 120
console.log(factorial(4))   // 24
 
若是如今把函數名factorial換成了jieCheng,執行jieCheng(5) 就會報錯了,外面改了,裏面也得改,若是是遞歸的層次較深就比較麻煩。事實上,這樣的代碼也是不夠健壯的

這裏有兩種解決方案:
 
(1)、使用 arguments.callee
arguments.callee 是一個指向正在執行的函數的指針,函數名也是指向函數的指針,所以,能夠在函數內部用 arguments.callee 來替代函數名
function fn() {
  console.log(arguments.callee)
}
fn()

// ƒ fn() {
//   console.log(arguments.callee)
// }

function factorial(num) {
  if(num <= 1) {
    return 1
  }
  return num * arguments.callee(num - 1)
}

console.log(factorial(5))   // 120

 

但在嚴格模式下,不能經過腳本訪問 arguments.callee,訪問這個屬性會致使錯誤
'use strict'

function factorial(num) {
  if(num <= 1) {
    return 1
  }
  return num * arguments.callee(num - 1)
}

console.log(factorial(5))   
// Uncaught TypeError: 'caller', 'callee', and 'arguments' properties may not be accessed on strict mode functions or the arguments objects for calls to them

 

(2)、命名函數表達式
var factorial = function jieCheng(num) {
  if(num <= 1) {
    return 1
  }
  return num * jieCheng(num - 1)
};
console.log(factorial(5))  // 120

var result = factorial;
console.log(result(4));    // 24

 

五、間接調用
apply()和 call()。這兩個方法的用途都是在特定的做用域中調用函數,實際上等於設置函數體內 this 對象的值。
 
首先,apply()方法接收兩個參數:一個是在其中運行函數的做用域,另外一個是參數數組。其中,第二個參數能夠是 Array 的實例,也能夠是arguments 對象。
function add(a, b) {
  console.log(a + b);
}

function sum1(a, b) {
  add.apply(window, [a, b]);
}

function sum2(a, b) {
  add.apply(this, arguments)
}

sum1(1, 2);    // 3
sum2(3, 5);    // 8

 

call()方法與 apply()方法的做用相同,它們的區別僅在於接收參數的方式不一樣。對於 call()方法而言,第一個參數是 this 值沒有變化,變化的是其他參數都直接傳遞給函數。換句話說,在使用call()方法時,傳遞給函數的參數必須逐個列舉出來
var color = 'red';
var obj = {
  color: 'blue'
};

function getColor() {
  console.log(this.color)
}

getColor.call(this)    // red
getColor.call(obj)     // blue

 

2、預解析機制數組

第一步:js運行時,會找全部的var和function關鍵字
  --、把全部var關鍵字聲明的變量提高到各自做用域的頂部並賦初始值爲undefined,簡單說就是 「聲明提早,賦值留在原地」
  --、函數聲明提高
 
第二步:從上至下逐行解析代碼
 
var color = 'red';
var size = 31;

function fn() {
  console.log(color);
  var color = 'blue';
  var size = 29;
}

fn();    // undefined

 

// 第一步:在全局做用域內查找全部使用var和function關鍵字聲明的變量,把 color、size、fn 提高到全局做用域頂端併爲其賦初始值;同理,在fn函數做用域內執行此操做
// 第二步:從上至下依次執行代碼,調用fn函數時,按序執行代碼,函數做用域內的輸出語句中color此時僅賦初始值undefined
注意:
(1)、若是函數是經過 「函數聲明」 的方式定義的,遇到與函數名相同的變量時,不論函數與變量的位置順序如何,預解析時函數聲明會覆蓋掉var聲明的變量
console.log(fn)    // ƒ fn() {}

function fn() {}

var fn = 32

 

(2)、若是函數是經過 「函數表達式」 的方式定義的,遇到與函數名相同的變量時,會視同兩個var聲明的變量,後者會覆蓋前者
console.log(fn);         // undefined
var fn = function() {};
var fn = 32;
console.log(fn)          // 32

 

(3)、兩個經過 「函數聲明」 的方式定義的同名函數,後者會覆蓋前者
console.log(fn);     // ƒ fn() {console.log('你好 世界')}

function fn() {console.log('hello world')}

function fn() {console.log('你好 世界')}

 

預解析練習一:瀏覽器

var fn = 32

function fn() {
  alert('eeee')
}

console.log(fn)          // 32
fn()                     // Uncaught TypeError: fn is not a function
console.log(typeof fn)   // number

// 按照上面的預解析規則,預解析第一步時,fn會被賦值爲 function fn() {alert('eeee')};第二步從上到下逐步執行時,因爲函數fn聲明提早,優於var聲明的fn執行了,
// 因此fn會被覆蓋爲一個Number類型的基本數據類型變量,而不是一個函數,其值爲32

 

預解析練習二:app

console.log(a);        // function a() {console.log(4);}

var a = 1;

console.log(a);       // 1

function a() {
  console.log(2);
}

console.log(a);       // 1

var a = 3;

console.log(a);       // 3

function a() {
  console.log(4);
}

console.log(a);       // 3

a();                  // 報錯:不是一個函數
 
預解析步驟:
(1)、找出當前相同做用域下全部使用var和function關鍵字聲明的變量,因爲全部變量都是同名變量,按照規則,權值最高的是最後一個聲明的同名的function,因此第一行輸出 function a() {console.log(4);}
(2)、從上至下逐步執行代碼,在第二行爲變量a 賦值爲1,所以輸出了一個1
(3)、執行到第一個函數a,因爲沒有調用,直接跳過不會輸出裏面的2,執行到下一行輸出1
(4)、繼續執行,爲a從新賦值爲3,所以輸出了一個3
(5)、執行到第二個函數a,仍是沒有調用,直接跳過不會輸出裏面的4,執行到下一行輸出3
(6)、最後一行調用函數a,但因爲預解析時率先把a賦值爲一個函數代碼段,後面依次爲a賦值爲1和3,所以,a是一個Number類型的基本變量,而不是一個函數了

 

預解析練習三:
var a = 1;
function fn(a) {
  console.log(a);     // 999
  a = 2;
  console.log(a)      // 2
}

fn(999);
console.log(a);       // 1
 
預解析步驟:
(1)、全局做用域內,爲a賦值爲undefined,把函數fn提高到最前面;fn函數做用域內,函數參數在預解析時也視同局部變量,爲其賦初始值 undefined
(2)、執行fn函數,傳入實參999,爲局部變量a賦值爲999並輸出;從新爲a賦值爲2,輸出2
(3)、因爲全局做用域下的a被賦值爲1,而函數做用域內部的a是訪問不到的,所以直接輸出1
預解析練習四:
var a = 1;
function fn() {
  console.log(a);
  var a = 2;
}

fn();            // undefined
console.log(a);  // 1
var a = 1;
function fn() {
  console.log(a);
  a = 2;
}

fn();            // 1
console.log(a);  // 2
 
對比兩段代碼,惟一的區別就是fn函數內的變量a的做用域問題,前者屬於函數做用域,後者屬於全局做用域,因此致使輸出結果徹底不一樣
相關文章
相關標籤/搜索