ES6深入學習(二)關於函式

Thea_more發表於2019-04-19

一、函式引數的預設值

ES6之前,不能直接為函式的引數指定預設值,只能採用變通的方法。對於函式的命名引數如果不顯示傳值則預設為undefined

function foo(x,y) {
    x = x || 'Hello';
    y = y || 'World';
    console.log(x,y)
}
foo(0,'Thea');//Hello Thea
複製程式碼

這種寫法的確定在於,如果引數x或者y賦值了,但是對應的布林值為false,則該賦值不起作用。這種情況下更安全的選擇是通過typeof檢查引數型別:

function foo(x,y) {
    debugger
    x = (typeof x !== 'undefined') ? x : 'Hello';
    y = (typeof y !== 'undefined') ? y : 'World';
    console.log(x,y);
}
foo(false,'Thea');//false "Thea"
複製程式碼
  • 1.1 ES6中的預設引數值

ES6簡化了為形參提供預設值的過程,如果沒為引數傳入值則提供一個初始值:

function foo(x,y='World'){
    console.log(x,y)
}
foo('I love')//I love World
複製程式碼

除了簡潔,ES6 的寫法還有兩個好處:首先,閱讀程式碼的人,可以立刻意識到哪些引數是可以省略的,不用檢視函式體或文件;其次,有利於將來的程式碼優化,即使未來的版本在對外介面中,徹底拿掉這個引數,也不會導致以前的程式碼無法執行。

  • 1.2值得注意:

1、引數變數是預設宣告的,函式體中不能用let或const再次宣告,否則報錯

function foo(x,y) {
    let x = 'Hello';//Uncaught SyntaxError
}
複製程式碼

2、使用引數預設值時,函式不能有同名引數。

 function foo(x,x,y=3) {
     //...
 }
 //Uncaught SyntaxError: Duplicate parameter name not allowed in this context
複製程式碼

3、引數預設值不是傳值的,而是每次都重新計算預設值的表示式的值(引數預設值是惰性求值的)初次解析函式宣告時不會計算預設值的表示式值,只有當呼叫foo()函式且不傳引數時才會呼叫。

 let x = 3;
 function foo(,p = x + 1){
     console.log(p)
 }
 foo()//4
 x = 5;
 foo()//6
複製程式碼

引數p的預設值是x+1,每次呼叫函式foo都會重新計算x+1,而不是預設等於4。 正因為預設引數是在函式呼叫時求值,所以可以使用先定義的引數作為後定義引數的預設值

function foo(x,y=x) {
    return x+y
}
foo(1) //2
複製程式碼

4、引數預設值的位置

通常情況下,定義了預設值的引數應該是函式的尾引數。因為這樣容易看出到底省略了哪些引數,實際上如果非尾部的引數設定預設值,這個引數是沒法省略的,(除非不為其後引數傳入值或主動為第二個引數傳入undefined才會使用這個預設引數)null沒有這個效果,因為null是一個合法值。

  • 1.3函式的lengh屬性

預設引數值對arguments物件的影響。 在ES5非嚴格模式下,函式命名引數的變化會體現在argumnets物件中;

function foo(x,y) {
    console.log(x === arguments[0]); // true
    y = 3;
    console.log(y === arguments[1]); // true
}
foo.length //2
複製程式碼

在非嚴格模式下,命名引數的變化會同步更新到arguments物件中,所以x,y被賦予新值時,最終===全等比較的結果為true。然而在ECMAScript5的嚴格模式下,取消了arguments隨之改變的行為。無論引數如何改變,arguments物件不再隨之改變。

在ES6中,如果一個函式使用了預設引數值,無論是否顯示定義了嚴格模式,arguments物件的行為都將與ES5嚴格模式下保持一致。

function foo(x,y=1){
    console.log(arguments.length);
    console.log(x === arguments[0]);
    console.log(y === arguments[1]);
}
foo(1);//1,true,false
foo(1,2);//2,true,true
foo.length;//1
複製程式碼

指定了預設值後,函式的length屬性將返回沒喲指定預設值的引數個數,因為length屬性的含義是,該函式預期傳入的引數的個數。某個引數指定預設值以後,預期傳入的引數個數就不包括這個引數了。同理rest引數也不會計入length屬性。

  • 1.4作用域

