1 函數參數的默認值
ES6之前,不能直接爲函數的參數指定默認值,方法如下:
function log(x, y) {
y = y || 'World';
console.log(x, y);
}
log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello World
ES6允許爲函數的參數設置默認值,即直接寫在參數定義的後面
function log(x, y = 'World') {
console.log(x, y);
}
log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello
參數變量是默認聲明的,所以不能用let或const再次聲明
function foo(x = 5) {
let x = 1; // error
}
注意: 參數默認值不是傳值的,而是每次都重新計算默認值表達式的值。
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
以上代碼只使用了對象的解構賦值默認值,沒有使用函數參數的默認值。只有當函數foo的參數是一個對象時,變量x和y纔會通過解構賦值生成。如果函數foo調用時沒提供參數,變量x和y就不會生成,從而報錯。通過提供函數參數的默認值,就可以避免這種情況。
function foo({x, y = 5} = {}) {
console.log(x, y);
}
foo() // undefined 5
解構賦值的例子:
function fetch(url, { body = '', method = 'GET', headers = {} }) {
console.log(method);
}
fetch('http://example.com', {})
// "GET"
fetch('http://example.com')
// error
雙重默認值:
function fetch(url, { body = '', method = 'GET', headers = {} } = {}) {
console.log(method);
}
fetch('http://example.com')
// "GET"
上面的代碼中,函數fetch沒有第二個參數時,函數參數的默認值就會生效,然後纔是解構賦值的默認值生效,變量method纔會取到默認值GET。
函數的length屬性
指定了默認值之後,函數的length屬性,將返回沒有指定默認值的參數個數。也就是說,指定了默認值後,length屬性將失真。
(function (a) {}).length // 1
(function (a = 5) {}).length // 0
(function (a, b, c = 5) {}).length // 2
作用域
一旦設置了參數的默認值,函數進行聲明初始化時,參數會形成一個單獨的作用於。等到初始化結束,這個作用域就會消失。這種語法行爲,在不設置參數默認值時,是不會出現的。
var x = 1;
function f(x, y = x) {
console.log(y);
}
f(2) // 2
上面代碼中,參數y的默認值等於變量x。調用函數f時 ,參數形成一個單獨的作用域。在這個作用域裏面,默認值變量x指向第一個參數x,而不是全局變量x,所以輸出是2。
let x = 1;
function f(y = x) {
let x = 2;
console.log(y);
}
f() // 1
上面代碼中,函數f調用時,參數y=x形成一個單獨的作用於。這個作用域裏面,變量x本身沒有定義,所以指向外層的全局變量x。函數調用時,函數體內部的局部變量x影響不到默認值變量x。
應用
利用參數默認值,可以指定某一個參數不得省略,如果省略就拋出一個錯誤。
function throwIfMissing() {
throw new Error('Missing parameter');
}
function foo(mustBeProvided = throwIfMissing()) {
return mustBeProvided;
}
foo()
// Error: Missing parameter
上面代碼,如果調用的時候沒有參數,就會調用默認值throwIfMissing函數,從而拋出一個錯誤。
從上面代碼還可以看到,參數mustBeprovided的默認值等於throwIfMissing函數的運行結果,這表明參數的默認值不是在定義時執行,而是在運行時執行。如果參數已經賦值,默認值中的函數就不會運行。
2 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參數,可以向該函數傳入任意數目的參數。
// error
function f(a, ...b, c) {
// ...
}
注意: rest參數之後不能再有其他參數(即只能是最後一個參數),否則會報錯。
3 name屬性
函數的name屬性,返回該函數的函數。
function foo() {}
foo.name // "foo"
注意: 如果將一個匿名函數賦值給一個變量,ES5的name屬性,會返回空字符串,而ES6的name屬性會返回實際的函數名。
var f = function () {};
// ES5
f.name // ""
// ES6 當然現在的瀏覽器都已經支持返回"f"
f.name // "f"
將一個具名函數賦值給一個變量,則ES5和ES6的name屬性都返回這個具名函數原本的名字。
const bar = function baz() {};
// ES5
bar.name // "baz"
// ES6
bar.name // "baz"
箭頭函數
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;
}
箭頭函數可以與變量解構結合使用
const full = ({first, last} => first + ' ' + last);
// 等同於
function full(person) {
return person.first + ' ' + person.last;
}
一個用處: 箭頭函數簡化回調函數
[1, 2, 3].map(function (x) {
return x * x;
})
// 箭頭函數寫法
[1, 2, 3].map(x => x * x);
尾調用
尾調用是函數式編程的一個重要概念,本身非常簡單,就是指某個函數的最後一步是調用另一個函數。
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的最後一步操作。
尾調用優化
函數調用會在內存形成一個”調用記錄”,又稱爲”調用幀”,保存調用位置和內部變量等信息。如果在函數A的內部調用函數B,那麼在A的調用幀上方,會形成一個B的調用幀。等到B運行結束,將結果返回到A,B的調用 幀纔會消失。如果函數B內部還調用函數C,那就還有一個C的調用幀,以此類推。所有的調用幀,就形成一個”調用棧”
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)的調用幀。
這就叫做”尾調用優化”,即只保留內層函數的調用幀。如果所有函數 都是尾調用,那麼完全可以做到每次執行時,調用幀只有一項,這將大大節省內存。這就是 “尾調用優化”的意義。
尾遞歸
函數調用自身,稱爲遞歸。如果尾調用自身,就成爲尾遞歸。
遞歸非常耗費內存,因爲需要同時保存成千上百個調用幀,很容易發生”棧溢出”錯誤。但是對於尾遞歸來說,由於只存在一個調用幀,所以永遠不會發生”棧溢出”錯誤。
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
ES6明確規定,所有ECMAScript的實現,都必須部署”尾調用優化”,這就是說,ES6中只要使用尾遞歸,就不會發生棧溢出,相對節省內存。
本博客內容摘抄自
阮一峯老師寫的ECMAScript6入門一書