函數的擴展

函數的擴展html

  1. 1.         函數參數的默認值
  2. 2.         rest參數
  3. 3.         擴展運算符
  4. 4.         name屬性
  5. 5.         箭頭函數
  6. 6.         函數綁定
  7. 7.         尾調用優化
  8. 8.         函數參數的尾逗號

函數參數的默認值node

基本用法python

在ES6以前,不能直接爲函數的參數指定默認值,只能採用變通的方法。git

function log(x, y) {es6

  y = y || 'World';github

  console.log(x, y);算法

}數據庫

 

log('Hello') // Hello World編程

log('Hello', 'China') // Hello China數組

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

上面代碼檢查函數log的參數y有沒有賦值,若是沒有,則指定默認值爲World。這種寫法的缺點在於,若是參數y賦值了,可是對應的布爾值爲false,則該賦值不起做用。就像上面代碼的最後一行,參數y等於空字符,結果被改成默認值。

爲了不這個問題,一般須要先判斷一下參數y是否被賦值,若是沒有,再等於默認值。

if (typeof y === 'undefined') {

  y = 'World';

}

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

function log(x, y = 'World') {

  console.log(x, y);

}

 

log('Hello') // Hello World

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

log('Hello', '') // Hello

能夠看到,ES6的寫法比ES5簡潔許多,並且很是天然。下面是另外一個例子。

function Point(x = 0, y = 0) {

  this.x = x;

  this.y = y;

}

 

var p = new Point();

p // { x: 0, y: 0 }

除了簡潔,ES6的寫法還有兩個好處:首先,閱讀代碼的人,能夠馬上意識到哪些參數是能夠省略的,不用查看函數體或文檔;其次,有利於未來的代碼優化,即便將來的版本在對外接口中,完全拿掉這個參數,也不會致使之前的代碼沒法運行。

參數變量是默認聲明的,因此不能用let或const再次聲明。

function foo(x = 5) {

  let x = 1; // error

  const x = 2; // error

}

上面代碼中,參數變量x是默認聲明的,在函數體中,不能用let或const再次聲明,不然會報錯。

與解構賦值默認值結合使用

參數默認值能夠與解構賦值的默認值,結合起來使用。

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就不會生成,從而報錯。若是參數對象沒有y屬性,y的默認值5纔會生效。

下面是另外一個對象的解構賦值默認值的例子。

function fetch(url, { body = '', method = 'GET', headers = {} }) {

  console.log(method);

}

 

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

// "GET"

 

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

// 報錯

上面代碼中,若是函數fetch的第二個參數是一個對象,就能夠爲它的三個屬性設置默認值。

上面的寫法不能省略第二個參數,若是結合函數參數的默認值,就能夠省略第二個參數。這時,就出現了雙重默認值。

function fetch(url, { method = 'GET' } = {}) {

  console.log(method);

}

 

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

// "GET"

上面代碼中,函數fetch沒有第二個參數時,函數參數的默認值就會生效,而後纔是解構賦值的默認值生效,變量method纔會取到默認值GET。

再請問下面兩種寫法有什麼差異?

// 寫法一

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]

參數默認值的位置

一般狀況下,定義了默認值的參數,應該是函數的尾參數。由於這樣比較容易看出來,到底省略了哪些參數。若是非尾部的參數設置默認值,實際上這個參數是無法省略的。

// 例一

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,就沒有觸發默認值。

函數的length屬性

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

(function (a) {}).length // 1

(function (a = 5) {}).length // 0

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

上面代碼中,length屬性的返回值,等於函數的參數個數減去指定了默認值的參數個數。好比,上面最後一個函數,定義了3個參數,其中有一個參數c指定了默認值,所以length屬性等於3減去1,最後獲得2。

這是由於length屬性的含義是,該函數預期傳入的參數個數。某個參數指定默認值之後,預期傳入的參數個數就不包括這個參數了。同理,rest參數也不會計入length屬性。

(function(...args) {}).length // 0

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

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

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

做用域

一個須要注意的地方是,若是參數默認值是一個變量,則該變量所處的做用域,與其餘變量的做用域規則是同樣的,即先是當前函數的做用域,而後纔是全局做用域。

var x = 1;

 

function f(x, y = x) {

  console.log(y);

}

 

f(2) // 2