一旦設定了引數的預設值,函式進行宣告初始化時,引數會形成一個單獨的作用域(context),等到初始化結束,這個作用域會消失,不設定預設值是不會出現。

const x = 1;
function foo(x,y = x) {
    console.log(y);
}
foo(3)//3
複製程式碼

引數y的預設值等於變數x,呼叫函式foo時,引數形成一個單獨的作用域,在這個作用域裡,預設值變數x指向第一個引數x,而不是全域性變數x。

const x = 3;
function foo(y = x) {
    let x = 2;
    console.log(y)
}
foo();//3
複製程式碼

呼叫函式foo時,引數y=x形成一個單獨的作用域,這個作用域裡x沒定義,所以指向外層的全域性變數x。函式體內部的區域性變數x不會形象預設值變數x。如果此時全域性變數x不存在,就會報錯。

const x = 3;
function foo(x = x) {
    //...
}
foo()//ReferenceError: x is not defined
複製程式碼

上述程式碼中x=x形成一個單獨的作用域,實際執行的是 let x = x,由於臨時死區(與let的行為類似)原因。

若引數的預設值是一個函式,該函式的作用域也遵循這個規則:

let z = 'z-outer';
function foo(func = () => z) {
    let z = 'z-inner';
    console.log(func());
}
foo()//z-outer   z指向外層全域性變數z
複製程式碼

請看下面一個複雜的例子:

var d = 'd-outer';
function foo(d,y=() => {d = 'd-argumnets';}){
    var d = 'd-inner';
    y();
    console.log(d);
}
foo()//d-inner
d; // d-outer
複製程式碼

上述程式碼中,函式foo的引數形成一個單獨作用域,這個作用域裡先宣告瞭變數d,然後宣告瞭一個預設值是匿名函式的y。匿名函式內部的變數d指向該引數作用域裡的第一個引數d。函式foo內部又宣告瞭一個內部的變數d,這個d與引數作用域裡的d不是同一個變數,所以執行匿名函式y後,函式foo內部的d以及全域性變數d的值都沒有改變。

如果將函式foo內部的var d = 'd-inner';的var 去掉,那麼函式foo內部的d就指向第一個引數d,與匿名函式內部的d同一個引數,外層全域性變數d仍不受影響。

var d = 'd-outer';
function foo(d,y=() => {d = 'd-argumnets';}){
    d = 'd-inner';
    y();
    console.log(d);
}
foo()//d-argumnets
d; // d-outer
複製程式碼

函式引數有自己的作用域和臨時死區,其與函式體的作用域是各自獨立的,也就是說引數的預設值不可訪問函式體內宣告的變數。

二、處理無命名引數

JavaScript的函式語法規定,無論函式已定義的命名引數有多少,都不限制呼叫時傳入的實際引數數量,呼叫時總可以傳入任意數量的引數。

ES6引入不定引數,在函式的命名引數前新增三個點(...)就表名這是一個不定引數,rest引數,用於獲取函式的多餘引數,該引數為一個陣列,包含著自它之後傳入的所有引數,通過這個陣列名即可逐一訪問裡面的引數。rest引數之後不能再有其他引數,只能作為最後一個引數,且每個函式最多隻能宣告一個不定引數,否則會報錯函式的length也不包括rest引數。

function foo(...arrs) {
    for(let val of arrs){
        console.log(val)
    }
}
foo(1,2,3);//1,2,3
複製程式碼

知識點擴充套件:

for...of 語句建立一個迴圈來迭代可迭代的物件。在 ES6 中引入的 for...of 迴圈,以替代 for...in 和 forEach() ,並支援新的迭代協議。for...of 允許你遍歷 Arrays(陣列), Strings(字串), Maps(對映), Sets(集合)等可迭代的資料結構等。

用法:

for (variable of iterable) {
    statements
}
複製程式碼

variable:每個迭代的屬性值被分配給該變數。 iterable:一個具有可列舉屬性並且可以迭代的物件。

