深入理解ES6筆記(三)函數

主要知識點有:函數參數默認值、剩餘參數、擴展運算符、new.target屬性、塊級函數、箭頭函數以及尾調用優化
圖片描述

《深入理解ES6》筆記 目錄

函數的默認參數

在ES5中,我們給函數傳參數,然後在函數體內設置默認值,如下面這種方式。

function a(num, callback) {
  num = num || 6
  callback = callback || function (data) {console.log('ES5: ', data)}
  callback(num * num)
}
a() //ES5: 36,不傳參輸出默認值

//你還可以這樣使用callback
a(10, function(data) {
  console.log(data * 10) // 1000, 傳參輸出新數值
})

弊端:此處的 num 的有效值實際上有可能是 0 ,但因爲 0 是假值,就會導致 num 的值在這種情況下會被替換爲 6;
可以用 typeof 來檢測參數的類型:

function a(num, callback) {
  num = (typeof num!== "undefined") ? num: 6;
  callback = (typeof callback !== "undefined") ? callback : function (data) {console.log('ES5: ', data)};
  callback(num * num)
}

雖然這種方法更安全,但依然爲實現一個基本需求而書寫了過多的代碼。它代表了一種公共
模式,而流行的 JS 庫中都充斥着類似的模式。

ES6 中的參數默認值

function a(num = 6, callback = function (data) {console.log('ES6: ', data)}) {
  callback(num * num)
}

a() //ES6: 36, 不傳參輸出默認值

a(10, function(data) {
  console.log(data * 10) // 1000,傳參輸出新數值
})

使用ES6的默認值寫法可以讓函數體內部的代碼更加簡潔優雅

參數默認值如何影響 arguments 對象

  • ES5 的非嚴格模式下
function mixArgs(first, second) {
    console.log(first === arguments[0]);
    console.log(second === arguments[1]);
    first = "c";
    second = "d";
    console.log(first === arguments[0]);
    console.log(second === arguments[1]);
}
mixArgs("a", "b");
//輸出
true
true
true
true
  • ES5 的嚴格模式下
function mixArgs(first, second) {
    "use strict";
    console.log(first === arguments[0]);
    console.log(second === arguments[1]);
    first = "c";
    second = "d"
    console.log(first === arguments[0]);
    console.log(second === arguments[1]);
}
mixArgs("a", "b");
//輸出
true
true
false
false
  • ES6

arguments 對象的表現總是會與 ES5 的嚴格模式一致,無論此時函數是否明確運行在嚴格模式下。

// 非嚴格模式
function mixArgs(first, second = "b") {
    console.log(arguments.length);
    console.log(first === arguments[0]);
    console.log(second === arguments[1]);
    first = "c";
    second = "d"
    console.log(first === arguments[0]);
    console.log(second === arguments[1]);
}
mixArgs("a");
//輸出
1
true
false
false
false

此時arguments.length =1 ,因爲只給 mixArgs() 傳遞了一個參數。這也意味着arguments[1] 的值是 undefined ,符合將單個參數傳遞給函數時的預期;這同時意味着first 與 arguments[0] 是相等的。改變 first 和 second 的值不會對 arguments 對象造成影響,無論是否在嚴格模式下,所以你可以始終依據 arguments 對象來反映初始調用狀態。

默認參數表達式

參數不僅可以設置默認值爲字符串,數字,數組或者對象,還可以是一個函數。

function add() {
  return 10
}
function a(num = add()){
  console.log(num)
}
a() // 10

默認參數的臨時死區

第一章我們提到了let和const什麼變量的臨時死區(TDZ),默認參數既然是參數,那麼也同樣有臨時死區,函數的作用域是獨立的,a函數不能共享b函數的作用域參數。

//這是個默認參數臨時死區的例子,當初始化a時,b還沒有聲明,所以第一個參數對b來說就是臨時死區。
function add(a = b, b){
  console.log(a + b)
}
add(undefined, 2) // b is not define

處理無命名參數

上面說的參數都是命名參數,而無命名參數也是函數傳參時經常用到的。當傳入的參數是一個對象,不是一個具體的參數名,則是無命名參數。

function add(object){
  console.log(object.a + object.b)
}
let obj = {
  a: 1,
  b: 2
}
add(obj) // 3

不定參數

使用...(展開運算符)的參數就是不定參數,它表示一個數組。

function add(...arr){
  console.log(a + b)
}
let a = 1,b = 2
add(a, b) // 3

不定參數的使用限制:

  • 函數只能有一個剩餘參數,並且它必須被放在最後
//錯誤的寫法1
function add(...arr, c){
  console.log(a + b)
}
let a = 1,b = 2,c = 3
add(a, b, c)
  • 剩餘參數不能在對象字面量的 setter 屬性中使用
