一、函數的擴展
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
,然後聲明瞭變量y
,y
的默認值是一個匿名函數。這個匿名函數內部的變量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;
}