1. 程式人生 > >《ES6標準入門》:函式的擴充套件

《ES6標準入門》:函式的擴充套件

目錄

1.函式引數預設值

1.1 基本用法

ES6之前不能指定預設引數:

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


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

sfasdf

ES6支援指定預設引數:

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

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

好處:

1.程式碼更加簡潔;
2.語義化更強,易於閱讀;
3.容錯更高,引數不寫也不會報錯。

錯誤用法:
1.引數變數是預設宣告的,所以不能用letconst再次宣告。

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

2.使用引數預設值時,函式不能有同名引數。

function foo(x, x, y = 1) {
  // ...
}
// SyntaxError: Duplicate parameter name not allowed in this context

1.2 與解構賦值預設值結合使用

基本場景

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 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]

1.3 引數預設值位置

通常情況下,定義了預設值的引數,應該是函式的尾引數。因為這樣比較容易看出來,到底省略了哪些引數。如果非尾部的引數設定預設值,實際上這個引數是沒法省略的。

// 例一
function f(x = 1, y) {
  return [x, y];
}

f() // [1, undefined]
f(2) // [2, undefined])
f(, 1) // 報錯
f(undefined, 1) // [1, 1]

// 例二
function f(x, y = 5, z) {
  return [x, y, z];
}

f() // [undefined, 5, undefined]
f(1) // [1, 5, undefined]
f(1, ,2) // 報錯
f(1, undefined, 2) // [1, 5, 2]

上面程式碼中,有預設值的引數都不是尾引數。這時,無法只省略該引數,而不省略它後面的引數,除非顯式輸入undefined

如果傳入undefined,將觸發該引數等於預設值,null則沒有這個效果。

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

foo(undefined, null)
// 5 null

上面程式碼中,x引數對應undefined,結果觸發了預設值,y引數等於null,就沒有觸發預設值。

1.4 函式的 length 屬性

指定了預設值以後,函式的length屬性,將返回沒有指定預設值的引數個數。也就是說,指定了預設值後,length屬性將失真。

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

1.5 作用域

區域性優先於全域性

var x = 1;

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

f(2) // 2

預設賦值前未宣告情況

let x = 1;

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

f() // 1

也就是用變數給引數賦值前得先宣告。

關於函式作為引數作用域
和之前遵循一樣的規則:

let foo = 'outer';

function bar(func = x => foo) {
  let foo = 'inner';
  console.log(func());
}

bar(); // outer

2.rest引數

引數形式 :
...變數名

其實就是把引數轉化成了一個數組:

function add(...values) {
  console.log(values)
}

add(2, 5, 3) // [2,3,5]

function push(array, ...items) {
  items.forEach(function(item) {
    array.push(item);
    console.log(item);
  });
}

var a = [];
push(a, 1, 2, 3)

注意 :
rest 引數之後不能再有其他引數(即只能是最後一個引數),否則會報錯。

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

函式的length屬性,不包括 rest 引數。

(function(a) {}).length  // 1
(function(...a) {}).length  // 0
(function(a, ...b) {}).length  // 1

3.擴充套件運算子

形式 :

...

好比 rest 引數的逆運算,將一個數組轉為用逗號分隔的引數序列。

基本用法 :

console.log(...[1, 2, 3])
// 1 2 3

console.log(1, ...[2, 3, 4], 5)
// 1 2 3 4 5

[...document.querySelectorAll('div')]
// [<div>, <div>, <div>]

該運算子主要用於函式呼叫。

function push(array, ...items) {
  array.push(...items);
}

function add(x, y) {
  return x + y;
}

var numbers = [4, 38];
add(...numbers) // 42

替代陣列的apply方法

由於擴充套件運算子可以展開陣列,所以不再需要apply方法,將陣列轉為函式的引數了。

// ES5的寫法
function f(x, y, z) {
  // ...
}
var args = [0, 1, 2];
f.apply(null, args);

// ES6的寫法
function f(x, y, z) {
  // ...
}
var args = [0, 1, 2];
f(...args);

下面是擴充套件運算子取代apply方法的一個實際的例子,應用Math.max方法,簡化求出一個數組最大元素的寫法。

// ES5的寫法
Math.max.apply(null, [14, 3, 77])

// ES6的寫法
Math.max(...[14, 3, 77])

// 等同於
Math.max(14, 3, 77);

上面程式碼表示,由於JavaScript不提供求陣列最大元素的函式,所以只能套用Math.max函式,將陣列轉為一個引數序列,然後求最大值。有了擴充套件運算子以後,就可以直接用Math.max了。

另一個例子是通過push函式,將一個數組新增到另一個數組的尾部。

// ES5的寫法
var arr1 = [0, 1, 2];
var arr2 = [3, 4, 5];
Array.prototype.push.apply(arr1, arr2);