上面代碼中,參數y的默認值等於x。調用時,因爲函數做用域內部的變量x已經生成,因此y等於參數x,而不是全局變量x。

若是調用時,函數做用域內部的變量x沒有生成,結果就會不同。

let x = 1;

 

function f(y = x) {

  let x = 2;

  console.log(y);

}

 

f() // 1

上面代碼中,函數調用時,y的默認值變量x還沒有在函數內部生成,因此x指向全局變量。

若是此時,全局變量x不存在,就會報錯。

function f(y = x) {

  let x = 2;

  console.log(y);

}

 

f() // ReferenceError: x is not defined

下面這樣寫,也會報錯。

var x = 1;

 

function foo(x = x) {

  // ...

}

 

foo() // ReferenceError: x is not defined

上面代碼中,函數foo的參數x的默認值也是x。這時,默認值x的做用域是函數做用域,而不是全局做用域。因爲在函數做用域中,存在變量x,可是默認值在x賦值以前先執行了,因此這時屬於暫時性死區(參見《let和const命令》一章),任何對x的操做都會報錯。

若是參數的默認值是一個函數,該函數的做用域是其聲明時所在的做用域。請看下面的例子。

let foo = 'outer';

 

function bar(func = x => foo) {

  let foo = 'inner';

  console.log(func()); // outer

}

 

bar();

上面代碼中,函數bar的參數func的默認值是一個匿名函數,返回值爲變量foo。這個匿名函數聲明時,bar函數的做用域尚未造成,因此匿名函數裏面的foo指向外層做用域的foo,輸出outer。

若是寫成下面這樣,就會報錯。

function bar(func = () => foo) {

  let foo = 'inner';

  console.log(func());

}

 

bar() // ReferenceError: foo is not defined

上面代碼中,匿名函數裏面的foo指向函數外層,可是函數外層並無聲明foo,因此就報錯了。

下面是一個更復雜的例子。

var x = 1;

function foo(x, y = function() { x = 2; }) {

  var x = 3;

  y();

  console.log(x);

}

 

foo() // 3

上面代碼中,函數foo的參數y的默認值是一個匿名函數。函數foo調用時,它的參數x的值爲undefined,因此y函數內部的x一開始是undefined,後來被從新賦值2。可是,函數foo內部從新聲明瞭一個x,值爲3,這兩個x是不同的,互相不產生影響,所以最後輸出3。

若是將var x = 3的var去除,兩個x就是同樣的,最後輸出的就是2。

var x = 1;

function foo(x, y = function() { x = 2; }) {

  x = 3;

  y();

  console.log(x);

}

 

foo() // 2

應用

利用參數默認值,能夠指定某一個參數不得省略,若是省略就拋出一個錯誤。

function throwIfMissing() {

  throw new Error('Missing parameter');

}

 

function foo(mustBeProvided = throwIfMissing()) {

  return mustBeProvided;

}

 

foo()

// Error: Missing parameter

上面代碼的foo函數,若是調用的時候沒有參數,就會調用默認值throwIfMissing函數,從而拋出一個錯誤。

從上面代碼還能夠看到,參數mustBeProvided的默認值等於throwIfMissing函數的運行結果(即函數名以後有一對圓括號),這代表參數的默認值不是在定義時執行,而是在運行時執行(即若是參數已經賦值,默認值中的函數就不會運行),這與python語言不同。

另外,能夠將參數默認值設爲undefined,代表這個參數是能夠省略的。

function foo(optional = undefined) { ··· }

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參數,能夠向該函數傳入任意數目的參數。

下面是一個rest參數代替arguments變量的例子。

// arguments變量的寫法

function sortNumbers() {

  return Array.prototype.slice.call(arguments).sort();

}

 

// rest參數的寫法

const sortNumbers = (...numbers) => numbers.sort();

上面代碼的兩種寫法,比較後能夠發現,rest參數的寫法更天然也更簡潔。

rest參數中的變量表明一個數組,因此數組特有的方法均可以用於這個變量。下面是一個利用rest參數改寫數組push方法的例子。

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

擴展運算符

含義

擴展運算符(spread)是三個點(...)。它比如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

上面代碼中,array.push(...items)和add(...numbers)這兩行,都是函數的調用,它們的都使用了擴展運算符。該運算符將一個數組,變爲參數序列。

擴展運算符與正常的函數參數能夠結合使用,很是靈活。

