8-函數的擴展

函數參數的默認值

基本用法

es5中,傳入函數的參數若是爲'',那麼在進行判斷時會轉化爲false,對結果形成影響es6

function log(x, y) {
  y = y || 'World';
  console.log(x, y);
}

log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello World //這裏!

es6容許爲函數的參數設置默認值,直接寫在參數定義的後面,只有在參數是undefined時才採用默認值編程

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

log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello

function Point(x=0,y=0){
    this.x = x
    this.y = y
}
const p = new Point()
p //{x:0,y:0}

參數的變量是默認聲明的,不能再次聲明segmentfault

function foo(x=5){
    let x =1 // error
    const x =2 // error
}

使用參數默認值時,函數不能有同名函數數組

function foo(x,x,y=1){...} //報錯

與解構賦值結合使用

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: Cannot read property 'x' of undefined
解釋:
上面代碼只使用了對象的解構賦值默認值,沒有使用函數參數的默認值
{x,y =5}是一個參數,這個參數裏面的內容,賦了默認值,可是這個參數自己,並無默認值,因此找不到x

能夠寫成:雙重默認值:若是沒提供參數,foo的參數默認一個空對象瀏覽器

function foo({x,y=5}={}){
    console.log(x,y)
}

下面代碼中,函數fetch沒有第二個參數時,函數參數的默認值就會生效,
而後纔是解構賦值的默認值生效,變量method纔會取到默認值GETapp

function fetch(url, { body = '', method = 'GET', headers = {} } = {}) {
  console.log(method);
}

fetch('http://example.com')
// "GET"

有差異的寫法:編程語言

寫法一:
function m1({x=0,y=0} = {}){
    return [x,y]
}
寫法二:
function m2({x,y} ={x:0,y:0}){
    return [x,y]
}

寫法一:對函數參數進行了解構賦值,爲空對象,對函數參數裏面的內容進行了解構賦值
寫法二:對函數參數進行了解構賦值,但沒進行參數裏面內容的解構賦值,因此只有在沒有傳入參數的時候,默認值才生效,傳入參數,默認值久被覆蓋,返回undefined函數式編程

m1() //[0,0]
m2() //[0,0]

m1({x:3,y:8}) //[3,8]
m2({x:3,y:8}) //[3,8]

m1({x:3}) //[3,0]
m2({x:3}) //[3,undefined]

m1({}) //[0,0]
m2({}) //[undefined,undefined]

m1({z:3}) //[0,0]
m2({z:3}) //[undefined,undefined]

參數默認的位置

當定義參數默認值的時候,應該是函數的尾參數函數

function f(x=1,y){
    return [x,y]
}
f() // [1, undefined]
f(2) // [2, undefined]
f(, 1) // 報錯
f(undefined, 1) // [1, 1]

function f(x, y = 5, z) {
  return [x, y, z];
}

f() // [undefined, 5, undefined]
f(1) // [1, 5, undefined]
f(1, ,2) // 報錯
f(1, undefined, 2) // [1, 5, 2]

函數的length屬性

length屬性的含義:該函數預期傳入的參數個數fetch

(function (a){...}).length //1

當參數指定了默認值以後,length將返回沒有指定默認值的參數個數

(function (a=5){}).length // 0
(function (a, b, c = 5) {}).length // 2

當指定了默認值的參數不是尾參數,length將再也不計入後面的參數

(function (a = 0, b, c) {}).length // 0
(function (a, b = 1, c) {}).length // 1

做用域

當給參數設置了默認值,當函數初始化時,參數會造成一個單獨的做用域,初始化結束,做用域消失

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: x is not defined

下面代碼中,參數x = x造成一個單獨做用域。
實際執行的是let x = x,因爲暫時性死區的緣由,這行代碼會報錯」x 未定義「。

var x = 1;
function foo(x = x) {...}
foo() // ReferenceError: x is not defined

若是參數的默認值是一個函數,該函數的做用域也遵照這個規則

let foo = 'outer'
function bar(func = () =>foo){
    let foo = 'inner'
    console.log(func())
}
bar() //outer

若是寫成下面這樣,就會報錯。

