鍍金池/ 問答/HTML/ 關(guān)于es6函數(shù)參數(shù)默認值的理解問題?

關(guān)于es6函數(shù)參數(shù)默認值的理解問題?

var x = 1;
function foo(x, y = function() { x = 2; }) {
  var x = 3;
  y();
  console.log(x);
}

foo() // 3
x // 1

上面是從阮一峰老師寫的es6入門一書中函數(shù)作用域倒數(shù)第二個例子,阮一峰老師的解釋是這樣的

上面代碼中,函數(shù)foo的參數(shù)形成一個單獨作用域。這個作用域里面,首先聲明了變量x,然后聲明了變量y,y的默認值是一個匿名函數(shù)。這個匿名函數(shù)內(nèi)部的變量x,指向同一個作用域的第一個參數(shù)x。函數(shù)foo內(nèi)部又聲明了一個內(nèi)部變量x,該變量與第一個參數(shù)x由于不是同一個作用域,所以不是同一個變量,因此執(zhí)行y后,內(nèi)部變量x和外部全局變量x的值都沒變。

瀏覽器中的運行結(jié)果也和預(yù)期一樣

圖片描述

而通過Babel將這個例子轉(zhuǎn)es5后為

"use strict";

var x = 1;
function foo(x) {
  var y = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : function () {
    x = 2;
  };

  var x = 3;
  y();
  console.log(x);
}

foo(); // 3
x; // 1

也就是下圖所展示的內(nèi)容

圖片描述

再放到瀏覽器運行時,產(chǎn)生了下圖

圖片描述

函數(shù)執(zhí)行結(jié)果居然不再是es6環(huán)境當(dāng)中的3

根據(jù)阮一峰老師所說

一旦設(shè)置了參數(shù)的默認值,函數(shù)進行聲明初始化時,參數(shù)會形成一個單獨的作用域(context)。等到初始化結(jié)束,這個作用域就會消失。這種語法行為,在不設(shè)置參數(shù)默認值時,是不會出現(xiàn)的。

那么這個參數(shù)作用域應(yīng)該如何理解,而es6的代碼通過Babel轉(zhuǎn)換后放到瀏覽器運行結(jié)果發(fā)生改變?這是不是說是Babel轉(zhuǎn)換的問題?那么es6應(yīng)當(dāng)以何為標(biāo)準?

回答
編輯回答
久舊酒

Babel的實現(xiàn)是錯的。

這是ECMA-262中 9.2.12函數(shù)聲明初始化 小節(jié)中的部分說明:

...If the function’s formal parameters do not include any defaultvalue initializers then the body declarations are instantiated in the same Environment Record as the parameters.If default value parameter initializers exist, a second Environment Record is created for the body declarations...

"...如果函數(shù)形參不含有默認參數(shù),那么函數(shù)體聲明和參數(shù)在同一個Enviroment Record中初始化。否則將為函數(shù)體聲明創(chuàng)建第二個Enviroment Record..."

這里可以簡單地將Enviroment Record看做獨立的作用域。詳細說明在這里

所以當(dāng)函數(shù)存在默認參數(shù)的時候,應(yīng)為參數(shù)創(chuàng)建一個獨立的作用域,然后在這個作用域之中再創(chuàng)建函數(shù)體的作用域,因此一個定義在全局環(huán)境的、帶有默認參數(shù)的函數(shù)聲明,在運行時共產(chǎn)生至少3個作用域,長這個樣子(這里"作用域"更準確的說法是Lexical Enviroment,詞法環(huán)境):

clipboard.png

(為什么說至少呢,因為function body內(nèi)部可能還會有其他作用域,這個不是重點)

ES Spec之所以這么規(guī)定,是因為如果默認參數(shù)引用了函數(shù)作用域外部的變量,同時函數(shù)內(nèi)部有同名的變量存在的話,那么實際所使用的變量應(yīng)該是外部的變量,而不是函數(shù)內(nèi)部的。這是符合人類的思考習(xí)慣的,你不會在一個變量定義之前就使用它。

所以如果要把含有默認參數(shù)的函數(shù)轉(zhuǎn)為ES5寫法的話,必須用另一個函數(shù)隔絕參數(shù)和函數(shù)體,以實現(xiàn)作用域的隔離。以題目的做法為例,轉(zhuǎn)化后的函數(shù)應(yīng)該長這樣:

function foo(x, y) {
  if (typeof y === 'undefined') y = function () { x = 2; };
  return (function() {
    var x = 3;
    y();
    console.log(x);
  }).call(this, x, y)
}

如果在Babel基礎(chǔ)上修改的話,應(yīng)該是這樣:

function foo(x) {
  var y = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : function () {
    x = 2;
  };

  return (function () {
    var x = 3;
    y();
    console.log(x);
  }).call(this, x, y)
}

Babel沒有這么做可能是有別的考慮,也可能是沒想到或者不愿意改,無論如何當(dāng)前的實現(xiàn)都是錯誤的。

最后放一個Chrome運行這段代碼的scope狀態(tài),可以看出是按spec實現(xiàn)的:

clipboard.png

Local即為上面圖示中的parameters,Block就是function body

另外阮一峰說的也不全對,初始化之后作用域是不會消失的,否則運行函數(shù)y()的時候就沒辦法獲取Local里面的x

2018年1月22日 21:40
編輯回答
綰青絲

你可能要補一下js的基礎(chǔ)變量提升。在運行時(細講的話有兩個階段),變量都會提升到當(dāng)前作用域的頂端的,所以你的代碼等價于

var x;

function foo(x) {
  // 變量提升
  var y;
  // 變量提升
  var x;
  

  y = function () { x = 2};
  x = 3;
  
  // y函數(shù)改變了x的值
  y();
  console.log(x);
}

x = 1;

foo();           // 2
console.log(x);  // 1

所以你的foo()函數(shù)里一旦有var x的存在,那么這個函數(shù)內(nèi)部的x都是局部的變量

然后要譴責(zé)你的一點是:下次能用markdown貼代碼嗎?

2017年5月19日 14:28