//錯誤的寫法2
let obj = {
  set add(...arr) {
  
  }
}

剩餘參數如何影響 arguments 對象
arguments 對象在函數被調用時反映了傳入的參數,與剩餘參數能協同工作,就像如下程序所演示的:

function checkArgs(...args) {
    console.log(args.length);
    console.log(arguments.length);
    console.log(args[0], arguments[0]);
    console.log(args[1], arguments[1]);
}
checkArgs("a", "b");
//輸出
2
2
a a
b b

arguments 對象總能正確反映被傳入函數的參數,而無視剩餘參數的使用。

ES6中的構造函數Function新增了支持默認參數和不定參數。

擴展運算符

考慮一下Math.max()方法,它接受任意數量的參數,並會返回其中的最大值。

//兩個值進行比較
let value1 = 25,
value2 = 50;
console.log(Math.max(value1, value2)); // 50
//一個數組中找到最大值(es5)
let values = [25, 50, 75, 100]
console.log(Math.max.apply(Math, values)); 
//es6
let values = [25, 50, 75, 100]
// 等價於 console.log(Math.max(25, 50, 75, 100));
console.log(Math.max(...values)); // 100

擴展運算符傳遞參數

//假設你想讓  Math.max()  返回的最小值爲 0 (以防數組中混入了負值),你可以將參數 0 單獨傳入,並繼續爲其他參數使用擴展運算符
let values = [-25, -50, -75, -100]
console.log(Math.max(...values, 0)); // 0

ES6 的名稱屬性

ES6 給所有函數添加了 name 屬性。

選擇合適的名稱

//函數聲明
function doSomething() {
// ...
}
//匿名函數表達式
var doAnotherThing = function() {
// ...
};
console.log(doSomething.name); // "doSomething"
console.log(doAnotherThing.name); // "doAnotherThing"

名稱屬性的特殊情況

//doSomethingElse的優先級高於doSomething 
var doSomething = function doSomethingElse() {
// ...
};
//person.firstName  實際是個 getter 函數,因此它的名稱是  "get firstName"
var person = {
    get firstName() {
        return "Nicholas"
    },
    sayName: function() {
        console.log(this.name);
    }
}
console.log(doSomething.name); // "doSomethingElse"
console.log(person.sayName.name); // "sayName"
var descriptor = Object.getOwnPropertyDescriptor(person, "firstName");
console.log(descriptor.get.name); // "get firstName"

另外兩個特殊情況

  • 使用 bind() 創建的函數會在名稱屬性值之前帶有"bound"前綴
  • 使用 Function 構造器創建的函數,其名稱屬性則會有 "anonymous" 前綴
var doSomething = function() {
// ...
};
console.log(doSomething.bind().name); // "bound doSomething"
console.log((new Function()).name); // "anonymous"

明確函數的雙重用途

JS 爲函數提供了兩個不同的內部方法: [[Call]] 與 [[Construct]] 。當函數未使用 new進行調用時, [[call]] 方法會被執行,運行的是代碼中顯示的函數體。而當函數使用 new進行調用時, [[Construct]] 方法則會被執行,負責創建一個被稱爲新目標的新的對象,並
且使用該新目標作爲 this 去執行函數體。擁有 [[Construct]] 方法的函數被稱爲構造器。

在 ES5 中判斷函數如何被調用

使用instanceof

function Person(name) {
    if (this instanceof Person) {
        this.name = name; // 使用 new
    } else {
        throw new Error("You must use new with Person.")
    }
}
var person = new Person("Nicholas");
var notAPerson = Person("Nicholas"); // 拋出錯誤

但這種情況下並不可靠:

function Person(name) {
    if (this instanceof Person) {
        this.name = name; // 使用 new
    } else {
        throw new Error("You must use new with Person.")
    }
}
var person = new Person("Nicholas");
var notAPerson = Person.call(person, "Michael"); // 奏效了!

new.target 元屬性

通過檢查 new.target 是否被定義,這個新的元屬性就讓你能安全地判斷函數是否被使用new進行了調用。

function Person(name) {
    if (typeof new.target !== "undefined") {
        this.name = name; // 使用 new
    } else {
        throw new Error("You must use new with Person.")
    }
}
var person = new Person("Nicholas");
var notAPerson = Person.call(person, "Michael"); // 出錯!

也可以檢查 new.target 是否被使用特定構造器進行了調用,例如以下代碼:

function Person(name) {
    if (new.target === Person) {
        this.name = name; // 使用 new
    } else {
        throw new Error("You must use new with Person.")
    }
}
function AnotherPerson(name) {
    Person.call(this, name);
}
var person = new Person("Nicholas");
var anotherPerson = new AnotherPerson("Nicholas"); // 出錯!
警告:在函數之外使用 new.target 會有語法錯誤。