不定引數的設計初衷是代替JavaScript的arguments物件,起初在ECMAScript4草案中,arguments物件被移除並新增了不定引數的特性,從而可以傳入不限制數量的引數,但ECMAScript4從未被標準化,這個想法被擱置下來,直到重新引入了ES6標準,唯一的區別是arguments物件依然存在。如果宣告函式時定義了不定引數,則在函式被呼叫時,arguments物件包含了所有傳入函式的引數。

function foo(a,...b){
    console.log(b.length);
    console.log(arguments.length);
}
foo.length;//1
foo(1,2,3,4);//3,4
複製程式碼
  • 嚴格模式

從ES5開始,函式內部可以設定為嚴格模式。ES6做了一點修改,規定只要函式引數使用了預設值、解構賦值或者擴充套件運算子,那麼函式就不能顯式設定為嚴格模式,否則會報錯。這樣規定的原因是,函式內部的嚴格模式,同時適用於函式體和函式引數。但是,函式執行的時候,先執行函式引數,然後再執行函式體。這樣就有一個不合理的地方,只有從函式體之中,才能知道引數是否應該以嚴格模式執行,但是引數卻應該先於函式體執行。

有兩種方法可以規避這種限制

//1、設定全域性性的嚴格模式
'use strict';
function foo(x,y=x) {
    //statements
}

//2、把函式包在一個無引數的立即執行函式的裡面
const doSomething = (function() {
    'use strict';
    return function(...a) {
        for(let val of a){
            console.log(val)
        }
    }
}())
複製程式碼
  • name屬性

函式的name屬性,返回該函式的函式名。注意:函式name屬性的值不一定引用同名變數,它只是協助除錯用的額外資訊,所以不能使用name屬性的值來獲取對於函式的引用。

function foo(){}
foo.name //'foo'
複製程式碼

如果將一個匿名函式賦值給一個變數,ES5 的name屬性,會返回空字串,而 ES6 的name屬性會返回實際的函式名。

var foo = function() {}
//ES5
foo.name //'';
//ES6
foo.name //"foo"
複製程式碼

如果將一個具名函式賦值給一個變數,則 ES5 和 ES6 的name屬性都返回這個具名函式原本的名字。函式表示式有一個名字,這個名字比函式本身被賦值的變數權重高。

let foo = function bar() {};
//ES5、ES6
foo.name //bar
複製程式碼

另外還有兩個特例:通過bind()函式建立的函式,其名稱帶有“bound”字首;通過Function建構函式建立的函式,其名稱將是“anonymous”。

var foo = function() {};
console.log(foo.bind().name);//bound foo
console.log(new Function().name);//anonymous
複製程式碼
  • 箭頭函式

ES6執行使用箭頭(=>)定義函式,箭頭函式同樣也有一個name屬性,這與其他函式的規則相同。

var foo = x => x;
//等價於
var foo = function(x){
    return x;
}
複製程式碼

如果箭頭函式不需要引數或需要多個引數,就使用一個圓括號代表引數部分,如果箭頭函式的程式碼塊部分多於一條語句,就要使用大括號將它們括起來,並顯示地定義一個返回值。由於大括號被解釋為程式碼塊,所以如果箭頭函式直接返回一個物件,必須在物件外面加上括號,否則會報錯。

與傳統JavaScript函式不同點:

沒有this、supper、arguments和new.target繫結:箭頭函式的這些值由外圍最近一層非箭頭函式決定。函式內部的this值不可被改變,在函式的生命週期內始終保持一致。

不能通過new關鍵字調:箭頭函式沒有[[Construct]]方法,所以不能被用作建構函式,如果通過new關鍵字呼叫箭頭函式,程式會丟擲錯誤。由於不能通過new關鍵字呼叫箭頭函式,因而沒有構建原型的需求,所以箭頭函式不存在prototype屬性

不支援arguments物件:該物件在函式體內不存在。如果要用,可以用命名引數或 rest 引數代替。

不支援重複命名引數:無論在嚴格還是非嚴格模式下,箭頭函式都不支援重複的命名引數,而在傳統函式規定中,只有在嚴格模式下才不能有重複的命名引數