// ES6的寫法
var arr1 = [0, 1, 2];
var arr2 = [3, 4, 5];
arr1.push(...arr2);

上面程式碼的ES5寫法中,push方法的引數不能是陣列,所以只好通過apply方法變通使用push方法。有了擴充套件運算子,就可以直接將陣列傳入push方法。

下面是另外一個例子。

// ES5
new (Date.bind.apply(Date, [null, 2015, 1, 1]))
// ES6
new Date(...[2015, 1, 1]);

擴充套件運算子的應用

(1)合併陣列

// ES5
[1, 2].concat(more)
// ES6
[1, 2, ...more]

var arr1 = ['a', 'b'];
var arr2 = ['c'];
var arr3 = ['d', 'e'];

// ES5的合併陣列
arr1.concat(arr2, arr3);
// [ 'a', 'b', 'c', 'd', 'e' ]

// ES6的合併陣列
[...arr1, ...arr2, ...arr3]
// [ 'a', 'b', 'c', 'd', 'e' ]

(2)與解構賦值結合來生成陣列

// ES5
a = list[0], rest = list.slice(1)
// ES6
[a, ...rest] = list

如果將擴充套件運算子用於陣列賦值,只能放在引數的最後一位,否則會報錯。

const [...butLast, last] = [1, 2, 3, 4, 5];
// 報錯

const [first, ...middle, last] = [1, 2, 3, 4, 5];
// 報錯

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

function doSomething(a, b) {
  'use strict';
  // code
}

4.嚴格模式

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

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

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

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

const obj = {
  // 報錯
  doSomething({a, b}) {
    'use strict';
    // code
  }
};

5.箭頭函式

5.1 基本用法

箭頭函式是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;
};

箭頭函式的一個用處是簡化回撥函式。

// 正常函式寫法
[1,2,3].map(function (x) {
  return x * x;
});

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

下面是 rest 引數與箭頭函式結合的例子。

const numbers = (...nums) => nums;

numbers(1, 2, 3, 4, 5)
// [1,2,3,4,5]

const headAndTail = (head, ...tail) => [head, tail];

headAndTail(1, 2, 3, 4, 5)
// [1,[2,3,4,5]]

5.2 注意點

this物件的指向是可變的,但是在箭頭函式中,它是固定的。

function foo() {
  setTimeout(() => {
    console.log('id:', this.id);
  }, 100);
}

var id = 21;

foo.call({ id: 42 });
// id: 42

5.3 巢狀的箭頭函式

箭頭函式內部,還可以再使用箭頭函式。下面是一個 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]

6.尾呼叫優化

6.1 什麼是尾呼叫?

尾呼叫(Tail Call)是函數語言程式設計的一個重要概念,本身非常簡單,一句話就能說清楚,就是指某個函式的最後一步是呼叫另一個函式。

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);
}

上面程式碼中,函式mn都屬於尾呼叫,因為它們都是函式f的最後一步操作。

6.2 尾呼叫優化

尾呼叫之所以與其他呼叫不同,就在於它的特殊的呼叫位置。

我們知道,函式呼叫會在記憶體形成一個“呼叫記錄”,又稱“呼叫幀”(call frame),儲存呼叫位置和內部變數等資訊。如果在函式A的內部呼叫函式B,那麼在A的呼叫幀上方,還會形成一個B的呼叫幀。等到B執行結束,將結果返回到AB的呼叫幀才會消失。如果函式B內部還呼叫函式C,那就還有一個C的呼叫幀,以此類推。所有的呼叫幀,就形成一個“呼叫棧”(call stack)。

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

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

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

// 等同於
g(3);

上面程式碼中,如果函式g不是尾呼叫,函式f就需要儲存內部變數mn的值、g的呼叫位置等資訊。但由於呼叫g之後,函式f就結束了,所以執行到最後一步,完全可以刪除f(x)的呼叫幀,只保留g(3)的呼叫幀。

這就叫做“尾呼叫優化”(Tail call optimization),即只保留內層函式的呼叫幀。如果所有函式都是尾呼叫,那麼完全可以做到每次執行時,呼叫幀只有一項,這將大大節省記憶體。這就是“尾呼叫優化”的意義。

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

function addOne(a){
  var one = 1;
  function inner(b){
    return b + one;
  }
  return inner(a);
}

上面的函式不會進行尾呼叫優化,因為內層函式inner用到了外層函式addOne的內部變數one

6.3 尾遞迴

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

遞迴非常耗費記憶體,因為需要同時儲存成千上百個呼叫幀,很容易發生“棧溢位”錯誤(stack overflow)。但對於尾遞迴來說,由於只存在一個呼叫幀,所以永遠不會發生“棧溢位”錯誤。

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