function bar(func = () => foo) {
  let foo = 'inner';
  console.log(func());
}
bar() // ReferenceError: foo is not defined
var x=1;
function foo(x,y=function(){x=2}){
    var x = 3
    y()
    console.log(x)
}
foo() //3
console.log(x) //1

解釋:

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

首先全局做用域中有一個x(x1),函數foo的參數(中間做用域)中有一個x(x2),函數foo內部有一個x(x3)
在foo參數造成的做用域裏聲明瞭變量x,y.
y的默認值是一個函數,這個函數內部的x,指向同一個做用域的x.
在foo內部造成的做用域裏聲明瞭變量x,由於和x1不在同一個做用域,因此不一樣於x1,是x3
因此執行y後,內部變量x和外部全局變量x的值都沒變

中間做用域
若是將var x = 3的var刪掉,函數內部的x指向外部的x
去掉var,爲何是2
而這個爲何就是4了!

應用

  • 利用參數默認值,能夠指定某一個參數不得省略,若是省略就拋出一個錯誤。

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

rest參數

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

function add(...values){
    let sum = 0
    for(var val og values){
        sum += val
    }
    return sum
}
add(2,5,3) //10

下面是一個 rest 參數代替arguments變量的例子。

// arguments變量的寫法
function sortNumbers() {
  return Array.prototype.slice.call(arguments).sort();
}

// rest參數的寫法
const sortNumbers = (...numbers) => numbers.sort();

/*arguments對象不是數組,而是一個相似數組的對象。
因此爲了使用數組的方法,必須使用Array.prototype.slice.call先將其轉爲數組。*/
下面是一個利用 rest 參數改寫數組push方法的例子

function push(array,...items){
    items.forEach(function(item){
        array.push(item)
    })
}
var a = [];
push(a, 1, 2, 3)

注意:rest 參數以後不能再有其餘參數(即只能是最後一個參數),不然會報錯。
函數的length屬性,不包括 rest 參數

(function(a) {}).length  // 1
(function(...a) {}).length  // 0
(function(a, ...b) {}).length  // 1

嚴格模式

函數參數使用了默認值、解構賦值、或者擴展運算符,
那麼函數內部就不能顯式設定爲嚴格模式,不然會報錯
兩種方法能夠規避這種限制。第一種是設定全局性的嚴格模式,這是合法的。

'use strict';

function doSomething(a, b = a) {
  // code
}

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

const doSomething = (function () {
  'use strict';
  return function(value = 42) {
    return value;
  };
}());

name屬性

返回該函數的函數名

var f = function () {};

// ES5
f.name // ""

// ES6
f.name // "f"

Function構造函數返回的函數實例,name屬性的值爲anonymous。
(new Function).name // "anonymous"

bind返回的函數,name屬性值會加上bound前綴。
function foo() {};
foo.bind({}).name // "bound foo"
(function(){}).bind({}).name // "bound "

箭頭函數

基本用法

var f = v => v;
// 等同於
var f = function (v) {
  return v;
};
  • 若是箭頭函數不須要參數或須要多個參數,就使用一個圓括號表明參數部分
var f=()=>5;
// 等同於
var f = function(){return 5};

var sum = (num1,num2)=>num1+num2
// 等同於
var sum = function(num1, num2) {
  return num1 + num2;
};
  • 若是箭頭函數的代碼塊部分多於一條語句,就要使用大括號將它們括起來,而且使用return語句返回
var sum = (num1, num2) => { return num1 + num2; }
  • 因爲大括號被解釋爲代碼塊,因此若是箭頭函數直接返回一個對象,必須在對象外面加上括號
// 報錯
let getTempItem = id => { id: id, name: "Temp" };

// 不報錯
let getTempItem = id => ({ id: id, name: "Temp" });
  • 若是箭頭函數只有一行語句,且不須要返回值,能夠採用下面的寫法
let fn = ()=>void doesNotReturn()
  • 箭頭函數能夠與變量解構結合使用。
const full = ({ first, last }) => first + ' ' + last;

// 等同於
function full(person) {
  return person.first + ' ' + person.last;
}
  • 簡化回調函數
[1,2,3].map(x=>x*x)

[1,2,3].map(function (x) {
  return x * x;
});

