ES6 學(xué)習(xí)筆記-函數(shù)的擴(kuò)展

參數(shù)默認(rèn)值不是傳值的出嘹,而是每次都重新計(jì)算默認(rèn)值表達(dá)式的值唯咬。也就是說纱注,參數(shù)默認(rèn)值是惰性求值的。

let x = 99;
function foo(p = x + 1) {
  console.log(p);
}

foo() // 100

x = 100;
foo() // 101

使用參數(shù)默認(rèn)值時胆胰,函數(shù)不能有同名參數(shù)狞贱。

// 不報(bào)錯
function foo(x, x, y) {
  // ...
}

// 報(bào)錯
function foo(x, x, y = 1) {
  // ...
}
// SyntaxError: Duplicate parameter name not allowed in this context

同名參數(shù)只會取最后的參數(shù)名

作用域

一旦設(shè)置了參數(shù)的默認(rèn)值,函數(shù)進(jìn)行聲明初始化時蜀涨,參數(shù)會形成一個單獨(dú)的作用域(context)瞎嬉。等到初始化結(jié)束,這個作用域就會消失厚柳。這種語法行為氧枣,在不設(shè)置參數(shù)默認(rèn)值時,是不會出現(xiàn)的别垮。

var x = 1;

function f(x, y = x) {
  console.log(y);
}

f(2) // 2

上面代碼中便监,參數(shù)y的默認(rèn)值等于變量x。調(diào)用函數(shù)f時,參數(shù)形成一個單獨(dú)的作用域烧董。在這個作用域里面毁靶,默認(rèn)值變量x指向第一個參數(shù)x,而不是全局變量x逊移,所以輸出是2预吆。

再看下面的例子。

let x = 1;

function f(y = x) {
  let x = 2;
  console.log(y);
}

f() // 1

上面代碼中胳泉,函數(shù)f調(diào)用時拐叉,參數(shù)y = x形成一個單獨(dú)的作用域。這個作用域里面胶背,變量x本身沒有定義巷嚣,所以指向外層的全局變量x。函數(shù)調(diào)用時钳吟,函數(shù)體內(nèi)部的局部變量x影響不到默認(rèn)值變量x廷粒。

如果此時,全局變量x不存在红且,就會報(bào)錯坝茎。

function f(y = x) {
  let x = 2;
  console.log(y);
}

f() // ReferenceError: x is not defined

下面這樣寫,也會報(bào)錯

var x = 1;

function foo(x = x) {
  // ...
}

foo() // ReferenceError: x is not defined

上面代碼中暇番,參數(shù)x = x形成一個單獨(dú)作用域嗤放。實(shí)際執(zhí)行的是let x = x,由于暫時性死區(qū)的原因壁酬,這行代碼會報(bào)錯”x 未定義“次酌。

應(yīng)用

利用參數(shù)默認(rèn)值,可以指定某一個參數(shù)不得省略舆乔,如果省略就拋出一個錯誤岳服。

function throwIfMissing() {
  throw new Error('Missing parameter');
}

function foo(mustBeProvided = throwIfMissing()) {
  return mustBeProvided;
}

foo()
// Error: Missing parameter

上面代碼的foo函數(shù),如果調(diào)用的時候沒有參數(shù)希俩,就會調(diào)用默認(rèn)值throwIfMissing函數(shù)吊宋,從而拋出一個錯誤。

從上面代碼還可以看到颜武,參數(shù)mustBeProvided的默認(rèn)值等于throwIfMissing函數(shù)的運(yùn)行結(jié)果(注意函數(shù)名throwIfMissing之后有一對圓括號)璃搜,這表明參數(shù)的默認(rèn)值不是在定義時執(zhí)行,而是在運(yùn)行時執(zhí)行鳞上。如果參數(shù)已經(jīng)賦值这吻,默認(rèn)值中的函數(shù)就不會運(yùn)行

rest 參數(shù)

ES6 引入 rest 參數(shù)(形式為...變量名),用于獲取函數(shù)的多余參數(shù)因块,這樣就不需要使用arguments對象了橘原。rest 參數(shù)搭配的變量是一個數(shù)組,該變量將多余的參數(shù)放入數(shù)組中。