function f(v, w, x, y, z) { }

var args = [0, 1];

f(-1, ...args, 2, ...[3]);

替代數組的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 [first, ...rest] = [1, 2, 3, 4, 5];

first // 1

rest  // [2, 3, 4, 5]

 

const [first, ...rest] = [];

first // undefined

rest  // []:

 

const [first, ...rest] = ["foo"];

first  // "foo"

rest   // []

若是將擴展運算符用於數組賦值,只能放在參數的最後一位,不然會報錯。

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

// 報錯

 

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

// 報錯

3)函數的返回值

JavaScript的函數只能返回一個值,若是須要返回多個值,只能返回數組或對象。擴展運算符提供瞭解決這個問題的一種變通方法。

var dateFields = readDateFields(database);

var d = new Date(...dateFields);

上面代碼從數據庫取出一行數據,經過擴展運算符,直接將其傳入構造函數Date。

4)字符串

擴展運算符還能夠將字符串轉爲真正的數組。

[...'hello']

// [ "h", "e", "l", "l", "o" ]

上面的寫法,有一個重要的好處,那就是可以正確識別32位的Unicode字符。

'x\uD83D\uDE80y'.length // 4

[...'x\uD83D\uDE80y'].length // 3

上面代碼的第一種寫法,JavaScript會將32位Unicode字符,識別爲2個字符,採用擴展運算符就沒有這個問題。所以,正確返回字符串長度的函數,能夠像下面這樣寫。

function length(str) {

  return [...str].length;

}

 

length('x\uD83D\uDE80y') // 3

凡是涉及到操做32位Unicode字符的函數,都有這個問題。所以,最好都用擴展運算符改寫。

let str = 'x\uD83D\uDE80y';

 

str.split('').reverse().join('')

// 'y\uDE80\uD83Dx'

 

[...str].reverse().join('')

// 'y\uD83D\uDE80x'

上面代碼中,若是不用擴展運算符,字符串的reverse操做就不正確。

5)實現了Iterator接口的對象

任何Iterator接口的對象,均可以用擴展運算符轉爲真正的數組。

var nodeList = document.querySelectorAll('div');

var array = [...nodeList];

上面代碼中,querySelectorAll方法返回的是一個nodeList對象。它不是數組,而是一個相似數組的對象。這時,擴展運算符能夠將其轉爲真正的數組,緣由就在於NodeList對象實現了Iterator接口。

對於那些沒有部署Iterator接口的相似數組的對象,擴展運算符就沒法將其轉爲真正的數組。

let arrayLike = {

  '0': 'a',

  '1': 'b',

  '2': 'c',

  length: 3

};

 

// TypeError: Cannot spread non-iterable object.

let arr = [...arrayLike];

上面代碼中,arrayLike是一個相似數組的對象,可是沒有部署Iterator接口,擴展運算符就會報錯。這時,能夠改成使用Array.from方法將arrayLike轉爲真正的數組。

6MapSet結構,Generator函數

擴展運算符內部調用的是數據結構的Iterator接口,所以只要具備Iterator接口的對象,均可以使用擴展運算符,好比Map結構。

let map = new Map([

  [1, 'one'],

  [2, 'two'],

  [3, 'three'],

]);

 

let arr = [...map.keys()]; // [1, 2, 3]

Generator函數運行後,返回一個遍歷器對象,所以也可使用擴展運算符。

var go = function*(){

  yield 1;

  yield 2;

  yield 3;

};

 

[...go()] // [1, 2, 3]

上面代碼中,變量go是一個Generator函數,執行後返回的是一個遍歷器對象,對這個遍歷器對象執行擴展運算符,就會將內部遍歷獲得的值,轉爲一個數組。

若是對沒有iterator接口的對象,使用擴展運算符,將會報錯。

var obj = {a: 1, b: 2};

let arr = [...obj]; // TypeError: Cannot spread non-iterable object

name屬性

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

function foo() {}

foo.name // "foo"

這個屬性早就被瀏覽器普遍支持,可是直到ES6,纔將其寫入了標準。

須要注意的是,ES6對這個屬性的行爲作出了一些修改。若是將一個匿名函數賦值給一個變量,ES5的name屬性,會返回空字符串,而ES6的name屬性會返回實際的函數名。

var func1 = function () {};

 

// ES5

