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
參數只能是最後一個參數,不然會報錯。bash
const foo = (a, ...rest, b) => {}
// Uncaught SyntaxError: Rest parameter must be last formal parameter
複製代碼
ES6 容許爲函數的參數設置默認值,即直接寫在參數定義的後面。app
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
複製代碼
這種寫法有兩個好處:函數式編程
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
複製代碼
若是調用函數時沒有給參數就會報錯.
經過提供函數參數的默認值,就能夠避免這種狀況。ui
function foo({x, y = 5} = {}) {
console.log(x, y);
}
foo() // undefined 5
複製代碼
要注意函數解構設置默認值的寫。
如下有兩種寫法:this
// 寫法一
function m1({x = 0, y = 0} = {}) {
return [x, y];
}
// 寫法二
function m2({x, y} = { x: 0, y: 0 }) {
return [x, y];
}
複製代碼
兩個都是給了默認值,可是是有區別的:spa
// 函數沒有參數的狀況
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
複製代碼
x
和 y
的值不受外界影響。函數體內的值也優先爲頭部的值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;
};
}());
複製代碼
特色:
=>
來定義函數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]
複製代碼
this
對象,就是定義時所在的對象(他的外層對象),而不是使用時所在的對象。this
對象。arguments
對象,該對象在函數體內不存在。若是要用,能夠用 rest
參數代替。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
,如下三個變量在箭頭函數之中也是不存在的,指向外層函數的對應變量:arguments
、super
、new.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
屬性將返回該函數預期傳入的參數個數。某個參數指定默認值之後,預期傳入的參數個數就不包括這個參數了。同理,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)的時候,其過程是這樣的:
整個計算過程是線性的,調用一次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
複製代碼