var result = values.sort((a,b)=>{a*b})

var result = values.sort(function(a,b){
    return a*b
})
  • rest參數和箭頭函數結合
const numbers = (...nums)=>nums
numbers(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]

注意點:

  • this對象,是定義生效時所在的對象,而不是使用時所在的對象
  • 不能夠看成構造函數,不可使用new命令
  • 不可使用arguments對象,該對象在函數體內不存在,能夠用rest參數代替
  • 不可使用yield命令,箭頭函數不能用做Generator函數
function foo(){
    setTimeout(()=>{
        console.log('id':this.id)
    },100)
}
var id = 21
foo.call({id:42}) //id:42

解釋:

上面代碼中,setTimeout()的參數是一個箭頭函數,
這個箭頭函數的定義生效是在foo函數生成時,而它的真正執行要等到 100 毫秒後。
若是是普通函數,執行時this應該指向全局對象window,這時應該輸出21。
可是,箭頭函數致使this老是指向函數定義生效時所在的對象(本例是{id: 42}),
因此打印出來的是42
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函數內部設置了2個定時器:
箭頭函數的this指向了定義時所在的做用域,因此是Timer函數
普通函數的this指向了運行時所在的做用域,因此是window(全局對象)
因此3100ms後,timer.s1被更新了3次,timer.s2一次都沒更新
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方法中箭頭函數的this指向handler對象.

this指向固定化,並非由於箭頭函數內部綁定this,而是箭頭函數沒有本身的this,
致使內部的this就是外層代碼塊的this,也是由於沒有this,就不能用做構造函數.

es6:
function foo(){
    setTimeout(()=>{
        console.log('id':this.id)
    },100)
}
es5:
function foo(){
    var _this = this
    setTimeout(funciton(){
        console.log('id',_this.id)
    },100)
}
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

下面代碼中,箭頭函數內部的變量arguments,實際上是函數foo的arguments變量

function foo(){
    setTimeout(()=>{
        console.log('args:',arguments)
    },100)
}
foo(2,4,6,8) //args:[2,4,6,8]

由於箭頭函數沒有本身的this,因此bind(),apply(),call()都不能改變this指向

(function (){
    return[(()=>this.x).bind({x:'inner'})()]    
}).call({x:'outer'})
//['outer']

不適用場合

  • 定義對象的方法
const cat = {
    lives:9,
    jump:()=>{
        this.lives--
    }
}

由於對象不構成單獨的做用域,因此此時的箭頭函數的this指向全局對象
若是是普通函數的this指向cat

  • 須要動態的this的時候
var button = document.getElementById('press')
button.addEventListener('click',()=>{
    this.classList.toggle('on')
})

上面代碼運行時,點擊按鈕會報錯,
由於button的監聽函數是一個箭頭函數,致使裏面的this就是全局對象。
若是改爲普通函數,this就會動態指向被點擊的按鈕對象。

嵌套的箭頭函數

* es5:
function insert(value) {
  return {into: function (array) {
    return {after: function (afterValue) {
      array.splice(array.indexOf(afterValue) + 1, 0, value);
      return array;
    }};
  }};
}

insert(2).into([1, 3]).after(1);
* es6:
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]

indexOf,存在返回對應位置,不存在返回-1
splice(新增/刪除位置,刪除的數量(若是爲0就不刪除),內容)

