ES6學習筆記之Function

星塵FE發表於2019-01-29

函式的擴充套件

rest 引數

ES6 引入 rest 引數(形式為…變數名),用於獲取函式的多餘引數,這樣就不需要使用arguments物件了。rest 引數搭配的變數是一個陣列,該變數將多餘的引數放入陣列中。

const foo = (...values) = {
  console.log(values)
}
foo(1,2,3)  // [1,2,3]
複製程式碼

arguments 物件是使用function宣告函式時自動生成的物件, 包含了函式的引數,但結構複雜。在箭頭函式中被rest代替,不可使用,否則報錯。

// arguments變數的寫法
function f1() {
  console.log(arguments)
}
const f11 = () => {
  console.log(arguments)
}

// rest引數的寫法
const f2 = (...numbers) => {
  console.log(numbers)
};
複製程式碼

注意:rest 引數只能是最後一個引數,否則會報錯。

const foo = (a, ...rest, b) => {}
//  Uncaught SyntaxError: Rest parameter must be last formal parameter
複製程式碼

函式引數的預設值

特點

ES6 允許為函式的引數設定預設值,即直接寫在引數定義的後面。

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

log(`Hello`) // Hello World
log(`Hello`, undefined) // Hello World

log(`Hello`, ``) // Hello
log(`Hello`, null) // Hello null
log(`Hello`, `China`) // Hello China

複製程式碼

這種寫法有兩個好處:

  1. 就算不用閱讀文件也可以直觀的看出哪些引數可以忽略;
  2. 有利於將來的程式碼優化,即使未來的版本在對外介面中,徹底拿掉這個引數,也不會導致以前的程式碼無法執行。
  • 只有在這個引數沒有設定或者設定為undefined的時候,預設值才會生效,跟解構賦值很相近。所以定義預設值的引數,最好是函式的尾引數。不然會出現一下情況:
function f(x = 1, y) {
  return [x, y];
}

f() // [1, undefined]
f(2) // [2, undefined])
f(, 1) // 報錯
f(undefined, 1) // [1, 1]
複製程式碼

另外,一個容易忽略的地方是,引數預設值不是傳值的,而是每次都重新計算預設值表示式的值。也就是說,引數預設值是惰性求值的。

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
複製程式碼

如果呼叫函式時沒有給引數就會報錯.
通過提供函式引數的預設值,就可以避免這種情況。

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

foo() // undefined 5
複製程式碼

要注意函式解構設定預設值的寫。
以下有兩種寫法:

// 寫法一
function m1({x = 0, y = 0} = {}) {
  return [x, y];
}

// 寫法二
function m2({x, y} = { x: 0, y: 0 }) {
  return [x, y];
}
複製程式碼

兩個都是給了預設值,但是是有區別的:

  • 寫法一函式引數的預設值是空物件,但是設定了物件解構賦值的預設值;
  • 寫法二函式引數的預設值是一個有具體屬性的物件,但是沒有設定物件解構賦值的預設值。
// 函式沒有引數的情況
m1() // [0, 0]
m2() // [0, 0]

// x 和 y 都有值的情況
m1({x: 3, y: 8}) // [3, 8]
m2({x: 3, y: 8}) // [3, 8]

// x 有值,y 無值的情況
m1({x: 3}) // [3, 0]
m2({x: 3}) // [3, undefined]

// x 和 y 都無值的情況
m1({}) // [0, 0];
m2({}) // [undefined, undefined]

m1({z: 3}) // [0, 0]
m2({z: 3}) // [undefined, undefined]

複製程式碼

作用域

一旦設定了引數的預設值,函式進行宣告初始化時,引數會形成一個單獨的作用域(context)。等到初始化結束,這個作用域就會消失。

let x = 1;
let y = 3;
function f1(x, y = x) {
  console.log(x, `x`)
  x = 3
  console.log(y, `y`);
}

f1(2) // 2
複製程式碼
  1. 函式頭內 xy 的值不受外界影響。函式體內的值也優先為頭部的值
  2. y 在函式頭就已經賦值,所以在執行時即便x 改變,也不會受影響。