func1.name // ""

 

// ES6

func1.name // "func1"

上面代碼中,變量func1等於一個匿名函數,ES5和ES6的name屬性返回的值不同。

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

const bar = function baz() {};

 

// ES5

bar.name // "baz"

 

// ES6

bar.name // "baz"

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

(new Function).name // "anonymous"

bind返回的函數,name屬性值會加上「bound 」前綴。

function foo() {};

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

 

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

箭頭函數

基本用法

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;

};

若是箭頭函數的代碼塊部分多於一條語句,就要使用大括號將它們括起來,而且使用return語句返回。

var sum = (num1, num2) => { return num1 + num2; }

因爲大括號被解釋爲代碼塊,因此若是箭頭函數直接返回一個對象,必須在對象外面加上括號。

var getTempItem = id => ({ id: id, name: "Temp" });

箭頭函數能夠與變量解構結合使用。

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

 

// 等同於

function full(person) {

  return person.first + ' ' + person.last;

}

箭頭函數使得表達更加簡潔。

const isEven = n => n % 2 == 0;

const square = n => n * n;

上面代碼只用了兩行,就定義了兩個簡單的工具函數。若是不用箭頭函數,可能就要佔用多行,並且還不如如今這樣寫醒目。

箭頭函數的一個用處是簡化回調函數。

// 正常函數寫法

[1,2,3].map(function (x) {

  return x * x;

});

 

// 箭頭函數寫法

[1,2,3].map(x => x * x);

另外一個例子是

// 正常函數寫法

var result = values.sort(function (a, b) {

  return a - b;

});

 

// 箭頭函數寫法

var result = values.sort((a, b) => a - b);

下面是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]]

使用注意點

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

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

(2)不能夠看成構造函數,也就是說,不可使用new命令,不然會拋出一個錯誤。

(3)不可使用arguments對象,該對象在函數體內不存在。若是要用,能夠用Rest參數代替。

(4)不可使用yield命令,所以箭頭函數不能用做Generator函數。

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

function foo() {

  setTimeout(() => {

    console.log('id:', this.id);

  }, 100);

}

 

var id = 21;

 

foo.call({ id: 42 });

// id: 42

上面代碼中,setTimeout的參數是一個箭頭函數,這個箭頭函數的定義生效是在foo函數生成時,而它的真正執行要等到100毫秒後。若是是普通函數,執行時this應該指向全局對象window,這時應該輸出21。可是,箭頭函數致使this老是指向函數定義生效時所在的對象(本例是{id: 42}),因此輸出的是42。

箭頭函數可讓setTimeout裏面的this,綁定定義時所在的做用域,而不是指向運行時所在的做用域。下面是另外一個例子。

function Timer() {

  this.s1 = 0;

  this.s2 = 0;

  // 箭頭函數

  setInterval(() => this.s1++, 1000);

  // 普通函數

  setInterval(function () {

    this.s2++;

  }, 1000);

}

 

var timer = new Timer();

 

setTimeout(() => console.log('s1: ', timer.s1), 3100);

setTimeout(() => console.log('s2: ', timer.s2), 3100);

// s1: 3

// s2: 0

上面代碼中,Timer函數內部設置了兩個定時器,分別使用了箭頭函數和普通函數。前者的this綁定定義時所在的做用域(即Timer函數),後者的this指向運行時所在的做用域(即全局對象)。因此,3100毫秒以後,timer.s1被更新了3次,而timer.s2一次都沒更新。

箭頭函數可讓this指向固定化,這種特性頗有利於封裝回調函數。下面是一個例子,DOM事件的回調函數封裝在一個對象裏面。

var handler = {

  id: '123456',

 

  init: function() {

    document.addEventListener('click',

      event => this.doSomething(event.type), false);

  },

 

  doSomething: function(type) {

    console.log('Handling ' + type  + ' for ' + this.id);

  }

};

上面代碼的init方法中,使用了箭頭函數,這致使這個箭頭函數裏面的this,老是指向handler對象。不然,回調函數運行時,this.doSomething這一行會報錯,由於此時this指向document對象。

this指向的固定化,並非由於箭頭函數內部有綁定this的機制,實際緣由是箭頭函數根本沒有本身的this,致使內部的this就是外層代碼塊的this。正是由於它沒有this,因此也就不能用做構造函數。

