學習使用ES6(三)

一、函數的擴展

1.函數參數的默認值

function Point(x = 0, y = 0) {
  this.x = x;
  this.y = y;
}

const p = new Point();
p // { x: 0, y: 0 }

參數變量是默認聲明的,所以不能用let或const再次聲,參數變量x是默認聲明的,在函數體中,不能用let或const再次聲明,否則會報錯。

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

 下面兩種寫法都對函數的參數設定了默認值,區別是寫法一函數參數的默認值是空對象,但是設置了對象解構賦值的默認值;寫法二函數參數的默認值是一個有具體屬性的對象,但是沒有設置對象解構賦值的默認值。

// 寫法一
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]

 函數的length屬性

指定了默認值以後,函數的length屬性將返回沒有指定默認值的參數個數,也就是指定默認值後,length屬性將失真

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

如果設置了默認值的參數不是尾參數,那麼length屬性也不再計入後面的參數了。

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

 作用域

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

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

var x = 1;

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

f(2) // 2

函數f調用時,參數y = x形成一個單獨的作用域。這個作用域裏面,變量x本身沒有定義,所以指向外層的全局變量x。函數調用時,函數體內部的局部變量x影響不到默認值變量x。

let x = 1;

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

f() // 1

 函數foo的參數形成一個單獨作用域。這個作用域裏面,首先聲明瞭變量x,然後聲明瞭變量yy的默認值是一個匿名函數。這個匿名函數內部的變量x,指向同一個作用域的第一個參數x。函數foo內部又聲明瞭一個內部變量x,該變量與第一個參數x由於不是同一個作用域,所以不是同一個變量,因此執行y後,內部變量x和外部全局變量x的值都沒變。

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

foo() // 3
x // 1

2.rest參數 

rest 參數(形式爲...變量名),用於獲取函數的多餘參數,這樣就不需要使用arguments對象了。rest 參數搭配的變量是一個數組,該變量將多餘的參數放入數組中。

function add(...values) {
  let sum = 0;

  for (var val of values) {
    sum += val;
  }

  return sum;
}

add(2, 5, 3) // 10

 3.嚴格模式

規定只要函數參數使用了默認值、解構賦值、或者擴展運算符,那麼函數內部就不能顯式設定爲嚴格模式,否則會報錯

// 報錯
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
  }
};

兩種方法可以規避這種限制。第一種是設定全局性的嚴格模式,這是合法的。

'use strict';

function doSomething(a, b = a) {
  // code
}

第二種是把函數包在一個無參數的立即執行函數裏面。

const doSomething = (function () {
  'use strict';
  return function(value = 42) {
    return value;
  };
}());

 4.name屬性

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

var f = function () {};

// ES5
f.name // ""

// ES6
f.name // "f"

Function構造函數返回的函數實例,name屬性的值爲anonymous。

(new Function).name // "anonymous"
bind返回的函數,name屬性值會加上bound前綴。

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

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

5.箭頭函數

ES6 允許使用“箭頭”(=>)定義函數,箭頭函數可以與變量解構結合使用。

// 報錯
let getTempItem = id => { id: id, name: "Temp" };

// 不報錯
let getTempItem = id => ({ id: id, name: "Temp" });

使用注意點

箭頭函數有幾個使用注意點。

(1)函數體內的this對象,就是定義時所在的對象,而不是使用時所在的對象。

(2)不可以當作構造函數,也就是說,不可以使用new命令,否則會拋出一個錯誤。

(3)不可以使用arguments對象,該對象在函數體內不存在。如果要用,可以用 rest 參數代替。

(4)不可以使用yield命令,因此箭頭函數不能用作 Generator 函數。

上面四點中,第一點尤其值得注意。this對象的指向是可變的,但是在箭頭函數中,它是固定的。

6.雙冒號運算符

函數綁定運算符是並排的兩個冒號(::),雙冒號左邊是一個對象,右邊是一個函數。該運算符會自動將左邊的對象,作爲上下文環境(即this對象),綁定到右邊的函數上面。

foo::bar;
// 等同於
bar.bind(foo);

foo::bar(...arguments);
// 等同於
bar.apply(foo, arguments);

const hasOwnProperty = Object.prototype.hasOwnProperty;
function hasOwn(obj, key) {
  return obj::hasOwnProperty(key);
}

如果雙冒號左邊爲空,右邊是一個對象的方法,則等於將該方法綁定在該對象上面。

var method = obj::obj.foo;
// 等同於
var method = ::obj.foo;

let log = ::console.log;
// 等同於
var log = console.log.bind(console);

如果雙冒號運算符的運算結果,還是一個對象,就可以採用鏈式寫法。

import { map, takeWhile, forEach } from "iterlib";

getPlayers()
::map(x => x.character())
::takeWhile(x => x.strength > 100)
::forEach(x => console.log(x));

7.尾調用優化 

什麼是尾調用?

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

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

錯誤的尾調用

// 情況一
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的最後一步操作。

尾遞歸優化的實現?

蹦牀函數(trampoline)可以將遞歸執行轉爲循環執行,但蹦牀函數並不是真正的尾遞歸優化。

function trampoline(f) {
  while (f && f instanceof Function) {
    f = f();
  }
  return f;
}

 

 

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