ECMAScript 6的一些注意點 第二部分(函數拓展)

函數拓展

1.參數變量是默認聲明的,所以不能用letconst再次聲明。

function a(x = 5){
    let x = 1;    //error
}

2.使用參數默認值時,函數不能有同名參數。

function a(x,x,y){}      //正確
function a(x,x,y=1){}    //錯誤

3.參數默認值不是傳值的,而是每次都重新計算默認值表達式的值。

let a = 90;
function x( p = b + 1){
    console.log(p);
}
x()    //91
a = 100;
x()    //101
每次調用函數x,都會重新計算b + 1

與解構賦值默認值結合使用 

1.參數默認值與解構賦值並用,可以避免因未傳入參數導致的不生成變量報錯狀況

//只使用解構賦值,未定默認值:
function foo({x, y = 5}) {
  console.log(x, y);
}
foo() // TypeError: Cannot read property 'x' of undefined

//默認值 + 解構賦值結合
function foo({x,y=5} = {}){    
console.log(x,y);
}
foo()    //undefined    5

2.如果設置默認值是一個具體對象:

function a({x, y} = { x: 0, y: 0 })

代表:默認值是一個有屬性的對象,並未設置解構賦值,所以需要按照該默認值對象個數進行傳值

  • 傳入空對象時沒有值的顯示:[undefined,undefined]
  • 傳入參數個數不相同時:a({x:1}) [1,undefined]
  • 傳入參數不存在時:[undefined,undefined]

參數默認值的位置

1.參數默認值應該設在尾部,如果是非尾部的位置,則這個參數無法省略,想省略可以傳入undefined,null不可以。


函數的 length 屬性

1.指定默認值後,lenth屬性將失真(從指定默認值的參數位置開始往後所有的參數,將不計入length)。


作用域

1.一旦設置了參數的默認值,函數進行聲明初始化時,參數會形成一個單獨的作用域,初始化結束後作用域會消失。

let x = 1;
function f(x,y = x){
    console.log(x);
}
f(2) //2 默認值變量x指向第一個參數x,而不是全局變量x

2.默認值中的參數未定義,由於單獨作用域原因會去全局中找該參數,故函數內部無法影響該參數,如果全局中不存在該參數,則報錯

let a = 10;
function f(x = a){
  let a = 2;
  console.log(x);
}
f() //10

function f2(x = b){
  let b = 2;
  console.log(x);
}
f2()    //error

3.暫時性死區的狀況:(由於單獨作用域原因,x=x實際執行的是let x = x)

let x = 1;
function f(x = x){    
...
}
f();    //x is not defined

4.默認值是函數的時候同樣如此

5.複雜示例

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

解析:參數爲單獨作用域,匿名函數y與參數x在同一作用域,所以不影響全局中的x,函數中單獨定義了x,由於作用域不同,所以匿名函

數y並不能影響函數內部x,故調用函數時輸出3,全局中x還爲1。

6.當去掉var:

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

去掉var後,函數內x指向參數x,故匿名y的執行會影響函數內部,所以輸出2,由於作用域不同,全局仍不受影響。

7.應用--設置一個函數必須傳參才能調用:

function a(){
    throw new Errow('請傳入參數再調用')
}
function foo(b = a()){
    return b;
}
foo();    //Error:請傳入參數再調用

8.參數的默認值不是在定義時執行,而是在運行時執行。如果參數已經賦值,默認值中的函數就不會運行。

9.將參數默認值設爲undefined,表明這個參數是可以省略的。


Rest參數

1. rest 參數(形式爲...變量名),用於獲取函數的多餘參數,rest 參數搭配的變量是一個數組,該變量將多餘的參數放入數組中。

2.是一個真正的數組,數組特有的方法都可以使用。

3.rest 參數之後不能再有其他參數(即只能是最後一個參數),否則會報錯。

4.函數的length屬性,不包括 rest 參數。


嚴格模式

1.只要函數參數使用了默認值、解構賦值、或者擴展運算符,那麼函數內部就不能顯式設定爲嚴格模式,否則會報錯。

2.兩種方法可以規避這種限制。第一種是設定全局性的嚴格模式,這是合法的。

3.第二種是把函數包在一個無參數的立即執行函數裏面。


name屬性