嚴格模式

從 ES5 開始,函式內部可以設定為嚴格模式。

function foo(a, b) {
  `use strict`;
  // code
}
複製程式碼

然而 ES2016 做了一點修改,規定只要函式引數使用了預設值、解構賦值、或者擴充套件運算子(rest),那麼函式內部就不能顯式設定為嚴格模式,否則會報錯。

// 報錯
function foo(a, b = a) {
  `use strict`;
  // code
}

// 報錯
const foo = function ({a, b}) {
  `use strict`;
  // code
};

// 報錯
const foo = (...a) => {
  `use strict`;
  // code
};

const obj = {
  // 報錯
  foo({a, b}) {
    `use strict`;
    // code
  }
};
複製程式碼

兩種方法可以規避這種限制:
1.設定全域性性的嚴格模式

`use strict`;

function foo(a, b = a) {
  // code
}
複製程式碼

2.把函式包在一個無引數的立即執行函式裡面。

const foo = (function () {
  `use strict`;
  return function(value = 42) {
    return value;
  };
}());
複製程式碼

箭頭函式

基本用法

特點:

  1. 使用=>來定義函式
  2. 引數數量有且只有一個的時候,可以不使用括號
  3. 如果函式體內直接返回某個值的時候可以不用寫大括號和return,就代表返回了這個值。(返回物件時需要用圓括號括起來,否則會被識別成程式碼塊。)
const f1 = (x) => {
  return x + 1
}

const f2 = x => x + 1

const f3 = x => ({ x })

function f4 (x) {
  return  { x }
}
複製程式碼

巢狀的箭頭函式

箭頭函式內部,還可以再使用箭頭函式。下面是一個 ES5 語法的多重巢狀函式。

function insert(value) {
  return {into: function (array) {
    return {after: function (afterValue) {
      array.splice(array.indexOf(afterValue) + 1, 0, value);
      return array;
    }};
  }};
}

insert(2).into([1, 3]).after(1); //[1, 2, 3]
複製程式碼

使用箭頭函式改寫,明顯少了很多程式碼:

let insert = (value) => ({into: (array) => ({after: (afterValue) => {
  array.splice(array.indexOf(afterValue) + 1, 0, value);
  return array;
}})});

insert(2).into([1, 3]).after(1); //[1, 2, 3]
複製程式碼

箭頭函式有幾個使用注意點

  1. 函式體內的this物件,就是定義時所在的物件(他的外層物件),而不是使用時所在的物件。
  2. 不可以當作建構函式,因我 他沒有自己的this物件。
  3. 不可以使用arguments物件,該物件在函式體內不存在。如果要用,可以用 rest 引數代替。
  4. 不可以使用yield命令,因此箭頭函式不能用作 Generator 函式。
var a=11
function f1(){
  this.a=22;
  let b=function(){
    console.log(this.a);
  };
  b();
}
function f2(){
  this.a=22;
  let b=()=>{console.log(this.a)}
  b();
}
var x=new f1();   // 11
var y=new f2();   // 22
複製程式碼

this指向的固定化,並不是因為箭頭函式內部有繫結this的機制,實際原因是箭頭函式根本沒有自己的this,導致內部的this就是外層程式碼塊的this。正是因為它沒有this,所以也就不能用作建構函式。
所以上面程式碼相當於這樣:

function f2(){
  this.a=22;
  let _this = this
  let b=()=>{console.log(_this.a)}
  b();
}

複製程式碼

除了this,以下三個變數在箭頭函式之中也是不存在的,指向外層函式的對應變數:argumentssupernew.target

const foo () =>{
  console.log(arguments)
}
foo() // Uncaught ReferenceError: arguments is not defined


function foo () {
  return () => {
    console.log(arguments)
  }
}

foo()()
複製程式碼

另外,由於箭頭函式沒有自己的this,所以當然也就不能用call()、apply()、bind()這些方法去改變this的指向。

(function() {
  return [
    (function () { return this.x }).bind({ x: `inner` })()
  ];
}).call({ x: `outer` });
// [`inner`]

