ECMAScript6(5):函數的擴展

參數默認值

ES5中設置默認值很是不方便, 咱們這樣寫:javascript

function fun(a){
  a = a || 2;
  console.log(a);
}
fun();   //2
fun(0);  //2
fun(1);  //1

以上寫法, 若是傳入了參數, 但這個參數對應值的布爾型是 false, 就不起做用了。固然你也能夠判斷 arguments.length 是否爲0來避免這個問題, 但每一個函數這樣寫就太囉嗦了, 尤爲參數比較多的時候。在 ES6 中咱們能夠直接寫在參數表中, 若是實際調用傳遞了參數, 就用這個傳過來的參數, 不然用默認參數。像這樣:java

function fun(a=2){
  console.log(a);
}
fun();   //2
fun(0);  //0
fun(1);  //1

其實函數默認參數這一點最強大的地方在於能夠和解構賦值結合使用:編程

//參數傳遞
function f([x, y, z=4]){
  return [x+1, y+2, z+3];
}
var [a, b, c] = f([1, 2]);  //a=2, b=4, c=7
[[1, 2], [3, 4]].map(([a, b]) => a + b);   //返回 [3, 7]

經過上面這個例子不難發現, 不只能夠用解構的方法設置初始值, 還能夠進行參數傳遞。固然, 這裏也能夠是對象形式的解構賦值。若是傳入的參數沒法解構, 就會報錯:segmentfault

function fun1({a=1, b=5, c='A'}){
  console.log(c + (a + b));
}
fun1({});   //'A6'
fun1();     //TypeError, 由於沒法解構
//但這樣設計函數對使用函數的碼農很不友好
//因此, 技巧:
function fun2({a=1, b=5, c='A'}={}){
  console.log(c + (a + b));
}
fun2();     //'A6'

注意, 其實還有一種方法, 但不如這個好, 咱們比較以下:數組

//fun1 比 fun2 好, 不會產生之外的 undefined
function fun1({a=1, b=5, c='A'}={}){
  console.log(c + (a + b));
}
function fun2({a, b, c}={a: 1, b: 5, c: 'A'}){
  console.log(c + (a + b));
}
//傳了參數, 但沒傳所有參數就會出問題
fun1({a: 8});     //'A13'
fun2({a: 8});     //NaN

不過這裏強烈建議, 將具備默認值的參數排在參數列表的後面。不然調用時依然須要傳參:閉包

function f1(a=1, b){
  console.log(a + b);
}
function f2(a, b=1){
  console.log(a + b);
}
f2(2);   //3
f1(, 2);  //報錯
f1(undefined, 2);  //3, 注意這裏不能用 null 觸發默認值

這裏咱們還須要單獨討論一下默認參數對 arguments 的影響:app

function foo(a = 1){
  console.log(a, arguments[0]);
}

foo();            //1 undefined
foo(undefined);   //1 undefined
foo(2);           //2 2
foo(null);        //null null

很明顯,默認參數並不能加到 arguments 中。函數式編程

  • 函數的 length 屬性

這個屬性ES6 以前就是存在的, 記得length表示預計傳入的形參個數, 也就是沒有默認值的形參個數:函數

(function(a){}).length;   //1
(function(a = 5){}).length;   //0
(function(a, b, c=5){}).length;   //2
(function(...args){}).length;   //0, rest參數也不計入 length

rest 參數

rest 參數形式爲 ...變量名, 它會將對應的所有實際傳遞的變量放入數組中, 能夠用它來替代 arguments:優化

function f(...val){
  console.log(val.join());
}
f(1, 2);      //[1, 2]
f(1, 2, 3, 4);  //[1, 2, 3, 4]

function g(a, ...val){
  console.log(val.join());
}
g(1, 2);      //[2]
g(1, 2, 3, 4);  //[2, 3, 4]

不然這個函數 g 你的這樣定義函數, 比較麻煩:

function g(a){
  console.log([].slice.call(arguments, 1).join());
}