function add(...values) {
  let sum = 0;

  for (var val of values) {
    sum += val;
  }

  return sum;
}

add(2, 5, 3) // 10

箭頭函數(shù)

ES6 允許使用“箭頭”(=>)定義函數(shù)趾断。

var f = v => v;

等同于:

var f = function(v) {
  return v;
};

如果箭頭函數(shù)不需要參數(shù)或需要多個參數(shù)拒名,就使用一個圓括號代表參數(shù)部分。

var f = () => 5;
// 等同于
var f = function () { return 5 };

var sum = (num1, num2) => num1 + num2;
// 等同于
var sum = function(num1, num2) {
  return num1 + num2;
};

如果箭頭函數(shù)的代碼塊部分多于一條語句芋酌,就要使用大括號將它們括起來增显,并且使用return語句返回

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

由于大括號被解釋為代碼塊,所以如果箭頭函數(shù)直接返回一個對象脐帝,必須在對象外面加上括號同云,否則會報(bào)錯。

// 報(bào)錯
let getTempItem = id => { id: id, name: "Temp" };

// 不報(bào)錯
let getTempItem = id => ({ id: id, name: "Temp" });

如果箭頭函數(shù)只有一行語句堵腹,且不需要返回值炸站,可以采用下面的寫法,就不用寫大括號了疚顷。

let fn = () => void doesNotReturn();

箭頭函數(shù)可以與變量解構(gòu)結(jié)合使用旱易。

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

// 等同于
function full(person) {
  return person.first + ' ' + person.last;
}

使用注意點(diǎn)

箭頭函數(shù)有幾個使用注意點(diǎn)。

(1)函數(shù)體內(nèi)的this對象腿堤,就是定義時所在的對象阀坏,而不是使用時所在的對象。

(2)不可以當(dāng)作構(gòu)造函數(shù)笆檀,也就是說忌堂,不可以使用new命令,否則會拋出一個錯誤酗洒。

(3)不可以使用arguments對象士修,該對象在函數(shù)體內(nèi)不存在。如果要用樱衷,可以用 rest 參數(shù)代替李命。

(4)不可以使用yield命令,因此箭頭函數(shù)不能用作 Generator 函數(shù)

上面四點(diǎn)中箫老,第一點(diǎn)尤其值得注意。this對象的指向是可變的黔州,但是在箭頭函數(shù)中耍鬓,它是固定的

function foo() {
  setTimeout(() => {
    console.log('id:', this.id);
  }, 100);
}

var id = 21;

foo.call({ id: 42 });
// id: 42

上面代碼中,setTimeout的參數(shù)是一個箭頭函數(shù)流妻,這個箭頭函數(shù)的定義生效是在foo函數(shù)生成時牲蜀,而它的真正執(zhí)行要等到 100 毫秒后。如果是普通函數(shù)绅这,執(zhí)行時this應(yīng)該指向全局對象window涣达,這時應(yīng)該輸出21。但是,箭頭函數(shù)導(dǎo)致this總是指向函數(shù)定義生效時所在的對象(本例是{id: 42})度苔,所以輸出的是42匆篓。

箭頭函數(shù)可以讓setTimeout里面的this,綁定定義時所在的作用域寇窑,而不是指向運(yùn)行時所在的作用域鸦概。下面是另一個例子。

function Timer() {
  this.s1 = 0;
  this.s2 = 0;
  // 箭頭函數(shù)
  setInterval(() => this.s1++, 1000);
  // 普通函數(shù)
  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函數(shù)內(nèi)部設(shè)置了兩個定時器窗市,分別使用了箭頭函數(shù)和普通函數(shù)。前者的this綁定定義時所在的作用域(即Timer函數(shù))饮笛,后者的this指向運(yùn)行時所在的作用域(即全局對象)咨察。所以,3100 毫秒之后福青,timer.s1被更新了 3 次摄狱,而timer.s2一次都沒更新。

箭頭函數(shù)可以讓this指向固定化素跺,這種特性很有利于封裝回調(diào)函數(shù)二蓝。下面是一個例子,DOM 事件的回調(diào)函數(shù)封裝在一個對象里面指厌。

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方法中刊愚,使用了箭頭函數(shù),這導(dǎo)致這個箭頭函數(shù)里面的this踩验,總是指向handler對象鸥诽。否則,回調(diào)函數(shù)運(yùn)行時箕憾,this.doSomething這一行會報(bào)錯牡借,因?yàn)榇藭rthis指向document對象。