塊級函數

嚴格模式的塊級函數

"use strict";
if (true) {
    // 在 ES5 會拋出語法錯誤, ES6 則不會
    function doSomething() {
    // ...
    }
}

塊級函數會被提升到定義所在的代碼塊的頂部:

"use strict";
if (true) {
    console.log(typeof doSomething); // "function"
    function doSomething() {
    // ...
}
doSomething();
}
console.log(typeof doSomething); // "undefined"

let 函數表達式:

"use strict";
if (true) {
    console.log(typeof doSomething); // 拋出錯誤
    let doSomething = function () {
        // ...
    }
doSomething();
}
console.log(typeof doSomething);

非嚴格模式的塊級函數

ES6 在非嚴格模式下同樣允許使用塊級函數,但行爲有細微不同。塊級函數的作用域會被提升到所在函數或全局環境的頂部,而不是代碼塊的頂部。

// ES6 behavior
if (true) {
    console.log(typeof doSomething); // "function"
    function doSomething() {
    // ...
    }
doSomething();
}
console.log(typeof doSomething); // "function"

箭頭函數

箭頭函數與傳統的 JS 函數區別:

  • 沒有 this 、 super 、 arguments ,也沒有 new.target 綁定
  • 不能被使用 new 調用
  • 沒有原型: 既然不能對箭頭函數使用 new ,那麼它也不需要原型,也就是沒有prototype 屬性。
  • 不能更改 this : this 的值在函數內部不能被修改,在函數的整個生命週期內其值會保持不變
  • 沒有 arguments 對象
  • 不允許重複的具名參數

箭頭函數語法

  • 無參數
var getName = () => "Nicholas";
// 有效等價於:
var getName = function() {
    return "Nicholas";
};
  • 單個參數
var reflect = value => value;
// 有效等價於:
var reflect = function(value) {
    return value;
};
  • 多個參數
var sum = (num1, num2) => num1 + num2;
// 有效等價於:
var sum = function(num1, num2) {
    return num1 + num2;
};
  • 多個函數語句體
var sum = (num1, num2) => {
    return num1 + num2;
};
// 有效等價於:
var sum = function(num1, num2) {
    return num1 + num2;
};

//將對象字面量包裹在括號內,標示了括號內是一個字面量而不是函數體。
var getTempItem = id => ({ id: id, name: "Temp" });
// 有效等價於:
var getTempItem = function(id) {
    return {
        id: id,
        name: "Temp"
    };
};

創建立即調用函數表達式

  • 傳統函數
let person = function(name) {
    return {
        getName: function() {
            return name;
        }
    };
}("Nicholas");
console.log(person.getName()); // "Nicholas"
  • 箭頭函數
let person = ((name) => {
    return {
    getName: function() {
        return name;
    }
};
})("Nicholas");
console.log(person.getName()); // "Nicholas"
譯註:使用傳統函數時, (function(){/函數體/})(); 與 (function(){/函數體/}());
這兩種方式都是可行的。
但若使用箭頭函數,則只有下面的寫法是有效的: (() => {/函數體/})();

尾調用優化

尾調用是指在函數return的時候調用一個新的函數,由於尾調用的實現需要存儲到內存中,在一個循環體中,如果存在函數的尾調用,你的內存可能爆滿或溢出。

ES6中,引擎會幫你做好尾調用的優化工作,你不需要自己優化,但需要滿足下面3個要求:

1、函數不是閉包

2、尾調用是函數最後一條語句

3、尾調用結果作爲函數返回

一個滿足以上要求的函數如下所示:

"use strict";   
function a() {
  return b();
}

下面的都是不滿足的寫法:

//沒有return不優化
"use strict";
function a() {
  b();
}

//不是直接返回函數不優化
"use strict";
function a() {
  return 1 + b();
}

//尾調用是函數不是最後一條語句不優化
"use strict";
function a() {
  const s = b();
  return s
}

//閉包不優化
"use strict";
function a() {
  const num = 1
  function b() {
    return num
  }
  return b
}

尾調用實際用途——遞歸函數優化

在ES5時代,我們不推薦使用遞歸,因爲遞歸會影響性能。
但是有了尾調用優化之後,遞歸函數的性能有了提升。

//新型尾優化寫法
"use strict";  
function a(n, p = 1) {
  if(n <= 1) {
    return 1 * p
  }
  let s = n * p
  return a(n - 1, s)
}
//求 1 x 2 x 3的階乘
let sum = a(3)
console.log(sum) // 6
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章