ES6函數的擴展

1 函數參數的默認值
ES6之前,不能直接爲函數的參數指定默認值,方法如下:

function log(x, y) {
  y = y || 'World';
  console.log(x, y);
}

log('Hello')            // Hello World
log('Hello', 'China')   // Hello China
log('Hello', '')        // Hello World

ES6允許爲函數的參數設置默認值,即直接寫在參數定義的後面

function log(x, y = 'World') {
  console.log(x, y);
}

log('Hello')            // Hello World
log('Hello', 'China')   // Hello China
log('Hello', '')        // Hello

參數變量是默認聲明的,所以不能用let或const再次聲明

function foo(x = 5) {
    let x = 1;    // error 
}

注意: 參數默認值不是傳值的,而是每次都重新計算默認值表達式的值。

let x = 99;
function foo(p = x + 1) {
  console.log(p);
}

foo() // 100

x = 100;
foo() // 101

與結構賦值默認值結合使用
參數默認值可以與解構複製的默認值,結合起來使用。

function foo({x, y = 5}) {
  console.log(x, y);
}

foo({})             // undefined 5
foo({x: 1})         // 1 5
foo({x: 1, y: 2})   // 1 2
foo()               // TypeError: Cannot read property 'x' of undefined

以上代碼只使用了對象的解構賦值默認值,沒有使用函數參數的默認值。只有當函數foo的參數是一個對象時,變量x和y纔會通過解構賦值生成。如果函數foo調用時沒提供參數,變量x和y就不會生成,從而報錯。通過提供函數參數的默認值,就可以避免這種情況。

function foo({x, y = 5} = {}) {
  console.log(x, y);
}

foo() // undefined 5

解構賦值的例子:

function fetch(url, { body = '', method = 'GET', headers = {} }) {
  console.log(method);
}

fetch('http://example.com', {})
// "GET"

fetch('http://example.com')
// error

雙重默認值:

function fetch(url, { body = '', method = 'GET', headers = {} } = {}) {
  console.log(method);
}

fetch('http://example.com')
// "GET"

上面的代碼中,函數fetch沒有第二個參數時,函數參數的默認值就會生效,然後纔是解構賦值的默認值生效,變量method纔會取到默認值GET。
函數的length屬性
指定了默認值之後,函數的length屬性,將返回沒有指定默認值的參數個數。也就是說,指定了默認值後,length屬性將失真。

(function (a) {}).length            // 1
(function (a = 5) {}).length        // 0
(function (a, b, c = 5) {}).length  // 2

作用域
一旦設置了參數的默認值,函數進行聲明初始化時,參數會形成一個單獨的作用於。等到初始化結束,這個作用域就會消失。這種語法行爲,在不設置參數默認值時,是不會出現的。

var x = 1;

function f(x, y = x) {
  console.log(y);
}

f(2)        // 2

上面代碼中,參數y的默認值等於變量x。調用函數f時 ,參數形成一個單獨的作用域。在這個作用域裏面,默認值變量x指向第一個參數x,而不是全局變量x,所以輸出是2。

let x = 1;

function f(y = x) {
  let x = 2;
  console.log(y);
}

f() // 1

上面代碼中,函數f調用時,參數y=x形成一個單獨的作用於。這個作用域裏面,變量x本身沒有定義,所以指向外層的全局變量x。函數調用時,函數體內部的局部變量x影響不到默認值變量x。
應用
利用參數默認值,可以指定某一個參數不得省略,如果省略就拋出一個錯誤。

function throwIfMissing() {
  throw new Error('Missing parameter');
}

function foo(mustBeProvided = throwIfMissing()) {
  return mustBeProvided;
}

foo()
// Error: Missing parameter

上面代碼,如果調用的時候沒有參數,就會調用默認值throwIfMissing函數,從而拋出一個錯誤。
從上面代碼還可以看到,參數mustBeprovided的默認值等於throwIfMissing函數的運行結果,這表明參數的默認值不是在定義時執行,而是在運行時執行。如果參數已經賦值,默認值中的函數就不會運行。
2 rest參數
ES6引入rest參數(形式爲…變量名),用於獲取函數的多餘參數,這樣就不需要使用arguments對象。rest參數搭配的變量是一個數組,該變量將多餘的參數放入數組中

function add(...values) {
  let sum = 0;
  for (var val of values) {
    sum += val;
  }
  return sum;
}

add(2, 5, 3)                // 10

上面代碼的add函數是一個求和函數,利用rest參數,可以向該函數傳入任意數目的參數。

// error
function f(a, ...b, c) {
  // ...
}