一個部署管道機制(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 plus = a=>a+1
const mult2 = a=>a*2
mult2(plus1(5))
// 12

reduce():

arr.reduce(function(prev,cur,index,arr){
    ...
},init)
arr:原數組
prev:上一次調用回調時的返回值.或者初始值init
cur:當前正在處理的元素
index:表示當前正在處理的數組元素的索引,若提供 init 值,則索引爲0,不然索引爲1
init:初始值

尾調用優化

概念

尾調用:函數式編程的一個概念,指某個函數的最後一步時調用另外一個函數

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

上面代碼中,函數f的最後一步是調用函數g,這就叫尾調用。
如下三種狀況,都不屬於尾調用。

// 狀況一:調用g以後還有賦值操做
function f(x){
  let y = g(x);
  return y;
}

// 狀況二:調用g以後還有別的操做
function f(x){
  return g(x) + 1;
}

// 狀況三:return的時undefined
function f(x){
  g(x);
}

尾調用不必定出如今函數尾部,只要是最後一步操做便可

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

m和n都屬於尾調用

尾調用優化

尾調用優化:只保留內層函數的調用幀

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

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

// 等同於
g(3);

若是全部函數都是尾調用,那麼徹底能夠作到每次執行時,調用幀只有一項,
這將大大節省內存。這就是「尾調用優化」的意義。

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

注意:目前只有 Safari 瀏覽器支持尾調用優化,Chrome 和 Firefox 都不支持

尾遞歸

遞歸:函數調用自身
尾遞歸:尾調用自身
下面代碼是一個階乘函數,計算n的階乘,最多須要保存n個調用記錄,複雜度 O(n) 。

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

factorial(5) // 120

改寫成尾遞歸,只保留一個調用記錄,複雜度 O(1)

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

factorial(5, 1) // 120

非尾遞歸的 Fibonacci 數列實現以下

function Fibonacci (n){
    if(n<=1){return 1}
    return Fibonacci(n-1)+Fibonacci(n-2)
}
Fibonacci(10) // 89
Fibonacci(100) // 超時
Fibonacci(500) // 超時
function Fibonacci(n,ac1=1,ac2=1){
    if(n<=1){return ac2}
    return Fibonacci(n-1,ac2,ac1+ac2)
}

Fibonacci2(100) // 573147844013817200000
Fibonacci2(1000) // 7.0330367711422765e+208
Fibonacci2(10000) // Infinity
ES6 中只要使用尾遞歸,就不會發生棧溢出(或者層層遞歸形成的超時),相對節省內存

遞歸函數的改寫

若是想採用尾遞歸的實現,須要改寫遞歸函數,確保最後一步只調用自身
作到這一點的方法,就是把全部用到的內部變量改寫成函數的參數
計算5的階乘,怎麼作才能夠只傳入一個參數:

方法一:是在尾遞歸函數以外,再提供一個正常形式的函數

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

function factorial(n) {
  return tailFactorial(n, 1);
}

factorial(5) // 120

柯里化:將多參數的函數轉換成單參數的形式

看不懂!

方法二:es6的函數默認值

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

遞歸的本質是循環操做,
純粹的函數式編程語言沒有循環操做命令,全部的循環都用遞歸實現,
這就是爲何尾遞歸對這些語言極其重要。

嚴格模式

ES6的尾調用優化直再嚴格模式下開啓,正常模式無效
這是由於在正常模式下,函數內部有兩個變量,能夠跟蹤函數的調用棧。

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

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

function restricted() {
  'use strict';
  restricted.caller;    // 報錯
  restricted.arguments; // 報錯
}
restricted();

尾遞歸優化的實現

(https://es6.ruanyifeng.com/?s...[沒看!]

(https://segmentfault.com/a/11...

函數參數的尾逗號

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

此前,函數定義和調用時,都不容許最後一個參數後面出現逗號。

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

clownsEverywhere(
  'foo',
  'bar'
);

上面代碼中,若是在param2或bar後面加一個逗號,就會報錯。

若是像上面這樣,將參數寫成多行(即每一個參數佔據一行),之後修改代碼的時候,想爲函數clownsEverywhere添加第三個參數,或者調整參數的次序,就勢必要在原來最後一個參數後面添加一個逗號。這對於版本管理系統來講,就會顯示添加逗號的那一行也發生了變更。這看上去有點冗餘,所以新的語法容許定義和調用時,尾部直接有一個逗號。

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

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

這樣的規定也使得,函數參數與數組和對象的尾逗號規則,保持一致了。

Function.prototype.toString()

修改後的toString()方法,明確要求返回如出一轍的原始代碼。

function /* foo comment */ foo () {}
原來:
foo.toString()
// function foo() {}

es6:
foo.toString()
// "function /* foo comment */ foo () {}"

catch

之前明確要求catch命令後面必須跟參數
try {
  // ...
} catch (err) {
  // 處理錯誤
}

ES2019 作出了改變,容許catch語句省略參數。
try {
  // ...
} catch {
  // ...
}
相關文章
相關標籤/搜索