還有一個比較著名的例子,就是計算 Fibonacci 數列,也能充分說明尾遞迴優化的重要性。

非尾遞迴的 Fibonacci 數列實現如下。

function Fibonacci (n) {
  if ( n <= 1 ) {return 1};

  return Fibonacci(n - 1) + Fibonacci(n - 2);
}

Fibonacci(10) // 89
Fibonacci(100) // 堆疊溢位
Fibonacci(500) // 堆疊溢位

尾遞迴優化過的 Fibonacci 數列實現如下。

function Fibonacci2 (n , ac1 = 1 , ac2 = 1) {
  if( n <= 1 ) {return ac2};

  return Fibonacci2 (n - 1, ac2, ac1 + ac2);
}

Fibonacci2(100) // 573147844013817200000
Fibonacci2(1000) // 7.0330367711422765e+208
Fibonacci2(10000) // Infinity

由此可見,“尾呼叫優化”對遞迴操作意義重大,所以一些函數語言程式設計語言將其寫入了語言規格。ES6 是如此,第一次明確規定,所有 ECMAScript 的實現,都必須部署“尾呼叫優化”。這就是說,ES6 中只要使用尾遞迴,就不會發生棧溢位,相對節省記憶體。

6.4 遞迴函式的改寫

尾遞迴的實現,往往需要改寫遞迴函式,確保最後一步只調用自身。做到這一點的方法,就是把所有用到的內部變數改寫成函式的引數。比如上面的例子,階乘函式 factorial 需要用到一箇中間變數total,那就把這個中間變數改寫成函式的引數。這樣做的缺點就是不太直觀,第一眼很難看出來,為什麼計算5的階乘,需要傳入兩個引數51

兩個方法可以解決這個問題。方法一是在尾遞迴函式之外,再提供一個正常形式的函式。

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

function factorial(n) {
  return tailFactorial(n, 1);
}

factorial(5) // 120

上面程式碼通過一個正常形式的階乘函式factorial,呼叫尾遞迴函式tailFactorial,看起來就正常多了。

函數語言程式設計有一個概念,叫做柯里化(currying),意思是將多引數的函式轉換成單引數的形式。這裡也可以使用柯里化。

function currying(fn, n) {
  return function (m) {
    return fn.call(this, m, n);
  };
}

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

const factorial = currying(tailFactorial, 1);

factorial(5) // 120

上面程式碼通過柯里化,將尾遞迴函式tailFactorial變為只接受一個引數的factorial

第二種方法就簡單多了,就是採用 ES6 的函式預設值。

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

factorial(5) // 120

上面程式碼中,引數total有預設值1,所以呼叫時不用提供這個值。

總結一下,遞迴本質上是一種迴圈操作。純粹的函數語言程式設計語言沒有迴圈操作命令,所有的迴圈都用遞迴實現,這就是為什麼尾遞迴對這些語言極其重要。對於其他支援“尾呼叫優化”的語言(比如Lua,ES6),只需要知道迴圈可以用遞迴代替,而一旦使用遞迴,就最好使用尾遞迴。

相關推薦

ES6標準入門》:函式擴充套件

目錄 1.函式引數預設值 1.1 基本用法 ES6之前不能指定預設引數: let a = 0 function log(x, y) { y = y || 'World'; console.log(x, y); } l