這裏須要注意2點:

  • rest參數必須是函數的最後一個參數, 它的後面不能再定義參數, 不然會報錯。
  • rest參數不計入函數的 length 屬性中

建議:

  • 全部配置項都應該集中在一個對象,放在最後一個參數,布爾值不能夠直接做爲參數。這樣方便調用者以任何順序傳遞參數。
  • 不要在函數體內使用arguments變量,使用rest運算符(...)代替。由於rest運算符顯式代表你想要獲取參數,並且arguments是一個相似數組的對象,而rest運算符能夠提供一個真正的數組。
  • 使用默認值語法設置函數參數的默認值。

擴展運算符

擴展運算符相似 rest運算符的逆運算, 用 ... 表示, 放在一個(類)數組前, 將該數組展開成獨立的元素序列:

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

擴展運算符的用處不少:

  • 能夠用於快速改變類數組對象爲數組對象, 也是用於其餘可遍歷對象:
[...document.querySelectorAll('li')];   //[<li>, <li>, <li>];
  • 結合 rest 參數使函數事半功倍:
function push(arr, ...val){
  return arr.push(...val);      //調用函數時, 將數組變爲序列
}
  • 替代 apply 寫法
var arr = [1, 2, 3];
var max = Math.max(...arr);   //3

var arr2 = [4, 5, 6];
arr.push(...arr2);     //[1, 2, 3, 4, 5, 6]

new Date(...[2013, 1, 1]);   //ri Feb 01 2013 00: 00: 00 GMT+0800 (CST)
  • 鏈接, 合併數組
var more = [4, 5];
var arr = [1, 2, 3, ...more];    //[1, 2, 3, 4, 5]

var a1 = [1, 2];
var a2 = [3, 4];
var a3 = [5, 6];
var a = [...a1, ...a2, ...a3];     //[1, 2, 3, 4, 5, 6]
  • 解構賦值
var a = [1, 2, 3, 4, 5];
var [a1, ...more] = a;      //a1 = 1, more = [2, 3, 4, 5]
//注意, 擴展運算符必須放在解構賦值的結尾, 不然報錯
  • 字符串拆分
var str = "hello";
var alpha = [...str];    //alpha = ['h', 'e', 'l', 'l', 'o']

[...'x\uD83D\uDE80y'].length;   //3, 正確處理32位 unicode 字符

建議:使用擴展運算符(...)拷貝數組。

name 屬性

name 屬性返回函數的名字, 對於匿名函數返回空字符串。不過對於表達式法定義的函數, ES5 和 ES6有差異:

var fun = function(){}
fun.name;     //ES5: "", ES6: "fun"

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

對於有2個名字的函數, 返回後者, ES5 和 ES6沒有差異:

var fun  = function baz(){}
fun.name;        //baz

對於 Function 構造函數獲得的函數, 返回 anonymous:

new Function("fun").name;    //"anonymous"
new Function().name;    //"anonymous"
(new Function).name;    //"anonymous"

對於 bind 返回的函數, 加上 bound 前綴

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

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

(new Function).bind({}).name;    //"bound anonymous"

箭頭函數

箭頭函數的形式以下:

var fun = (參數列表) => {函數體};

若是隻有一個參數(且不指定默認值), 參數列表的圓括號能夠省略; (若是沒有參數, 圓括號不能省略)
若是隻有一個 return 語句, 那麼函數體的花括號也能夠省略, 同時省略 return 關鍵字。

var fun = value => value + 1;
//等同於
var fun = function(value){
  return value + 1;
}
var fun = () => 5;
//等同於
var fun = function(){
  return 5;
}

若是箭頭函數的參數或返回值有對象, 應該用 () 括起來:

var fun = n => ({name: n});
var fun = ({num1=1, num2=3}={}) => num1 + num2;

看完以前的部分, 箭頭函數應該不陌生了:

var warp = (...val) => val;
var arr1 = warp(2, 1, 3);              //[2, 1, 3]
var arr2 = arr1.map(x => x * x);     //[4, 1, 9]
arr2.sort((a, b) => a - b);          //[1, 4, 9]