this指向的固定化袭异,并不是因?yàn)榧^函數(shù)內(nèi)部有綁定this的機(jī)制钠龙,實(shí)際原因是箭頭函數(shù)根本沒有自己的this,導(dǎo)致內(nèi)部的this就是外層代碼塊的this御铃。正是因?yàn)樗鼪]有this碴里,所以也就不能用作構(gòu)造函數(shù)。

請問下面的代碼之中有幾個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,就是函數(shù)foo的this睡互,所以t1根竿、t2陵像、t3都輸出同樣的結(jié)果。因?yàn)樗械膬?nèi)層函數(shù)都是箭頭函數(shù)寇壳,都沒有自己的this醒颖,它們的this其實(shí)都是最外層foo函數(shù)的this。

除了this九巡,以下三個變量在箭頭函數(shù)之中也是不存在的图贸,指向外層函數(shù)的對應(yīng)變量:arguments、super冕广、new.target疏日。

什么是尾調(diào)用?

尾調(diào)用(Tail Call)是函數(shù)式編程的一個重要概念,本身非常簡單撒汉,一句話就能說清楚沟优,就是指某個函數(shù)的最后一步是調(diào)用另一個函數(shù)。

function f(x){
  return g(x);
}

上面代碼中睬辐,函數(shù)f的最后一步是調(diào)用函數(shù)g挠阁,這就叫尾調(diào)用。

以下三種情況溯饵,都不屬于尾調(diào)用侵俗。

// 情況一
function f(x){
  let y = g(x);
  return y;
}

// 情況二
function f(x){
  return g(x) + 1;
}

// 情況三
function f(x){
  g(x);
}

上面代碼中,情況一是調(diào)用函數(shù)g之后丰刊,還有賦值操作隘谣,所以不屬于尾調(diào)用,即使語義完全一樣啄巧。情況二也屬于調(diào)用后還有操作寻歧,即使寫在一行內(nèi)。情況三等同于下面的代碼秩仆。

function f(x){
  g(x);
  return undefined;
}

尾調(diào)用不一定出現(xiàn)在函數(shù)尾部码泛,只要是最后一步操作即可。

function f(x) {
  if (x > 0) {
    return m(x)
  }
  return n(x);
}

尾調(diào)用優(yōu)化

尾調(diào)用之所以與其他調(diào)用不同澄耍,就在于它的特殊的調(diào)用位置噪珊。

我們知道,函數(shù)調(diào)用會在內(nèi)存形成一個“調(diào)用記錄”齐莲,又稱“調(diào)用幀”(call frame)卿城,保存調(diào)用位置和內(nèi)部變量等信息。如果在函數(shù)A的內(nèi)部調(diào)用函數(shù)B铅搓,那么在A的調(diào)用幀上方,還會形成一個B的調(diào)用幀搀捷。等到B運(yùn)行結(jié)束星掰,將結(jié)果返回到A多望,B的調(diào)用幀才會消失。如果函數(shù)B內(nèi)部還調(diào)用函數(shù)C氢烘,那就還有一個C的調(diào)用幀怀偷,以此類推。所有的調(diào)用幀播玖,就形成一個“調(diào)用椬倒ぃ”(call stack)。