1.函數的name屬性,返回該函數的函數名。

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

var f = function () {};

// ES5
f.name // ""

// ES6
f.name // "f"

3.Function構造函數返回的函數實例,name屬性的值爲anonymous

4.bind返回的函數,name屬性值會加上bound前綴。

function foo() {};
foo.bind({}).name // "bound foo"

(function(){}).bind({}).name // "bound "

箭頭函數

1.由於大括號被解釋爲代碼塊,所以如果箭頭函數直接返回一個對象,必須在對象外面加上括號,否則會報錯。

let getTempItem = id => ({ id: id, name: "Temp" });

下面是一種特殊情況,雖然可以運行,但會得到錯誤的結果。

let foo = () => { a: 1 };
foo() // undefined    
//a可以被解釋爲語句的標籤,因此實際執行的語句是1;,然後函數就結束了,沒有返回值。

2.箭頭函數有幾個使用注意點:

(1)函數體內的this對象,就是定義時所在的對象,而不是使用時所在的對象。

(2)不可以當作構造函數,也就是說,不可以使用new命令,否則會拋出一個錯誤。

(3)不可以使用arguments對象,該對象在函數體內不存在。如果要用,可以用 rest 參數代替。

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

上面四點中,第一點尤其值得注意。this對象的指向是可變的,但是在箭頭函數中,它是固定的。

3.this指向的固定化,並不是因爲箭頭函數內部有綁定this的機制,實際原因是箭頭函數根本沒有自己的this,導致內部的this就是外層代碼塊的this

4.由於箭頭函數沒有自己的this,所以當然也就不能用call()apply()bind()這些方法去改變this的指向

5.除了this,以下三個變量在箭頭函數之中也是不存在的,指向外層函數的對應變量:argumentssupernew.target

6.不適應場景:

  • 第一個場合是定義函數的方法,且該方法內部包括this,調用函數時,this指向了全局對象,因此不會得到預期結果。
let cat = {
     lives: 9,
     jumps: () => {
     this.lives--;
    }
}
cat.jumps();    //NaN 全局中並沒有定義lives
  • 第二個場合是需要動態this的時候,也不應使用箭頭函數。
var button = document.getElementById('press');
button.addEventListener('click', () => {
  this.classList.toggle('on');
});
this -> Window

因爲button的監聽函數是一個箭頭函數,導致裏面的this就是全局對象。

動態this -- 動態綁定事件 -- xx.addEventListener

  • 如果函數體很複雜,不單純是爲了計算值,這時也不應該使用箭頭函數,而是要使用普通函數,這樣可以提高代碼可讀性。

部署管道機制(pipeline)的例子詳解:

在解析之前需要的知識點

1.reduce()方法:

按照菜鳥教程所說:

語法和參數設定:

接下來看原函數:

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)

看起來並不好理解,用ES5轉換一下:

const pipeline = function (...funcs) {

        return function (val){

            return funcs.reduce(

                function(a,b){

                    return b(a);

                },val);
                
        }
}
const plus1 = function (a){
    
    return a + 1;

}
const mult2 = function (a){

    return a * 2;    

}

這樣看起來會清楚一些,部署管道機制意思就是將上一個函數的輸出傳遞給下一個函數使用:

我們來輸出一下addThenMult(它是上一個pipeline的輸出):

//當將參數傳入pipeline調用後,我們輸出一下:
ƒ (val) {
  return funcs.reduce(
     function (a, b) {
            return b(a)
        }, val
    )
}

這個時候pipeline的參數(兩個function)和上面的輸出即爲addThenMult所用,然後執行addThenMult(5)

由於該示例非常具有誤導性(可能是我腦子不太好使= =),所以分佈寫出來:

//步驟1:
ƒ (5) {
  return funcs.reduce(
     function (a=>a+1(total初始值), a=>a*2) {
            return b(a)
        }, 5    //傳遞給初始值
    )
}
//步驟2:
ƒ (5) {
  return funcs.reduce(
     function (a=>5+1(total初始值), a=>a*2) {
            return b(a)
        }, 5    //傳遞給初始值
    )
}
//步驟3:
ƒ (5) {
  return funcs.reduce(
     function (a=>5+1(total初始值), a=>a*2) {
            return b(6)
        }, 5    //傳遞給初始值
    )
}
//步驟4:
ƒ (5) {
  return funcs.reduce(
     function (a=>5+1(total初始值), a=>a*2) {
            return 6=>6 * 2 //12
        }, 5    //傳遞給初始值
    )
}