因此,箭頭函數轉成ES5的代碼以下。

// ES6

function foo() {

  setTimeout(() => {

    console.log('id:', this.id);

  }, 100);

}

 

// ES5

function foo() {

  var _this = this;

 

  setTimeout(function () {

    console.log('id:', _this.id);

  }, 100);

}

上面代碼中,轉換後的ES5版本清楚地說明了,箭頭函數裏面根本沒有本身的this,而是引用外層的this。

請問下面的代碼之中有幾個this?

function foo() {

  return () => {

    return () => {

      return () => {

        console.log('id:', this.id);

      };

    };

  };

}

 

var f = foo.call({id: 1});

 

var t1 = f.call({id: 2})()(); // id: 1

var t2 = f().call({id: 3})(); // id: 1

var t3 = f()().call({id: 4}); // id: 1

上面代碼之中,只有一個this,就是函數foo的this,因此t一、t二、t3都輸出一樣的結果。由於全部的內層函數都是箭頭函數,都沒有本身的this,它們的this其實都是最外層foo函數的this。

除了this,如下三個變量在箭頭函數之中也是不存在的,指向外層函數的對應變量:arguments、super、new.target。

function foo() {

  setTimeout(() => {

    console.log('args:', arguments);

  }, 100);

}

 

foo(2, 4, 6, 8)

// args: [2, 4, 6, 8]

上面代碼中,箭頭函數內部的變量arguments,實際上是函數foo的arguments變量。

另外,因爲箭頭函數沒有本身的this,因此固然也就不能用call()、apply()、bind()這些方法去改變this的指向。

(function() {

  return [

    (() => this.x).bind({ x: 'inner' })()

  ];

}).call({ x: 'outer' });

// ['outer']

上面代碼中,箭頭函數沒有本身的this,因此bind方法無效,內部的this指向外部的this。

長期以來,JavaScript語言的this對象一直是一個使人頭痛的問題,在對象方法中使用this,必須很是當心。箭頭函數」綁定」this,很大程度上解決了這個困擾。

嵌套的箭頭函數

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

下面是一個部署管道機制(pipeline)的例子,即前一個函數的輸出是後一個函數的輸入。

const pipeline = (...funcs) =>

  val => funcs.reduce((a, b) => b(a), val);

 

const plus1 = a => a + 1;

const mult2 = a => a * 2;

const addThenMult = pipeline(plus1, mult2);

 

addThenMult(5)

// 12

若是以爲上面的寫法可讀性比較差,也能夠採用下面的寫法。

const plus1 = a => a + 1;

const mult2 = a => a * 2;

 

mult2(plus1(5))

// 12

箭頭函數還有一個功能,就是能夠很方便地改寫λ演算。

// λ演算的寫法

fix = λf.(λx.f(λv.x(x)(v)))(λx.f(λv.x(x)(v)))

 

// ES6的寫法

var fix = f => (x => f(v => x(x)(v)))

               (x => f(v => x(x)(v)));

上面兩種寫法,幾乎是一一對應的。因爲λ演算對於計算機科學很是重要,這使得咱們能夠用ES6做爲替代工具,探索計算機科學。

函數綁定

箭頭函數能夠綁定this對象,大大減小了顯式綁定this對象的寫法(call、apply、bind)。可是,箭頭函數並不適用於全部場合,因此ES7提出了「函數綁定」(function bind)運算符,用來取代call、apply、bind調用。雖然該語法仍是ES7的一個提案,可是Babel轉碼器已經支持。

函數綁定運算符是並排的兩個雙冒號(::),雙冒號左邊是一個對象,右邊是一個函數。該運算符會自動將左邊的對象,做爲上下文環境(即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));

 

// 例二

let { find, html } = jake;

 

document.querySelectorAll("div.myClass")

::find("p")

::html("hahaha");

尾調用優化

什麼是尾調用?

尾調用(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);

}

上面代碼中,函數m和n都屬於尾調用,由於它們都是函數f的最後一步操做。

尾調用優化

尾調用之因此與其餘調用不一樣,就在於它的特殊的調用位置。

咱們知道,函數調用會在內存造成一個「調用記錄」,又稱「調用幀」(call frame),保存調用位置和內部變量等信息。若是在函數A的內部調用函數B,那麼在A的調用幀上方,還會造成一個B的調用幀。等到B運行結束,將結果返回到A,B的調用幀纔會消失。若是函數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就須要保存內部變量m和n的值、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。