使用箭頭函數應注意如下幾點:

  • 不能夠將函數當作構造函數調用, 即不能使用 new 命令;
  • 不能夠在箭頭函數中使用 yield 返回值, 因此不能用過 Generator 函數;
  • 函數體內不存在 arguments 參數;
  • 函數體內部不構成獨立的做用域, 內部的 this 和定義時候的上下文一致; 但能夠經過 call, apply, bind 改變函數中的 this。關於做用域, 集中在ES6函數擴展的最後討論。

舉幾個箭頭函數的實例:
實例1: 實現功能如: insert(2).into([1, 3]).after(1)insert(2).into([1, 3]).before(3)這樣的函數:

var insert = value => ({
  into: arr => ({
    before: val => {
      arr.splice(arr.indexOf(val), 0, value);
      return arr;
    },
    after: val => {
      arr.splice(arr.indexOf(val) + 1, 0, value);
      return arr;
    }
  })
});
console.log(insert(2).into([1, 3]).after(1));
console.log(insert(2).into([1, 3]).before(3));

實例2: 構建一個管道(前一個函數的輸出是後一個函數的輸入):

var pipe = (...funcs) => (init_val) => funcs.reduce((a, b) => b(a), init_val);

//實現 2 的 (3+2) 次方
var plus = a => a + 2;
pipe(plus, Math.pow.bind(null, 2))(3);         //32

實例3: 實現 λ 演算

//fix = λf.(λx.f(λv.x(x)(v)))(λx.f(λv.x(x)(v)))
var fix = f => (x => f(v => x(x)(v)))(x => f(v => x(x)(v)));

建議:箭頭函數取代 Function.prototype.bind,不該再用 self / _this / that 綁定 this。其次,簡單的、不會複用的函數,建議採用箭頭函數。若是函數體較爲複雜,行數較多,仍是應該採用傳統的函數寫法。

這裏須要強調,如下狀況不能使用箭頭函數:

  1. 定義字面量方法
let calculator = {
  array: [1, 2, 3],
  sum: () => {
    return this.array.reduce((result, item) => result + item);     //這裏的 this 成了 window
  }
};
calculator.sum();    //"TypeError: Cannot read property 'reduce' of undefined"
  1. 定義原型方法
function Cat(name) {
    this.name = name;
}
Cat.prototype.sayCatName = () => {
    return this.name;           //和上一個問題同樣:這裏的 this 成了 window
};
let cat = new Cat('Mew');
cat.sayCatName();               //undefined
  1. 綁定事件
const button = document.getElementById('myButton');
button.addEventListener('click', () => {
    this.innerHTML = 'Clicked button';        //這裏的 this 本應該是 button, 但不幸的成了 window
});
  1. 定義構造函數
let Message = (text) => {
    this.text = text;
};
let helloMessage = new Message('Hello World!');         //TypeError: Message is not a constructor
  1. 不要爲了追求代碼的簡短喪失可讀性
let multiply = (a, b) => b === undefined ? b => a * b : a * b;    //這個太難讀了,太費時間
let double = multiply(2);
double(3);      //6
multiply(2, 3); //6

函數綁定

ES7 中提出了函數綁定運算, 免去咱們使用 call, bind, apply 的各類不方便, 形式以下:

objName::funcName

如下幾組語句兩兩等同

var newFunc = obj::func;
//至關於
var newFunc = func.bind(obj);

var result = obj::func(...arguments);
//至關於
var result = func.apply(obj, arguments);

若是 :: 左邊的對象本來就是右邊方法中的 this, 左邊能夠省略

var fun = obj::obj.func;
//至關於
var fun = ::obj.func;
//至關於
var fun = obj.func.bind(obj);

:: 運算返回的仍是對象, 能夠進行鏈式調用:

$('.my-class')::find('p')::text("new text");
//至關於
$('.my-class').find('p').text("new text");

尾調用優化

尾調用是函數式編程的概念, 指在函數最後調用另外一個函數。