可讀性強的寫法:

const plus1 = a => a + 1;
const mult2 = a => a * 2;

mult2(plus1(5))// 12

//ES5
const plus1 = function(a){
    return a+1;
}
const mult2 = function(a){
    return a*2;
}

步驟:

  • plus1先傳入5調用 返回 5+1爲mult2所用
  • mult2使用6作爲參數帶入調用,返回12

雙冒號運算符

Emmm,到目前應該還是個提案


尾調用優化

1.某個函數的最後一步是調用另一個函數爲尾調用。

2.當最後一步調用函數後還有操作時不叫尾調用,沒有最後一個return確認尾部時也不屬於尾調用。

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

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

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

3.尾調用不一定出現在函數尾部,只要是最後一步操作即可。

function f(x) {
  if (x > 0) {
    return m(x)
  }
  return n(x);
}
//都屬於尾調用 都是函數f的最後一步操作

4.函數調用會在內存形成一個“調用記錄”,又稱“調用幀”,保存調用位置和內部變量等信息,如果A調B,則在A幀上方會形成一個B幀,B運

行結束後返回結果B幀纔會消失,以此類推會形成調用棧。


這個過程的優化叫做尾調用優化,即只保留內層函數的調用幀。

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

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

// 等同於
g(3);

1.只有不再用到外層函數的內部變量,內層函數的調用幀纔會取代外層函數的調用幀,否則就無法進行“尾調用優化”。

function addOne(a){
  var one = 1;
  function inner(b){
    return b + one;    //用到了外層one , 所以不會進行優化
  }
  return inner(a);
}

尾遞歸

1.遞歸非常耗費內存,因爲需要同時保存成千上百個調用幀,很容易發生“棧溢出”錯誤(stack overflow)

2.尾遞歸,由於只存在一個調用幀(即只調用一次),所以永遠不會發生“棧溢出”錯誤。

下面是例子:

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

factorial(5) // 120

上面函數會保存很多調用記錄,改寫:

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

factorial(5, 1) // 120    total的默認值爲1 

斐波那契數列:

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

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

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

//尾遞歸優化過的 Fibonacci 數列實現如下。

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
/*
Fibonacci2(3)的調用過程

return Fibonacci2(2,1,2)    

return Fibonacci2(1,2,3)    //輸出 ac2 = 3  


*/

遞歸函數的改寫

1.尾遞歸的實現缺點是不太直觀,第一眼很難看出來

解決方案:

一、遞歸外再定義一個函數

function foo( n , total){
    if(n === 1) return total;
    return foo(n-1,n*total)
}
//正常形式函數,調用它即可
function factorial(n){
    return foo(n,1);
}
factorial(5)

柯里化(意思是將多參數的函數轉換成單參數的形式),先了解一下JavaScript中的call()方法,MDN上:

1.call()允許爲不同的對象分配和調用屬於一個對象的函數/方法

2.call()提供新的 this 值給當前調用的函數/方法。你可以使用call來實現繼承:寫一個方法,然後讓另外一個新的對象來繼承它(而不是在新對象中再寫一次這個方法)

3.call() 方法調用一個函數, 其具有一個指定的this值和分別地提供的參數(參數的列表)。

注意:該方法的作用和 apply() 方法類似,只有一個區別,就是call()方法接受的是若干個參數的列表,而apply()方法接受的是一個包含多個參數的數組

然後我們參照JavaScript高級程序設計中的示例來解釋call()和this:

1.函數內的一個特殊對象是this,引用的是函數據以執行的環境對象(當在全局作用域中調用函數時,this對象引用的就是window)

window.color  = 'red';
var o = { color:"blue" };
function sayColor(){
    console.log(this.color);
}
sayColor();    //red
o.sayColor = sayColor;        //發生了耦合
o.sayColor();    //blue

解析:

sayColor是在全局函數中定義的,他引用了this對象,調用之前this並不確定,在全局調用時,this指向window,將函數賦值給o並調用,則

this引用對象o,因此對this.color求值將會轉換成對o.color求值。