尾調(diào)用由于是函數(shù)的最后一步操作蜀踏,所以不需要保留外層函數(shù)的調(diào)用幀维蒙,因?yàn)檎{(diào)用位置、內(nèi)部變量等信息都不會再用到了果覆,只要直接用內(nèi)層函數(shù)的調(diào)用幀颅痊,取代外層函數(shù)的調(diào)用幀就可以了。

function f() {
  let m = 1;
  let n = 2;
  return g(m + n);
}
f();

// 等同于
function f() {
  return g(3);
}
f();

// 等同于
g(3);

上面代碼中局待,如果函數(shù)g不是尾調(diào)用斑响,函數(shù)f就需要保存內(nèi)部變量m和n的值、g的調(diào)用位置等信息钳榨。但由于調(diào)用g之后舰罚,函數(shù)f就結(jié)束了,所以執(zhí)行到最后一步薛耻,完全可以刪除f(x)的調(diào)用幀营罢,只保留g(3)的調(diào)用幀。

這就叫做“尾調(diào)用優(yōu)化”(Tail call optimization)昭卓,即只保留內(nèi)層函數(shù)的調(diào)用幀愤钾。如果所有函數(shù)都是尾調(diào)用,那么完全可以做到每次執(zhí)行時候醒,調(diào)用幀只有一項(xiàng)能颁,這將大大節(jié)省內(nèi)存。這就是“尾調(diào)用優(yōu)化”的意義倒淫。

注意伙菊,只有不再用到外層函數(shù)的內(nèi)部變量,內(nèi)層函數(shù)的調(diào)用幀才會取代外層函數(shù)的調(diào)用幀敌土,否則就無法進(jìn)行“尾調(diào)用優(yōu)化”

function addOne(a){
  var one = 1;
  function inner(b){
    return b + one;
  }
  return inner(a);
}

上面的函數(shù)不會進(jìn)行尾調(diào)用優(yōu)化镜硕,因?yàn)閮?nèi)層函數(shù)inner用到了外層函數(shù)addOne的內(nèi)部變量one

尾遞歸

函數(shù)調(diào)用自身,稱為遞歸返干。如果尾調(diào)用自身兴枯,就稱為尾遞歸。

遞歸非常耗費(fèi)內(nèi)存矩欠,因?yàn)樾枰瑫r保存成千上百個調(diào)用幀财剖,很容易發(fā)生“棧溢出”錯誤(stack overflow)悠夯。但對于尾遞歸來說,由于只存在一個調(diào)用幀躺坟,所以永遠(yuǎn)不會發(fā)生“棧溢出”錯誤沦补。

function factorial(n) {
  if (n === 1) return 1;
  return n * factorial(n - 1);
}

factorial(5) // 120

上面代碼是一個階乘函數(shù),計(jì)算n的階乘咪橙,最多需要保存n個調(diào)用記錄夕膀,復(fù)雜度 O(n) 。

如果改寫成尾遞歸美侦,只保留一個調(diào)用記錄产舞,復(fù)雜度 O(1) 。

function factorial(n, total) {
  if (n === 1) return total;
  return factorial(n - 1, n * total);
}

factorial(5, 1) // 120

非尾遞歸的 Fibonacci 數(shù)列實(shí)現(xiàn)如下音榜。

function Fibonacci (n) {
  if ( n <= 1 ) {return 1};

  return Fibonacci(n - 1) + Fibonacci(n - 2);
}

Fibonacci(10) // 89
Fibonacci(100) // 堆棧溢出
Fibonacci(500) // 堆棧溢出

尾遞歸優(yōu)化過的 Fibonacci 數(shù)列實(shí)現(xiàn)如下庞瘸。

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

由此可見,“尾調(diào)用優(yōu)化”對遞歸操作意義重大赠叼,所以一些函數(shù)式編程語言將其寫入了語言規(guī)格擦囊。ES6 是如此,第一次明確規(guī)定嘴办,所有 ECMAScript 的實(shí)現(xiàn)瞬场,都必須部署“尾調(diào)用優(yōu)化”。這就是說涧郊,ES6 中只要使用尾遞歸贯被,就不會發(fā)生棧溢出,相對節(jié)省內(nèi)存妆艘。