注意: rest參數之後不能再有其他參數(即只能是最後一個參數),否則會報錯。
3 name屬性
函數的name屬性,返回該函數的函數。

function foo() {}
foo.name // "foo"

注意: 如果將一個匿名函數賦值給一個變量,ES5的name屬性,會返回空字符串,而ES6的name屬性會返回實際的函數名。

var f = function () {};

// ES5
f.name // ""

// ES6 當然現在的瀏覽器都已經支持返回"f"
f.name // "f"

將一個具名函數賦值給一個變量,則ES5和ES6的name屬性都返回這個具名函數原本的名字。

const bar = function baz() {};

// ES5
bar.name // "baz"

// ES6
bar.name // "baz"

箭頭函數
ES6允許使用”箭頭”(=>)定義函數

var f = v => v;

上面的箭頭函數等於:

var f = function(v) {
    return v;
};

如果箭頭函數不需要參數或需要多個參數,就使用一個圓括號代表參數部分。

var f = () => 5;
// 等同於
var f = function() {return 5};

var sum = (num1, num2) => num1+num2;
// 等同於
var sum = function(num1, num2) {
    return num1+num2;
}

箭頭函數可以與變量解構結合使用

const full = ({first, last} => first + ' ' + last);

// 等同於
function full(person) {
    return person.first + ' ' + person.last;
}

一個用處: 箭頭函數簡化回調函數

[1, 2, 3].map(function (x) {
    return x * x;
})

// 箭頭函數寫法
[1, 2, 3].map(x => x * x);

尾調用
尾調用是函數式編程的一個重要概念,本身非常簡單,就是指某個函數的最後一步是調用另一個函數。

function f(x) {
    return g(x)
}

函數f的最後一部是調用函數g,這就叫尾調用。
以下三種情況,都不屬於尾調用:

// 情況一
function f(x){
  let y = g(x);
  return y;
}

// 情況二
function f(x){
  return g(x) + 1;
}

// 情況三
function f(x){
  g(x);
}

情況一調用函數g之後,還有賦值操作,所以不屬於尾調用。情況而也屬於調用後還有操作,即使寫在一行內。情況三等同於下面的代碼。

function f(x) {
    g(x);
    return undefined;
}

尾調用不一定出現在函數尾部,只要是最後一步操作即可。

function f(x) {
    if (x>0) {
        return m(x);
    }
    return n(x);
}

上述,函數m和n都屬於尾調用,因爲它們都是函數f的最後一步操作。

尾調用優化
函數調用會在內存形成一個”調用記錄”,又稱爲”調用幀”,保存調用位置和內部變量等信息。如果在函數A的內部調用函數B,那麼在A的調用幀上方,會形成一個B的調用幀。等到B運行結束,將結果返回到A,B的調用 幀纔會消失。如果函數B內部還調用函數C,那就還有一個C的調用幀,以此類推。所有的調用幀,就形成一個”調用棧”

function f() {
    let m = 1;
    let n = 2;
    return g(m + n);
}
f();

// 等同於
function f() {
    return g(3);
}
f();

// 等同於
g(3);

上面代碼中,如果 函數g不是尾調用,函數f就需要保存內部變量m和n的值,g的調用信息等,但是由於調用g之後,函數f就結束了,所以執行到最後一步,完全可以刪除f(x)的調用幀,只保留g(3)的調用幀。

這就叫做”尾調用優化”,即只保留內層函數的調用幀。如果所有函數 都是尾調用,那麼完全可以做到每次執行時,調用幀只有一項,這將大大節省內存。這就是 “尾調用優化”的意義。

尾遞歸
函數調用自身,稱爲遞歸。如果尾調用自身,就成爲尾遞歸。
遞歸非常耗費內存,因爲需要同時保存成千上百個調用幀,很容易發生”棧溢出”錯誤。但是對於尾遞歸來說,由於只存在一個調用幀,所以永遠不會發生”棧溢出”錯誤。

function factorial(n) {
  if (n === 1) return 1;
  return n * factorial(n - 1);
}

factorial(5) // 120

階乘函數,計算n的階乘,最多需要保存n個調用記錄,複雜度O(n);

如果修改成尾調用,只保留一個調用記錄,複雜度O(1);

function factorial(n, total) {
  if (n === 1) return total;
  return factorial(n - 1, n * total);
}

factorial(5, 1) // 120

ES6明確規定,所有ECMAScript的實現,都必須部署”尾調用優化”,這就是說,ES6中只要使用尾遞歸,就不會發生棧溢出,相對節省內存。

本博客內容摘抄自
阮一峯老師寫的ECMAScript6入門一書

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章