ECMAScript6(ES6標準函式擴充套件特性箭頭函式、Rest引數及展開操作符

ES6擴充套件了很多語法糖語法 其中對於函式我們又可以使用一種叫做“箭頭函式”的寫法 同時引入了Rest引數 利用“…”可以獲取多餘引數 這樣就我們就不要使用arguments物件了 下面我來詳細地談一談 函式預設引數 ES6沒有出現之前 面

ES6標準入門》29~48Page 字符串拓展 正則拓展

har 字節 其中 logs 屬性表 regex fff 不能 包含 1.字符串的拓展 ES3允許使用類似\u0061這樣的形式來表示字符,其中的數字是Unicode-8編碼。 但如果超出\uffff的字符,必須使用雙字節的形式表達,例如 \uD842\uDFB7。 在ES

ES6標準入門》49~68Page 數值的拓展 數組的拓展

() 給定 結果 格式 int 指數 undefine define 可能 1.數值拓展 ES6提供的二進制和八進制表示法分別是二進制: 0B111110111(0b111110111) 八進制: 0O767(0o767) ES6提供了新的Number.isFinite()

ES6標準入門 第二章:塊級作用域 以及 let和const命令

函數聲明 web 頂部 16px 地址 value length window對象 成功 一、塊級作用域   1、為什麽需要塊級作用域?   ES5中只有全局作用域和函數作用域,帶來很多不合理的場景。   (1)內層變量可能會覆蓋外層變量;    var tem = ne

ES6標準入門 第四章:字符串的擴展

固定 缺陷 長度 需要 允許 實例對象 poi turn har 1、字符串的Unicode 表示法 JavaScript 允許采用 \uxxxx 表示一個字符,其中 xxxx 表示字符的碼點。 "\u0061" // "a" ES5中的缺陷: 以上表示

ES6標準入門(第三版)學習筆記(1)

  ES6宣告變數的六種方法   ES5只有兩種 var,function命令   ES6新增了let,const,class,import命令 驗證var與let用法上的不同 var a = []; for (var i = 0; i < 10; i++){

ES6標準入門之變數的解構賦值簡單解說

  首先我們來看一看解構的概念,在ES6標準下,允許按照一定模式從陣列和物件中提取值,然後對變數進行賦值,這被稱作解構,簡而言之粗糙的理解就是變相賦值。   解構賦值的規則是,只要等號右邊的值不是物件或者陣列,就先將其轉為物件。   一、陣列的結構賦值   以前為變數賦值只能直接指定。而ES6允許從陣列

ES6標準入門之字串的拓展講解

  在開始講解ES6中字串拓展之前,我們先來看一下ES5中字串的一些方法。     獲取字串長度 str.length     分割字串 str.split()     拼接字串 str1+str2 或 str1.concat(str2)     替換字串 str.replace(“玩遊戲”,”好好學習”)

ES6標準入門之正則表示式的拓展

  所謂正則表示式,又稱規則表示式。(英語:Regular Expression,在程式碼中常簡寫為regex、regexp或RE),電腦科學的一個概念。正則表示式通常被用來檢索、替換那些符合某個模式(規則)的文字。在之前使用基於Jquery庫開發專案的時候,用的正則表示式最多的就是一些輸入框的檢驗,比如檢驗

ES6標準入門》(九)之Class

昨天,360面試官問了個問題,說:用ES5怎麼實現ES6中的class? 因為沒有看過class,就說不了解,結果回來一看,這不就是ES5怎麼建立物件嗎???我寫了那麼多,看了那麼多,就這麼不會的完事的,哎。。。今天,來總結一下ES6中的Class 極客學院講的很詳細,參

es6標準入門》讀書筆記-第一章 ECMAScript 6簡介

以下內容使用的書籍為《es6標準入門-第2版》,阮一峰著,如有需要請購買正版 本文僅為個人讀書筆記,如有不詳之處請查閱原文 執行環境 node環境的檢查 檢視node對es6的支援狀況 $ node --v8-optio

es6標準入門》 阮一峰

2 let和const命令    2.1 let命令        2.1.1 基本用法        2.1.2 不存在變數提升        2.1.3 暫時性死區        2.1.4 不允許重複宣告    2.2 塊級作用域        2.2.1 為什麼需要塊

js -- ES6(一)-- 簡介(根據阮一峰ES6標準入門整理)

目前正在學習ES6,根據阮一峰的ES6入門2,學到哪更新到哪裡,都是基本的知識,複雜的目前還不會,涉及的程式碼都是親自執行過的,若發現錯誤請指正。 ES6 提供了許多新特性,但是並不是所有的瀏

ES6 標準入門》讀書筆記

過年在家閒著沒事,來看看ES6,選了阮一峰大大的《ES6 標準入門》這本書,瞭解一下新的js規範。這裡做一下讀書筆記。 ECMAScript 6 須知 目前各大瀏覽器的自新版本應該都支援ES6了,並且Node.js對ES6的支援度比瀏覽器還高,通過Nod

es6標準入門》知識整理(4)- Reflect

昨天,我做了 es6 中的新物件 Proxy 的相關的知識整理,現在我會整理一下 es6 中另外一個新的內建物件:Reflect。

es6 入坑筆記(二)---函式擴充套件,箭頭函式擴充套件運算子...

函式擴充套件 1.函式可以有預設值 function demo( a = 10,b ){} 2.函式可以使用解構  function demo( { a = 0,b = 0 } = {} ){ } 3.函式引數最後可以多一個逗號 function demo(a,b,

es6——函式擴充套件

1.形參設定預設值 es5 { function sum1(num1, num2) { num1 = num1 || 10;

es6 函式擴充套件,引數作用域和箭頭函式

函式的擴充套件 函式引數的預設值 基本用法 ES6 之前,不能直接為函式的引數指定預設值,只能採用變通的方法。 function log(x, y = 'World') { console.log(x, y); } log('Hello

js-ES6學習筆記-函式擴充套件

1、ES6函式引數的預設值,直接寫在引數定義的後面。引數變數是預設宣告的,所以不能用let或const再次宣告。 function Point(x = 0, y = 0) { this.x = x; this.y = y; } var p = new Point(); p //