//是尾調用
function a(){
  return g();
}
function b(p){
  if(p>0){
    return m();
  }
  return n();
}
function c(){
  return c();
}

//如下不是尾調用
function d(){
  var b1 = g();
  return b1;
}
function e(){
  g();
}
function f(){
  return g() + 1;
}

尾調用的一個顯著特色就是, 咱們能夠將函數尾部調用的函數放在該函數外面(後面), 而不改變程序實現結果。這樣能夠減小函數調用棧的開銷。
這樣的優化在 ES6 的嚴格模式中被強制實現了, 咱們須要作的僅僅是在使用時候利用好這個優化特性, 好比下面這個階乘函數:

function factorial(n){
  if(n <= 1) return 1;
  return n * factorial(n - 1);
}
factorial(5);     //120

這個函數計算 n 的階乘, 就要在內存保留 n 個函數調用記錄, 空間複雜度 O(n), 若是 n 很大可能會溢出。因此進行優化以下:

"use strict";
function factorial(n, result = 1){
  if(n <= 1) return result;
  return factorial(n - 1, n * result);
}
factorial(5);     //120

固然也可使用柯里化:

var factorial = (function factor(result, n){
  if(n <= 1) return result;
  return factor(n * result, n - 1);
}).bind(null, 1);
factorial(5);     //120

函數的尾逗號

這個僅僅是一個提案: 爲了更好地進行版本控制, 在函數參數尾部加一個逗號, 表示該函很多天後會被修改, 便於版本控制器跟蹤。目前並未實現。

做用域

這裏僅僅討論 ES6 中的變量做用域。除了 let 和 const 定義的的變量具備塊級做用域之外, varfunction 依舊遵照詞法做用域, 詞法做用域能夠參考博主的另外一篇文章javascript函數、做用域鏈與閉包

首先看一個例子:

var x = 1;
function f(x, y=x){
  console.log(y);
}
f(2);    //2

這個例子輸出了2, 由於 y 在初始化的時候, 函數內部的 x 已經定義並完成賦值了, 因此, y = x 中的 x 已是函數的局部變量 x 了, 而不是全局的 x。固然, 若是局部 x 變量在 y 聲明以後聲明就沒問題了。

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

那若是函數的默認參數是函數呢?燒腦的要來了:

var foo = "outer";
function f(x){
  return foo;
}
function fun(foo, func = f){
  console.log(func());
}
fun("inner");   //"outer"

若是基礎好, 那就根本談不上不燒腦。由於, 函數中的做用域取決於函數定義的地方, 函數中的 this 取決於函數調用的方式。(敲黑板)
但若是這樣寫, 就是 inner 了, 由於func默認函數定義的時候 fun內的 foo 已經存在了。

var foo = "outer";
function fun(foo, func = function(x){
  return foo;
}){
  console.log(func());
}
fun("inner");   //"inner"

技巧: 利用默認值保證必需的參數被傳入, 而減小對參數存在性的驗證:

function throwErr(){
  throw new Error("Missing Parameter");
}
function fun(necessary = throwErr()){
  //...若是參數necessary沒有收到就使用參數, 從而執行函數拋出錯誤
}

//固然也能夠這樣表示一個參數是可選的
function fun(optional = undefined){
  //...
}

箭頭函數的做用域和定義時的上下文一致, 但能夠經過調用方式改變:

window && (window.name = "global") || (global.name = "global");
var o = {
  name: 'obj-o',
  foo: function (){
    setTimeout(() => {console.log(this.name); }, 500);
  }
}

var p = {
  name: 'obj-p',
  foo: function (){
    setTimeout(function(){console.log(this.name); }, 1000);
  }
}

o.foo();    //"obj-o"
p.foo();    //"global"

var temp = {
  name: 'obj-temp'
}

o.foo.bind(temp)();     //"obj-temp"
o.foo.call(temp);     //"obj-temp"
o.foo.apply(temp);     //"obj-temp"

p.foo.bind(temp)();     //"global"
p.foo.call(temp);     //"global"
p.foo.apply(temp);     //"global"
相關文章
相關標籤/搜索