2.每個函數都包含兩個非繼承而來的方法:apply()和call()。這兩個方法的用途都是在特定的作用域中調用函數,實際上等於設定函數體內

this對象的值。(這裏重點說call())

function sum(num1 , num2){
    return num1 + num2;
}
function callSum( num1 , num2){
    return sum.call(this,num1,num2);
}
console.log(callSum(10,10))    //20

3.在call()方法下,必須明確傳入每一個參數,結果與apply()沒什麼不同如何選擇:如果打算傳入arguments對象或者函數先接受到的是一個數組,則選用apply()更方便,否則選擇call(),不傳遞參數的情況下無所謂。

4.傳遞參數並非apply()和call()的真正用武之地;他們真正強大的地方在於:能夠擴充函數賴以運行的作用域:

window.color = 'red';
var o = { color : 'blue' };
function sayColor(){
    alert(this.color);
}
sayColor();    //red
sayColor.call(this);    //red
sayColor.call(window);    //red
sayColor.call(o);    //red 無耦合

這個例子在前面說明this對象的基礎上修改,當運行sayColor.call(o)的時候,函數的執行環境發生了改變,因爲this對象指向了o,所以是

blue。

5.使用call()(或者apply())來擴充作用域的最大好處:對象不需要與方法有任何耦合關係,上一個例子中先是將sayColor放到了對象中再執

行,而這次則不需要。

接下來

我們看柯里化後的尾遞歸代碼:

function currying(fn, n) {
  return function (m) {
    return fn.call(this, m, n);    //this指向全局,提供n,m參數。
  };
}

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

const factorial = currying(tailFactorial, 1);

factorial(5) // 120    //全局調用

看下factorial的輸出:

ƒ (m) {
   return fn.call(this, m, n);
}

調用時將5傳入作爲tailFactorial的n,1作爲taiFactarial的total值。

二、使用ES6默認值:

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

factorial(5) // 120

一旦使用遞歸,就最好使用尾遞歸。


嚴格模式

1.ES6 的尾調用優化只在嚴格模式下開啓,正常模式是無效的。

2.這是因爲在正常模式下,函數內部有兩個變量,可以跟蹤函數的調用棧。

  • func.arguments:返回調用時函數的參數。
  • func.caller:返回調用當前函數的那個函數。

3.尾調用優化發生時,函數的調用棧會改寫,因此上面兩個變量就會失真。嚴格模式禁用這兩個變量,所以尾調用模式僅在嚴格模式下生效。


尾遞歸優化的實現 

1.採用“循環”換掉“遞歸”,蹦牀函數(trampoline)可以將遞歸執行轉爲循環執行。

蹦牀函數的實現:

function trampoline(f) {
  while (f && f instanceof Function) {
    f = f();
  }
  return f;
}

它接受一個函數f作爲參數。只要f執行後返回一個函數,就繼續執行,返回一個函數,然後執行該函數,而不是函數裏面調用函數,這樣就避免了遞歸執行,從而就消除了調用棧過大的問題。

2.改寫:

這裏需要了解bind()函數:

bind(),ES5提出,這個方法會創建一個函數的實例其this會被綁定到傳給bind()函數的值

window.color = 'red';
var o = {color:"blue"};
function sayColor(){
console.log(this.color);
}
var objectSayColor = sayColor.bind(o);
objectSayColor();    //blue

解析:

sayColor調用bind()傳入對象,創建了objectSayColor函數,這個函數的this等於o,即使在全局調用也會看到Blue。

瞭解後我們來看改寫的例子:

//跳板函數
function trampoline(f) {
  while (f && f instanceof Function) {
    f = f();    //只要執行後返回一個函數就繼續執行,最後將函數的輸出賦值給變量f
  }
  return f;
}
//原函數
function sum(x,y){
  if( y > 0 ){
    return sum (x+1 , y-1);
  }else{
    return x;
  }
}
sum(1 , 100000) //超出棧

//改寫sum函數使其每一步都返回一個函數
function sum2(x,y){
    if(y > 0){
        return sum2.bind(null,x,y);
    }else{
        return x;
    }
}
//使用跳板函數執行
trampoline(sum2(1,100000))    //正常輸出10000001

尾遞歸優化的實現