(function() {
  return [
    (() => this.x).bind({ x: `inner` })()
  ];
}).call({ x: `outer` });
// [`outer`]
複製程式碼

函式的 length 屬性 和 name 屬性

函式的length 屬性將返回該函式預期傳入的引數個數。

某個引數指定預設值以後,預期傳入的引數個數就不包括這個引數了。同理,rest 引數也不會計入length屬性。

如果 預設引數不是尾引數,那麼預設引數後面的引數也不計入 length

const f1 = (a,b) => {}
f1.length // 2

const f2 = (a, ...rest) => {}
f2.length // 1

const f3 = (a, b=1, c) => {}
f3.length // 1
複製程式碼

函式的name屬性,返回該函式的函式名。

function foo() {}
foo.name // "foo"
複製程式碼

1.如果將一個匿名函式賦值給一個變數.

var f = function () {};

// ES5
f.name // ""

// ES6
f.name // "f"
複製程式碼

2.如果將一個具名函式賦值給一個變數. 都返回函式原本的名字。

const bar = function baz() {};

// ES5
bar.name // "baz"

// ES6
bar.name // "baz"
複製程式碼

3.Function建構函式返回的函式例項,name屬性的值為anonymous

const foo = new Function
foo.name  //   "anonymous"
複製程式碼

4.bind返回的函式,name屬性值會加上bound字首。

function foo() {};
foo.bind({}).name // "bound foo"

(function(){}).bind({}).name // "bound "
複製程式碼

尾呼叫

簡介

尾呼叫(Tail Call)是函數語言程式設計的一個重要概念,是指某個函式的最後一步是呼叫另一個函式。

function f(x){
  return g(x);
}
複製程式碼

以下三種情況,都不屬於尾呼叫:

const g = (x) => {}

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

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

// 情況三
function f(x){
  g(x);
}
//  等同於
function f(x){
  g(x);
  return  undefined
}
複製程式碼

尾呼叫優化

呼叫幀:函式呼叫會在記憶體形成一個“呼叫記錄”,又稱“呼叫幀”(call frame),儲存呼叫位置和內部變數等資訊。

呼叫棧:
如果在函式A的內部呼叫函式B,那麼在A的呼叫幀上方,還會形成一個B的呼叫幀。
等到B執行結束,將結果返回到A,B的呼叫幀才會消失。
如果函式B內部還呼叫函式C,那就還有一個C的呼叫幀,以此類推。
所有的呼叫幀,就形成一個“呼叫棧”(call stack)。

尾呼叫:尾呼叫由於是函式的最後一步操作,所以不需要保留外層函式的呼叫幀,因為呼叫位置、內部變數等資訊都不會再用到了,只要直接用內層函式的呼叫幀,取代外層函式的呼叫幀就可以了。

function g (x) {
  console.log(x)
}

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

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

// 等同於
g(3);
複製程式碼

“尾呼叫優化”的意義: 如果所有函式都是尾呼叫,那麼就可以做到每次執行時,呼叫幀只有一項,這將大大節省記憶體.

注意:只有不再用到外層函式的內部變數,內層函式的呼叫幀才會取代外層函式的呼叫幀,否則就無法進行“尾呼叫優化”。

function addOne(a){
  var one = 1;
  function inner(b){
    return b + one;   // 這裡還要使用 外層函式的 one
  }
  return inner(a);
}
複製程式碼

尾遞迴

函式呼叫自身,稱為遞迴。如果尾呼叫自身,就稱為尾遞迴。

遞迴非常耗費記憶體,因為需要同時儲存成千上百個呼叫幀,很容易發生“棧溢位”錯誤(stack overflow)。但是如果使用尾呼叫優化,使每次只存在一個呼叫幀,就不會發生“棧溢位”錯誤。

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

f1(5) // 120

// 尾遞迴
function f2(n, total) {
  if (n === 1) return total;
  console.log(n - 1, n * total)
  return f2(n - 1, n * total);
}
f2(5,1) // 120
複製程式碼

這個函式更具有數學描述性:

如果輸入值是1 => 當前計算數1 * 上一次計算的積total
如果輸入值是x => 當前計算數x * 上一次計算的積total
計算f2(5, 1)的時候,其過程是這樣的:

  • f2(5, 1)
  • f2(4, 5)
  • f2(3, 20)
  • f2(2, 60)
  • f2(1, 120)
  • 120

整個計算過程是線性的,呼叫一次sum(x, total)後,會進入下一個棧,相關的資料資訊和跟隨進入,不再放在堆疊上儲存。當計算完最後的值之後,直接返回到最上層的sum(5,0)。

這能有效的防止堆疊溢位。

普通遞迴改寫需要在最後一步呼叫自身。做到這一點的方法,就是把所有用到的內部變數改寫成函式的引數。這樣做的缺點就是不太直觀。很難看出這些引數是幹什麼的。
有兩個方法可以解決這問題:
1. 內部要調的引數給個預設值

function f2(n, total=1) {
  if (n === 1) return total;
  return f2(n - 1, n * total);
}
f2(5) // 120
複製程式碼

2. 用另外一個函式來返回這個函式。

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

function f3 (n) {
  return  f2(n, 1)
}
f3(5) // 120
複製程式碼

嚴格模式

以上只是尾呼叫優化的寫法,但是並沒有實現真正的優化。

ES6 的尾呼叫優化只在嚴格模式下開啟,正常模式是無效的。

這是因為在正常模式下,函式內部有兩個變數,可以跟蹤函式的呼叫棧。

  • func.arguments:返回撥用時函式的引數。
  • func.caller:返回撥用當前函式的那個函式。

尾呼叫優化發生時,函式的呼叫棧會改寫,因此上面兩個變數就會失真。嚴格模式禁用這兩個變數,所以尾呼叫模式僅在嚴格模式下生效。

尾遞迴優化的實現

尾遞迴優化只在嚴格模式下生效,那麼正常模式下,或者那些不支援該功能的環境中,有沒有辦法也使用尾遞迴優化呢。
那就只有自己是實現遞迴優化了。

原理:尾遞迴之所以需要優化,原因是呼叫棧太多,造成溢位,那麼只要減少呼叫棧,就不會溢位。怎麼做可以減少呼叫棧呢?就是採用“迴圈”換掉“遞迴”。

下面是一個直接寫的尾遞迴:

function sum(x, y) {
  if (y > 0) {
    return sum(x + 1, y - 1);
  } else {
    return x;
  }
}
sum(1, 1000)  // 1001
sum(1, 100000)
// Uncaught RangeError: Maximum call stack size exceeded(…)
複製程式碼

一旦指定sum遞迴 100000 次,就會報錯,提示超出呼叫棧的最大次數。

有兩種方法避免:
1.使用蹦床函式(trampoline) 將遞迴執行轉為迴圈執行。

function trampoline(f) {
  while (f && f instanceof Function) {
    f = f();
  }
  return f;
}
複製程式碼

上面就是蹦床函式的一個實現,它接受一個函式f作為引數。只要f執行後返回一個函式,就繼續執行。
注意,這裡是返回一個函式,然後執行該函式,而不是函式裡面呼叫函式,這樣就避免了遞迴執行,從而就消除了呼叫棧過大的問題。

function sum(x, y) {
  if (y > 0) {
    return sum.bind(null, x + 1, y - 1);
  } else {
    return x;
  }
}
//  sum函式的每次執行,都會返回自身的另一個版本。

trampoline(sum(1, 100000))
// 100001
複製程式碼

2.蹦床函式並不是真正的尾遞迴優化,下面的實現才是

function tco(f) {
  var value;
  var active = false;
  var accumulated = [];
  // console.log(1)
  return function accumulator() {
      // console.log(2, arguments)
    accumulated.push(arguments);
    if (!active) {
      active = true;
      while (accumulated.length) {
        // console.log(3)
        value = f.apply(this, accumulated.shift());
      }
      active = false;
      return value;
    }
  };
}

var sum = tco(function(x, y) {
  if (y > 0) {
    return sum(x + 1, y - 1)
  }
  else {
    return x
  }
});

sum(1, 10000)
// 100001
複製程式碼

相關文章