尾遞歸

函數調用自身,稱爲遞歸。若是尾調用自身,就稱爲尾遞歸。

遞歸很是耗費內存,由於須要同時保存成千上百個調用幀,很容易發生「棧溢出」錯誤(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中,只要使用尾遞歸,就不會發生棧溢出,相對節省內存。

遞歸函數的改寫

尾遞歸的實現,每每須要改寫遞歸函數,確保最後一步只調用自身。作到這一點的方法,就是把全部用到的內部變量改寫成函數的參數。好比上面的例子,階乘函數 factorial 須要用到一箇中間變量 total ,那就把這個中間變量改寫成函數的參數。這樣作的缺點就是不太直觀,第一眼很難看出來,爲何計算5的階乘,須要傳入兩個參數5和1?

兩個方法能夠解決這個問題。方法一是在尾遞歸函數以外,再提供一個正常形式的函數。

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 變爲只接受1個參數的 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的尾調用優化只在嚴格模式下開啓,正常模式是無效的。

這是由於在正常模式下,函數內部有兩個變量,能夠跟蹤函數的調用棧。

  • func.arguments:返回調用時函數的參數。
  • func.caller:返回調用當前函數的那個函數。

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

function restricted() {

  "use strict";

  restricted.caller;    // 報錯

  restricted.arguments; // 報錯

}

restricted();

尾遞歸優化的實現

尾遞歸優化只在嚴格模式下生效,那麼正常模式下,或者那些不支持該功能的環境中,有沒有辦法也使用尾遞歸優化呢?回答是能夠的,就是本身實現尾遞歸優化。

它的原理很是簡單。尾遞歸之因此須要優化,緣由是調用棧太多,形成溢出,那麼只要減小調用棧,就不會溢出。怎麼作能夠減小調用棧呢?就是採用「循環」換掉「遞歸」。

下面是一個正常的遞歸函數。

function sum(x, y) {

  if (y > 0) {

    return sum(x + 1, y - 1);

  } else {

    return x;

  }

}

 

sum(1, 100000)

// Uncaught RangeError: Maximum call stack size exceeded(…)

上面代碼中,sum是一個遞歸函數,參數x是須要累加的值,參數y控制遞歸次數。一旦指定sum遞歸100000次,就會報錯,提示超出調用棧的最大次數。

蹦牀函數(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函數的每次執行,都會返回自身的另外一個版本。

如今,使用蹦牀函數執行sum,就不會發生調用棧溢出。

trampoline(sum(1, 100000))

// 100001

蹦牀函數並非真正的尾遞歸優化,下面的實現纔是。

function tco(f) {

  var value;

  var active = false;

  var accumulated = [];

 

  return function accumulator() {

    accumulated.push(arguments);

    if (!active) {

      active = true;

      while (accumulated.length) {

        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, 100000)

// 100001

上面代碼中,tco函數是尾遞歸優化的實現,它的奧妙就在於狀態變量active。默認狀況下,這個變量是不激活的。一旦進入尾遞歸優化的過程,這個變量就激活了。而後,每一輪遞歸sum返回的都是undefined,因此就避免了遞歸執行;而accumulated數組存放每一輪sum執行的參數,老是有值的,這就保證了accumulator函數內部的while循環老是會執行。這樣就很巧妙地將「遞歸」改爲了「循環」,然後一輪的參數會取代前一輪的參數,保證了調用棧只有一層。

函數參數的尾逗號

ES7有一個提案,容許函數的最後一個參數有尾逗號(trailing comma)。

目前,函數定義和調用時,都不容許有參數的尾逗號。

function clownsEverywhere(

  param1,

  param2

) { /* ... */ }

 

clownsEverywhere(

  'foo',

  'bar'

);

若是之後要在函數的定義之中添加參數,就勢必還要添加一個逗號。這對版本管理系統來講,就會顯示,添加逗號的那一行也發生了變更。這看上去有點冗餘,所以新提案容許定義和調用時,尾部直接有一個逗號。

function clownsEverywhere(

  param1,

  param2,

) { /* ... */ }

 

clownsEverywhere(

  'foo',

  'bar',

);

 轉載於   阮一峯的 博客

相關文章
相關標籤/搜索