正常模式下,或者那些不支持該功能的環境中的尾遞歸優化,將遞歸轉變成循環:

function tco(f) {
  var value;
  var active = false;
  var accumulated = [];

  return function accumulator() {
    accumulated.push(arguments);
    if (!active) {
      active = true;
      while (accumulated.length) {
        value = f.apply(this, accumulated.shift());  //apply(this,[1,10]) ->[2,9]->[3,8]... 
      }
      active = false;
      return value;
    }
  };
}

var sum = tco(function(x, y) {
  if (y > 0) {
    return sum(x + 1, y - 1)
  }
  else {
    return x
  }
});

sum(1, 100000)
// 100001

阮大神給出的原理:

默認情況下,active是不激活的。一旦進入尾遞歸優化的過程,這個變量就激活了。然後,每一輪遞歸sum返回的都是undefined,所以就避免了遞歸執行;而accumulated數組存放每一輪sum執行的參數,總是有值的,這就保證了accumulator函數內部的while循環總是會執行。這樣就很巧妙地將“遞歸”改成了“循環”,而後一輪的參數會取代前一輪的參數,保證了調用棧只有一層。

這裏需要了解:

shift()方法:(會改變原數組,所以每次輸出accmulated都是空數組)

shift() 方法用於把數組的第一個元素從其中刪除,並返回第一個元素的值。

如果數組是空的,那麼 shift() 方法將不進行任何操作,返回 undefined 值。請注意,該方法不創建新數組,而是直接修改原有的 arrayObject。

解釋幾個點:

  1. 爲什麼出結果之前,sum返回的是undefined

因爲,在y < 0之前,sum的返回值都是調用函數,並不存在明確的返回值,所以返回undefined

     2.f.apply(this,accmulated.shift())的作用?

將被刪除的數組[1,10] / [2,8] .... 帶入sum(x+1,y-1)中進行調用,一直到y = 0的時候停止返回x

過程解析:

我們先輸出一下sum:

ƒ accumulator() {
            accumulated.push(arguments);    //  accumulated -> [[1,10]]
            log(arguments)
            if (!active) {
                active = true;
                while (acc…

開始詳細解析:

//首先 調用了sum(1,10)
//相當於 
function accumulator() {
    accumulated.push(arguments);    //arguments變爲[1,10] accumulated變爲[[1,10]];
    if (!active) {
      active = true;
      while (accumulated.length) {    
        value = f.apply(this, accumulated.shift());    
        //shift()執行,返回數組被刪除的第一個參數[1,10],value = f.apply(this,[1,10]),此時            
        //accumulated變爲[]
      }
      active = false;
      return value;
    }
  };
}
//f.apply(this,[1,10]) 相當於調用sum(1+1,10-1) 即:sum(2,9),重複過程:
function accumulator() {
    accumulated.push(arguments);    //arguments變爲[2,9] accumulated變爲[[2,9]];
    if (!active) {
      active = true;
      while (accumulated.length) {    
        value = f.apply(this, accumulated.shift());    
        //shift()執行,返回數組被刪除的第一個參數[2,9],value = f.apply(this,[2,9]),此時            
        //accumulated變爲[]
      }
      active = false;
      return value;
    }
  };
}
//.................以此類推 , 當傳入sum的y值變爲0的時候,x爲11,根據:
if (y > 0) {
  return sum(x + 1, y - 1)    
}else{
  return x    
}
// 得知y<0 return從undefined變成了明確數組x = 11
// 所以 sum(1,10)   = 11
    

斐波那契同理:

function tco(f) {
    var arr = [];
    var value;
    var active = false;
    return function () {
        arr.push(arguments);
        if (!active) {
            active = true;
            while (arr.length) {
                value = f.apply(this, arr.shift());
            }
            active = false;
            return value;
        }
    };
}
let Fibonacci = tco(function (n, x1 = 1, x2 = 1) {
    if (n <= 1) {
        return x2
    }
    return Fibonacci(n - 1, x2, x1 + x2);
})
log(Fibonacci(8))    //34

 


函數參數的尾逗號

ES2017 允許函數的最後一個參數有尾逗號(trailing comma)。

function clownsEverywhere(
  param1,
  param2,
) { /* ... */ }

clownsEverywhere(
  'foo',
  'bar',
);

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章