ES6躬行記(14)——函式
在前面的章節中,已陸陸續續介紹了ES6為改良函式而引入的幾個新特性,本章將會繼續講解ES6對函式的其餘改進,包括預設引數、元屬性、塊級函式和箭頭函式等。
一、預設引數
在ES5時代,只能在函式體中定義引數的預設值,而自從ES6引入了預設引數(Default Parameter)後,就能讓引數在宣告時帶上它的預設值,如下程式碼所示,func2()函式中的引數預設值在可讀性和簡潔性方面更為優秀。
function func1(name) { name = name || "strick";//ES5的引數預設值 } function func2(name = "strick") {//ES6的引數預設值 }
1)undefined
只有當不給引數傳值或傳入undefined時,才會使用它的預設值。即使傳入和undefined一樣的假值(例如false、null等),也得不到它的預設值,如下所示。
function func(name = "strick") { return name; } func(undefined);//"strick" func(false);//false func(null);//null
2)位置
預設引數既可以位於普通引數之前,也可以位於其之後。例如下面的兩個函式,都包含兩個引數,其中一個帶有預設值,依次執行,都能得到預期的結果。
function func1(name = "strick", age) { return name; } function func2(name, age = 28) { return age; } func1(undefined);//"strick" func2("strick");//28
3)預設值
引數的預設值既可以是簡單的字面量,也可以是複雜的表示式。在每次呼叫函式時,不僅引數會被重新初始化,預設值如果是表示式的話,還會將其重新計算一次。
function expression1(name, full = "pw" + name) { return full; } expression1("strick");//"pwstrick" expression1("freedom");//"pwfreedom"
在上面的程式碼中,呼叫了兩次expression1()函式,返回的結果互不影響。並且full引數的預設值引用了前面的name引數,這是一種有效的語法,但反之就會報錯,如下所示。
function expression2(name = full, full) { return name; } expression2(undefined, "strick");//丟擲未定義的引用錯誤
4)限制
第一條限制是在包含預設值的引數序列中,不允許出現同名引數。無論同名的是有預設值,亦或是無預設值,都是不允許的,如下所示。
function restrict1(name = "strick", name) { } function restrict1(name = "strick", age, age) { }
第二條限制是不能在函式體中為預設引數用let或const重新宣告,如下程式碼所示,會丟擲重複宣告的語法錯誤。
function restrict2(name = "strick") { let name = "freedom"; }
因為預設引數相當於是用let宣告的變數,所以是不允許重複宣告的。上面程式碼中的restrict2()函式,它的name引數的初始化類似於下面這樣。
let name = "strick";
引數序列中只要包含了預設引數,那麼其它普通引數也會用let宣告。知道這一點後,就能很容易的解釋上一節第二個示例,在呼叫expression2()函式時會丟擲未定義的錯誤原因。函式中的兩個引數的初始化相當於下面這樣。
let name = full, full;
在 第一篇 中曾提到用let宣告的變數,在宣告之前都會被放到臨時死區中,而在此時訪問這些變數就會觸發執行時錯誤。
5)三個作用域
根據ES6規範的9.1.2小節可知,當引數序列中包含預設引數時,將會出現三個作用域:引數作用域、函式外層作用域和函式體內作用域。關於這三個作用域需要注意兩點:
(1)函式體內可以修改引數的值,但不能為其重新宣告。
(2)引數作用域可以訪問外層作用域中的變數,但不能訪問函式體內的變數。
第一點很好理解,已在上文中做過解釋。關於第二點,可先檢視下面的兩個函式。
let full = "freedom"; function scope1(name = full) { return name; } scope1(); function scope2(name = en) { let en = "justify"; return name; } scope2();
呼叫scope1()函式得到的返回值是“freedom”,而呼叫scope2()函式非但得不到結果,還會丟擲en未定義的錯誤。接下來改造scope1()函式,把full變數改成name變數,如下所示。
let name = "freedom"; function scope1(name = name) { return name; }
此時再次呼叫scope1()函式,得到的卻是name未定義的錯誤。雖然在外層作用域中包含名為name的變數,但是引數擁有自己的作用域,會先從當前作用域中查詢變數,此時的name正處在臨時死區中,因此在訪問它時會報錯。
除了以上所列的特性之外,在之前的 第三篇 的引數解構中,還介紹瞭解構預設值和引數預設值結合使用時的注意點。
二、函式屬性
1)name
通過函式的name屬性可得到它宣告時所用的名稱。ES6規定此屬性既不可寫,也不可列舉,只允許配置。在不同場景中,它的返回值會不同,具體如下所列,每一條規則後面都給出了相應的示例。
(1)利用Function構造器建立的函式,它的名稱是“anonymous”。
var func = new Function("a", "b", "return a+b;"); func.name;//"anonymous"
(2)如果是用匿名函式表示式建立的函式,那麼它的名稱就是變數名;如果改用命名函式表示式建立,那麼它的名稱就是等號右側的函式名稱。
var expression1 = function() { }; expression1.name;//"expression1" var expression2 = function named() { }; expression2.name;//"named"
(3)當用bind()方法繫結一個函式時,它的名稱就會加“bound”字首。
function age() { } age.bind(this).name;//"bound age"
(4)訪問器屬性包含寫入方法和讀取方法,它們的名稱會分別加“set”和“get”字首。注意,需要呼叫Object.getOwnPropertyDescriptor()才能引用這兩個方法。
var obj = { get age() { }, set age(value) { } }; var descriptor = Object.getOwnPropertyDescriptor(obj, "age"); descriptor.get.name;//"get age" descriptor.set.name;//"set age"
(5)如果物件的方法是用Symbol命名的,那麼這個Symbol的描述就是它的名稱。
var sym = Symbol("age"), obj = { [sym]: function() {} }; obj[sym].name;//"[age]"
2)length
函式的length屬性可返回形參個數(即宣告時的引數),但它的值會受剩餘引數(已在 第二篇 中做過介紹)和預設引數的影響,如下程式碼所示。
(function rest(name, ...args){ }).length;//1 (function rest(name, age = 28){ }).length;//1 (function rest(name, age = 28, school){ }).length;//1
根據上面的程式碼可知,形參個數的統計會忽略剩餘引數,並且止於預設引數。
三、塊級函式
ES6允許塊級函式(Block-Level Function)的宣告,即在塊級作用域中宣告函式,而在ES5中如此操作的話,將會丟擲語法錯誤的異常。
1)嚴格模式
在嚴格模式中,塊級函式的宣告可提升至當前程式碼塊的頂部,在程式碼塊之外是不可見的,如下程式碼所示。
"use strict"; (function() { func("strick"); //丟擲未定義的引用錯誤 if(true) { func("freedom"); //"freedom" function func(name) { return name; } { func("jane");//"jane" } } func("justify");//丟擲未定義的引用錯誤 })();
只有在func()函式所處的程式碼塊或與之相鄰的程式碼塊中,才能被正確呼叫。
2)普通模式
在普通模式(即非嚴格模式)中,只有當塊級函式所在的程式碼塊被成功執行後,它的宣告才能被提升至當前指令碼檔案或函式體的頂部,如下程式碼所示。
(function() { func("strick");//丟擲未定義的引用錯誤 if(true) { func("freedom");//"freedom" function func(name) { return name; } { func("jane");//"jane" } } func("justify");//"justify" })();
在程式碼塊之外呼叫了兩次func()函式,由於第一次呼叫時,func()函式所處的程式碼塊還未被執行(即還未宣告),因此會丟擲未定義的引用錯誤。
四、元屬性
元屬性(Meta Property)就是非物件的屬性,能夠以屬性訪問的形式讀取特殊的元資訊。new.target是由ES6引入的一個元屬性,可檢測一個函式是否與new運算子組合使用,並且只能存在於函式體內。
在JavaScript中,new是一個關鍵字,而不是一個物件。但當函式作為建構函式被呼叫時,new.target能夠指向新建立的目標物件;而當函式作為普通函式被呼叫時,new.target的值為undefined,如下所示。
function func1() { typeof new.target;//"function" } new func1(); function func2() { new.target === undefined;//true } func2();
把func1()作為建構函式使用,在其函式體中利用typeof運算子檢測出new.target是一個函式物件;而在func2()函式中,讓new.target和undefined進行了全等比較,得到的結果為true。