function foo(){
    setTimeout(() => {
        console.log('id:',this.id);
    },1000)
}
var id = 1;
foo.call({id:2});//id:2
複製程式碼

setTimeout的引數是一個箭頭函式,這個箭頭函式的定義生效是在foo函式生成時,而它的真正執行要等到 1000 毫秒後。如果是普通函式,執行時this應該指向全域性物件window,這時應該輸出1。但是,箭頭函式導致this總是指向函式定義生效時所在的物件(本例是{id: 2})而不是指向執行時所在的作用域,所以輸出的是2。

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指向執行時所在的作用域(即全域性物件)。所以,3100 毫秒之後,timer.s1被更新了 3 次,而timer.s2一次都沒更新。

  • 尾呼叫優化

尾呼叫(Tail Call)是函數語言程式設計的一個重要概念,本身非常簡單,一句話就能說清楚,就是指某個函式的最後一步是呼叫另一個函式。

function foo(){
    return bar();//尾呼叫
}
複製程式碼

在ES5的引擎中,尾呼叫的實現與其他函式呼叫的實現類似:建立一個新的棧幀(stack frame),將其推入呼叫棧來表示函式呼叫,也就是說迴圈呼叫中,每一個未用完的棧幀都會被儲存在記憶體中,當呼叫棧變得過大時,會造成程式問題。

以下三種情況都不屬於尾呼叫

//在呼叫函式g後還有賦值操作,即使語義完全一樣
function f(x) {
    let y = g(x);
    return y;
}

//呼叫後還有操作,即使寫在一行內
function f(x) {
    return g(x) + 1;
}

//呼叫後實際還有一個renturn undefined操作
function f(x) {
    g(x);
}
複製程式碼

尾呼叫不一定出現在函式尾部,只要是最後一步操作即可。

function f(x) {
  if (x > 0) {
    return m(x)
  }
  return n(x);
}
複製程式碼

尾呼叫之所以與其他呼叫不同,就在於它的特殊的呼叫位置。

我們知道,函式呼叫會在記憶體形成一個“呼叫記錄”,又稱“呼叫幀”(call frame),儲存呼叫位置和內部變數等資訊。如果在函式A的內部呼叫函式B,那麼在A的呼叫幀上方,還會形成一個B的呼叫幀。等到B執行結束,將結果返回到A,B的呼叫幀才會消失。如果函式B內部還呼叫函式C,那就還有一個C的呼叫幀,以此類推。所有的呼叫幀,就形成一個“呼叫棧”(call stack)。

ES6深入學習(二)關於函式
尾呼叫由於是函式的最後一步操作,所以不需要保留外層函式的呼叫幀,因為呼叫位置、內部變數等資訊都不會再用到了,只要直接用內層函式的呼叫幀,取代外層函式的呼叫幀就可以了。

function foo(){
    let x = 1,
        y = 2;
    return bar(x+y);
}
foo();

//等同於
function foo() {
    return bar(3);
}
f();

//等同於
bar(3)
複製程式碼

如果函式g不是尾呼叫,函式f就需要儲存內部變數m和n的值、g的呼叫位置等資訊。但由於呼叫g之後,函式f就結束了,所以執行到最後一步,完全可以刪除f(x)的呼叫幀,只保留g(3)的呼叫幀。

這就叫做“尾呼叫優化”(Tail call optimization),即只保留內層函式的呼叫幀。如果所有函式都是尾呼叫,那麼完全可以做到每次執行時,呼叫幀只有一項,這將大大節省記憶體。這就是“尾呼叫優化”的意義。只有不再用到外層函式的內部變數,內層函式的呼叫幀才會取代外層函式的呼叫幀,否則就無法進行“尾呼叫優化”。

function addOne(a){
  var one = 1;
  function inner(b){
    return b + one;
  }
  return inner(a);
}
複製程式碼

上面的函式不會進行尾呼叫優化,因為內層函式inner用到了外層函式addOne的內部變數one。

相關文章:ES6深入學習(一)塊級作用域詳解(juejin.im/post/5cb6c8…)

如有錯誤或者建議歡迎指出,我一定快馬加鞭的改正~一起學習交流

相關文章