轉(zhuǎn)自 阮一峰 ECMAScript 6 入門

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末彤灶,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子批旺,更是在濱河造成了極大的恐慌幌陕,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,681評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件汽煮,死亡現(xiàn)場離奇詭異搏熄,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)暇赤,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,205評論 3 399
  • 文/潘曉璐 我一進(jìn)店門心例,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人鞋囊,你說我怎么就攤上這事止后。” “怎么了溜腐?”我有些...
    開封第一講書人閱讀 169,421評論 0 362
  • 文/不壞的土叔 我叫張陵译株,是天一觀的道長微饥。 經(jīng)常有香客問我,道長古戴,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 60,114評論 1 300
  • 正文 為了忘掉前任矩肩,我火速辦了婚禮现恼,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘黍檩。我一直安慰自己叉袍,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,116評論 6 398
  • 文/花漫 我一把揭開白布刽酱。 她就那樣靜靜地躺著喳逛,像睡著了一般。 火紅的嫁衣襯著肌膚如雪棵里。 梳的紋絲不亂的頭發(fā)上润文,一...
    開封第一講書人閱讀 52,713評論 1 312
  • 那天,我揣著相機(jī)與錄音殿怜,去河邊找鬼典蝌。 笑死,一個胖子當(dāng)著我的面吹牛头谜,可吹牛的內(nèi)容都是我干的骏掀。 我是一名探鬼主播,決...
    沈念sama閱讀 41,170評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼柱告,長吁一口氣:“原來是場噩夢啊……” “哼截驮!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起际度,我...
    開封第一講書人閱讀 40,116評論 0 277
  • 序言:老撾萬榮一對情侶失蹤葵袭,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后甲脏,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體眶熬,經(jīng)...
    沈念sama閱讀 46,651評論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,714評論 3 342
  • 正文 我和宋清朗相戀三年块请,在試婚紗的時候發(fā)現(xiàn)自己被綠了娜氏。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,865評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡墩新,死狀恐怖贸弥,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情海渊,我是刑警寧澤绵疲,帶...
    沈念sama閱讀 36,527評論 5 351
  • 正文 年R本政府宣布哲鸳,位于F島的核電站,受9級特大地震影響盔憨,放射性物質(zhì)發(fā)生泄漏徙菠。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,211評論 3 336
  • 文/蒙蒙 一郁岩、第九天 我趴在偏房一處隱蔽的房頂上張望婿奔。 院中可真熱鬧,春花似錦问慎、人聲如沸萍摊。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,699評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽冰木。三九已至,卻和暖如春笼恰,著一層夾襖步出監(jiān)牢的瞬間踊沸,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,814評論 1 274
  • 我被黑心中介騙來泰國打工挖腰, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留径荔,地道東北人公浪。 一個月前我還...
    沈念sama閱讀 49,299評論 3 379
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親什乙。 傳聞我的和親對象是個殘疾皇子辜膝,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,870評論 2 361

推薦閱讀更多精彩內(nèi)容

  • 函數(shù)參數(shù)的默認(rèn)值 基本用法 在ES6之前深啤,不能直接為函數(shù)的參數(shù)指定默認(rèn)值拴魄,只能采用變通的方法。 上面代碼檢查函數(shù)l...
    呼呼哥閱讀 3,402評論 0 1
  • 1.函數(shù)參數(shù)的默認(rèn)值 (1).基本用法 在ES6之前崖飘,不能直接為函數(shù)的參數(shù)指定默認(rèn)值榴捡,只能采用變通的方法。
    趙然228閱讀 696評論 0 0
  • 第一章 塊級作用域綁定 let 和 const 都是不存在提升,聲明的都是塊級標(biāo)識符都禁止重聲明 每個const聲...
    NowhereToRun閱讀 1,590評論 0 2
  • 三翰蠢,字符串?dāng)U展 3.1 Unicode表示法 ES6 做出了改進(jìn)项乒,只要將碼點(diǎn)放入大括號,就能正確解讀該字符梁沧。有了這...
    eastbaby閱讀 1,539評論 0 8
  • 看到小阿alice的微博檀何,突然想吃金莎巧克力,想象香脆的外殼和入口即化的軟巧克力,有種馬上去買的沖動频鉴。吃完飯栓辜,慢慢...
    漢堡帝國閱讀 251評論 0 0