7.1.1 基本用法
在ES6之前,不能直接為函數的參數指定默認值,只能采用變通的方法。
function log(x.y){
y = y || 'World'l
console.log(x,y);
}log('hello') //hello World
log('hello','Chine') //hello Chine
log('hello','') //hello World
上面的代碼檢查函數log的參數y有沒有賦值,如果沒有,則指定默認值為World。這種寫法的缺點在于,如果參數y賦值了,但是對應的布爾值為false,則該賦值不起作用。就像以上代碼的最后一行,參數y就等于空字符,結果被改為默認值。
為了避免這個問題嗎,通常需要先判斷一下參數y是否被賦值,如果沒有,再令其等于默認值。
if(typeof y === 'undefined'){
y = 'world';
}
ES6允許為函數的參數設置默認值,即直接寫在參數定義的后面。
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,x,y = 1){
//...
}
// SyntaxError:
另外一個容易忽略的地方是,參數默認值不是傳值的,而是每次都重新計算默認值表達式的值,也就是說,參數默認值是惰性求值的。
function foo(p = x + 1){
console.log(p);
}
foo()//100
x = 100;
foo() //101
上面代碼中,參數p默認值是x+1。這時,每次調用函數foo都會重新計算,而不是默認p等于100。
7.1.2 與解構賦值默認值結合使用
參數默認值可以與解構賦值的默認值結合起來使用。
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:
上面的代碼使用了對象的解構賦值默認值,而沒有使用函數參數的默認值,只有當函數foo的參數是一個對象時,變量x和y才會通過解構賦值而生成。如果函數foo調用時參數不是對象,變量x和y就不會生成,從而報錯。只有參數對象沒有y屬性時,y的默認值5才會生效。
下面是另一個對象的解構賦值默認值的例子。
function fetch(url,{ bodu = '',method = 'GET',headers = {}}){
console.log(method):
}
fetch('http://example.com',{})
//“GET”
fetch('http://example.com')
//報錯
上面的代碼中,如果函數fetch的第二個參數是一個對象,就可以為他的3個屬性設置默認值。
上面的寫法不能省略第二個參數,如果結合函數參數的默認值,就可以省略第二個參數,這時就出現了雙重默認值。
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({}) //[0,0]
m2({x:3}) //[3,undefined]//x和y都無值的情況
m1({}) //[0,0]
m2({}) //[undefined,undefined]
m1({z:3}) //[0,0]
m2({z:3}) //[undefined,undefined]
7.1.3 參數默認值的位置
通常情況下,定義了默認值的參數應該是函數的尾參數。因為這樣比較容易看出到底省略了哪些參數。如果非尾部的參數設置默認值,實際上這個參數是無法省略的。
//列一
function f(x = 1,y){return [x, y];}f() //[1,undifned]f(2) //[2,undifned]f(,1) //報錯f(undifned,1) //[1,1]//列一
function f(x ,y = 5,z){return [x, y,z];}f() //[undifned,5,undifned]f(1) //[1,5,undifned]f(1,2) //報錯f(1,undifned,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,沒有觸發默認值。
7.1.4 函數的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。
這時因為lenght屬性的含義是該函數預期傳入的參數個數。某個參數指定默認值以后,預期傳入的參數個數就不包括參數了。同理,rest參數也不會計入length屬性。
(function (...args){}).length //0
如果設置了默認值的參數不是尾參數,那么length屬性也不再計入后面的參數。
(function (a = 0,b,c){}).length //0
(function (a,b = 1,c){}).length //0
7.1.5 作用域
一旦設置了參數的默認值,函數進行聲明初始化時,參數會形成一個單獨的作用域(context)。等到初始化結束,這個作用域就會消失。這種語法行為在不設置參數默認值時是不會出現的。
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。
如果此時全局變量x不存在,就會報錯。
function f(y = x){
let x= 2;
console.log(y);
}
f() // ReferenceError
像下面這樣寫,也會報錯
var x = 1;
function foo(x = x){
//...
}
foo() //ReferenceError
上面的代碼中,參數x=x 形成一個單獨作用域,實際執行的是let x = x。由于暫時性死區,這行這行代碼會產生“定義”錯誤。
如果參數的默認值是一個函數,該函數的作用域也遵守這個規則。請看下面的例子。
let foo = 'outer';function bar(func = x =>foo){let foo = 'inner';console.log(func());
}
bar(); // outer
上面代碼中,函數bar的參數func的默認值是一個匿名函數,返回值為變量foo。函數參數形成的單獨作用域里面并沒有定義變量foo,所以foo指向外層的全局變量foo,因此輸出outer如果寫成下面這樣就會報錯。
function bar(func = ()=>foo){let foo = 'inner';console.log(func());
}
bar() //ReferenceError
上面的代碼中,匿名函數里面的foo指向函數外層,但是函數外層并沒有聲明變量foo,所以報錯。下面是一個更復雜的例子。var x= 1;function foo(x,y = function(){ x = 2;}){var x = 3y();console.log(x);}foo() //3x // 1
上面的代碼中,函數foo的參數形成一個單獨作用域。這個作用域中首先聲明了變量x,然后聲明了變量y。y的默認值是一個匿名函數,這個匿名函數內部的變量x指向同一個作用域的第一個參數x。函數foo內部又聲明了一個內部變量x,該變量與第一個參數x由于不是同一個作用域,所以不是同一個變量,因此執行y后,內部變量x和外部全局變量x的值都沒變。如果將var x = 3 的var去除,函數foo 的內部變量x就指向第一個參數x,與匿名函數內部的x 是一致的,所以最后輸出的就是2,而外層的全局變量x依然不受影響。var x = 1;function foo(x, y = function(){ x = 2;}) {x = 3;y();console.log(x);}foo() //2x // 1
7.1.6 應用
利用參數默認值可以指定某一個參數不得省略,如果省略就拋出一個錯誤。
function throwIfMissing() {
throw new Error('Missing parameter');
}
function foo(musBeProvied = throwIfMissing()){return mustBeProvided;}
foo()//Error:Missing parameter
如果調用的時候沒有參數,以上代碼中的foo函數就會調用默認值throwIfMissing函數,從而拋出一個錯誤。從上面的代碼還可以看到,參數mustBeProvided的默認值等于throwIfMissing函數的運行結果(即函數名之后有一對圓括號),這表明參數的默認值不是定義是執行,而是在運行時執行。如果參數已經賦值,默認值中的函數就不會運行。另外,可以見參數默認值設為undefined,這表明這個參數是可以省略的。
function foo(optional = undefined) { ... }
7.2 rest參數
ES6引入了rest參數(形式為“…變量名”),用于獲取函數的多余參數,這樣就不需要使用arguments對象了。rest參數搭配的變量是一個數組,該變量將多余的參數放入其中。
function add(...values){let sun = 0;for(var val of values) {sum += val;}return sunm;
}
add(2,5,3) //10以上代碼中的add函數是一個求和函數,利用rest參數可以向該函數傳入任意數目的參數。下面是一個rest參數代替arguments變量的例子。//arguments變量的寫法
function sortNumbers() {return Arrayt.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 //0
7.3 嚴格模式
從ES5開始,函數內部可以設定為嚴格模式。
function doSomething(a,b) {'use strict'//code
}
ES2016做了一點修改,規定只要函數參數使用了默認值,解構賦值,或者拓展運算符,那么函數內部就顯示設定為嚴格模式,否則就會報錯。
//報錯
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
}
};
這樣規定的原因是,函數內部的嚴格模式同時適用于函數圖和函數參數。但是,函數執行時,先執行函數參數,然后再執行函數體,這樣就有一個不合理的地方;只有從函數體之中才能知道參數是否應該為嚴格模式執行,但是參數卻應該先于函數體執行。
//報錯
function doSomething(value = 070) {'use strict';return value;
}上面的代碼中,餐宿value的默認值是八進制數070,但是嚴格模式下不能用前綴0表示八進制,所以應該報錯,但是實際上,JavaScript引擎會先成功執行value = 070,然后進入函數體內部,發現需要嚴格模式執行時才會報錯。雖然可以先解析函數體代碼,在執行參數代碼,但是這樣無疑增加了復雜性。因此,標準索性禁止了這種用法,只要參數使用了默認值,解構賦值,拓展運算符,就不能顯式指定嚴格模式。有兩種方法可以規避這種限制。第一種是設定全局性嚴格模式,這是合法的。'use struct';function doSomething(a,b = a) {//code}第二種是把函數包在一個無參數的立即執行函數里面。
const doSomething = (function() {'use strict'return function(value = 42) {return value;};{());
7.4 name屬性
函數的name屬性返回該函數的函數名。
function foo(){}
foo.name //"foo"
這個屬性早就被瀏覽器廣泛支持,但是直到ES6才寫入了標準。
需要注意的是,ES6對這個屬性的行為做出了一些修改。如果將一個匿名函數賦值給一個變量,ES5的name屬性會返回空字符串,而ES6的name屬性會返回實際的函數名。
var f = function() {};// ES5
f.name //""
//ES6
f.name //"f"上面代碼中,變量func1等于一個匿名函數,ES5和ES6的name屬性返回的值不一樣。如果將一個劇名函數賦值給一個變量,則ES5和ES6的name屬性都返回這個具名函數原本的名字
const bar = function baz() {};
// ES5
var.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"
7.5 箭頭函數
7.5.1 基本用法
ES6允許使用“箭頭”(=>) 定義函數。
var f = v=> v;上面的箭頭函數等同于以下代碼。
var f = function(v) {
return v;
};如果箭頭函數不需要參數或需要多個參數,就使用圓括號代表參數部分。
var f = () => 5;
//等同于
var f = function() {retrun s};var sum = (num1,num2) => num1+num2;
//等同于
var sum = function(num1,num2) {return num1 + num2;
};如果箭頭函數的代碼塊部分多余一條語句,就要使用大括號將其括起來,并使用retrun語句返回。
var sun = (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;number(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]]
7.5.2 注意事項
箭頭函數有以下幾個使用注意事項。
- 函數體內的this對象就是定義是所在的對象,而不是使用時所在的對象。
- 不可以當作構造函數。也就是說,不可以使用new命令,否則會拋出一個錯誤
- 不可以使用arguments對象,該對象在函數體內不存在。如果要用,可以用rest參數代替。
- 不可以使用yield命令,因此箭頭函數不能用作Generatot函數。
其中一點尤其值得注意。this對象的指向是可變的,但箭頭函數中它是固定的,
function foo() {setTimeout(() =>{console.log('id',this.id);},100);}
var id = 21;
foo.call({ id : 42});
//id:42上面的代碼中,setTimeout的參數是一個箭頭函數,這個箭頭函數的定義是在foo函數生成時生效的,而他真正執行要等到100ms后。如果是普通函數,執行時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指向運行時所在的作用域(即全局對象)。所以3100ms之后,thmer.s1被更新了3次,而thimer。說一次都沒更新。箭頭函數可以讓this指向固定化,這種特性非常有利于封裝回調函數。假面是一個例子。DOM事件的回調函數封裝在一個對象里面。var handler = {id:'123456',init:function() {document.addEventListener('click,event => this.doSomething(event.type),false);},doSomething:function(type){console.log('Handing' + type +'for' + this.id);}};以上的代碼的init方法中使用了箭頭函數,這導致箭頭函數里面的this總是指向handler對象。否則,回調函數運行時,this.doSomething一行會報錯,因為此時this指向document對象。this指向的固定化并不是因為箭頭函數內部有綁定this的機制,實際原因是因為箭頭函數根本沒有自己的this,導致內部的this就是外層代碼塊的this。正是因為他沒有this,所以不能用作構造函數。箭頭函數轉成ES5的代碼如下。
//ES5
function foo() {setTimeout(() => {console.log('id:',this.id);},100);
}//ES5
function foo() {var _this = thislsetTimeout(function () {console.log('id:',_this.id);},100);
}上面的代碼中,轉換后的ES5版本清楚地說明了箭頭函數里面根本沒有自己的this,而是引用外層的this。請問下面的代碼之中有幾個this?function foo() [return ()=> {retrun () => {retrun () => {console.log('id:',this.id);};};};}var f = foo.call({id:1});var t1 = f.call({id:2})()();// id:1var t2 = f().call({id:3})();// id:1var t3 = f()().call({id:4}); // id:1上面的代碼中只有一個this,就是函數foo的this,所以t1,t2,t3都輸出同樣的結果。因為所有的內層函數都是箭頭函數,都沒有自己的this,他們的this其實就是最外層foo函數的this。除了this,以下3個變量在箭頭函數中也是不存在的,分別指向外層函數的對應變量:arguments,super和new.targetfunction foo() {setTimeout(() => {console.log('args:',arguments);},100);}
foo(2,4,5,8)
//args:[2,4,6,8]上面的代碼中,箭頭函數內部的變量arguments其實就是函數foo的arguments變量。另外,由于箭頭函數沒有自己的this,當然也就不用call(),apply(),bind()這些方法去改變this的指向。
(function() {retrun [(() => this.x).bind({ x: 'inner' })()];}).call({ x:'outer'});//['outer']上面代碼中,箭頭函數沒有自己的this,所以bind方法無效,內部的this只想外部的this。長期以來,JavaScript 語言的this對象一直是一個令人頭痛的問題,在對象方法中使用this必須非常小心。箭頭函數“綁定”this,很大程度上解決了這個困擾。
7.5.3 嵌套的箭頭函數
箭頭函數內部還可以在使用箭頭函數。下面是一個ES5語法的多重嵌套函數。
function insert(value) {return {into:function (array) {return {after:function (afterValue) }array.splice(arrat.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
7.6 綁定this
箭頭函數可以綁定this對象,大大減少了顯式綁定this對象的寫法(call,apply,bind).但是,箭頭函數并非適用于所有場合,所以ES7提出“函數綁定”(function bind )運算符,用來取代call,apply,bind調用。
函數綁定運算符是并排的雙冒號( ::),雙冒號左邊是一個對象,右邊是一個函數。該運算符會自動將左邊的對象作為上下文環境(即this對象)綁定到右邊函數上
foo::bar;//等同于bar.bind(foo)foo::bar(...arguments);//等同于bar.apply(foo,arguments);
const hasOwnPropery = 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"):
7.7 尾調用優化
7.7.1 什么是尾調用
尾調用(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)}retrun n(x);
}
上面的代碼中,函數m和n都屬于尾調用,因為他們都是函數f的最后一步操作。
7.7.2 尾調用優化
尾調用之所以與其他調用不同,就在于其特殊的調用位置。
我們知道,函數調用會在內存形成一個**“調用記錄”又稱“調用幀”**,保存調用位置和內存變量等信息。如果在函數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 addDne(a){var one = 1;funciton inner(b) {return b+one;}return inner(a);
}
上面的函數不會進行尾調用優化,因為內層函數inner用到了外層函數addOne的內部變量one。
7.7.3 尾遞歸
函數調用自身稱為遞歸,如果尾調用自身就稱尾遞歸。
遞歸非常耗費內存,因為需要同時保存成百上千個調用幀。很容易發生棧溢出錯誤。但對于尾遞歸來說由于只存在一個調用幀,所以永遠不會發生“棧溢出”錯誤。
function factcrial(n) {if(n=== 2) return 1;return n * factoral(n - 1);
}
factorial(5)// 120上面的代碼是一個階乘函數,計算n的階乘,最多需要保存n個調用記錄,復雜度尾O(n).
如果改寫成尾調用遞歸,只保留一個調用記錄,則復雜度尾O(n);function factorial (n,total) {if (n === 1) return total;return factorial(n = 1,n* total);
}
factorial(5,1) //120