javascript函數全解

0.0 概述

本文總結了js中函數相關的大部分用法,對函數用法不是特別清晰的同窗能夠了解一下。javascript

1.0 簡介

同其餘語言不一樣的是,js中的函數有2種含義。java

普通函數:同其餘語言的函數同樣,是用於封裝語句塊,執行多行語句的語法結構。node

構造函數:不要把它看成函數,把它看成class,內部可使用this表示當前對象。git

【注】後續代碼基於ES6&ES7標準,筆者是在nodejs v10.7.0環境下運行(你也能夠選擇其餘支持ES6的node版本)。es6

1.1 函數的聲明

雖然普通函數和構造函數,含義有所不一樣,但是聲明方法卻徹底同樣。github

1.1.0 函數聲明

function sort(arr) {
    let ret = [...arr];
    let length = ret.length;
    for (let i = 0; i < length; i++) {
        for (let j = i + 1; j < length; j++) {
            if (ret[i] > ret[j]) {
                [ret[j], ret[i]] = [ret[i], ret[j]];
            }
        }
    }
    return ret;
}
複製代碼

1.1.1 函數表達式

let sort = function (arr) {
    let ret = [...arr];
    ...
    ...
    return ret;
}
複製代碼

函數表達式和普通函數聲明的區別在於,普通函數聲明會提高,函數表達式不會提高編程

「提高」的意思是說: 在函數聲明前就能夠調用這個函數。沒必要先聲明後調用。c#

js會在運行時,將文件內全部的函數聲明,都提高到文件最頂部,這樣你能夠在代碼任意位置訪問這個函數。數組

而如今根據ES6標準,使用var修飾的函數表達式會提高,使用let修飾的則不會提高。promise

1.1.2 使用Function構造函數聲明

let sort = new Function("arr", ` function sort(arr) { let ret = [...arr]; let length = ret.length; for (let i = 0; i < length; i++) { for (let j = i + 1; j < length; j++) { if (ret[i] > ret[j]) { [ret[j], ret[i]] = [ret[i], ret[j]]; } } } return ret; } `);
複製代碼

這種使用Function構造方法建立的函數,同函數聲明產生的函數是徹底相同的。

構造函數接收多個字符串做爲參數,最後一個參數表示函數體,其餘參數表示參數名

像上面這個例子和1.1.0中的聲明徹底相同。

這種聲明方式,沒有發現有什麼優勢,並不推薦使用。

1.2 閉包

閉包,簡單說就是在函數中聲明的函數,也就是嵌套函數。它可以延長父做用域部分變量的生命週期。

閉包能夠直接使用其所在函數的任何變量,這種使用是引用傳遞,而不是值傳遞,這一點很重要。

let f = function generator() {
    let arr = [1, 2, 3, 4, 5, 6, 7];
    let idx = 0;
    return {
        next() {
            if (idx >= arr.length) {
                return { done: true };
            } else {
                return { done: false, value: arr[idx++] };
            }
        }
    }
}
let gen = f();
for (let i = 0; i < 10; i++) {
    console.log(gen.next());
}

複製代碼

上面的代碼中,generator函數中的閉包next()可直接訪問並修改所在函數中的變量arridx

通常說來,閉包須要實現尾遞歸優化。

尾遞歸是指,若是一個函數,它的最後一行代碼是一個閉包的時候,會在函數返回時,釋放父函數的棧空間。

這樣一來,依賴閉包的遞歸函數就不怕棧溢出了(nodejs在64位機器上可達到1萬多層的遞歸纔會溢出,有多是根據內存狀況動態計算的)。

ES6明確要求支持尾遞歸。

而據網絡上資料說,nodejs須要在嚴格模式下,使用--harmony選項,能夠開啓尾遞歸。

然而我使用下列代碼發現,並無開啓(nodejs版本爲v10.3.0)。

// File: test.js
// Run: node --harmony test.js
 "use strict"

function add(n, sum) {
    if (n == 0) {
        console.trace();
        return sum;
    } else {
        return add(n - 1, sum + n);
    }
}
console.log(add(10, 0));
/* 輸出爲: Trace at add (/Users/hongyuwang/Desktop/javascript/learn/learn.js:5:11) at add (/Users/hongyuwang/Desktop/javascript/learn/learn.js:8:10) at add (/Users/hongyuwang/Desktop/javascript/learn/learn.js:8:10) at add (/Users/hongyuwang/Desktop/javascript/learn/learn.js:8:10) at add (/Users/hongyuwang/Desktop/javascript/learn/learn.js:8:10) at add (/Users/hongyuwang/Desktop/javascript/learn/learn.js:8:10) at add (/Users/hongyuwang/Desktop/javascript/learn/learn.js:8:10) at add (/Users/hongyuwang/Desktop/javascript/learn/learn.js:8:10) at add (/Users/hongyuwang/Desktop/javascript/learn/learn.js:8:10) at add (/Users/hongyuwang/Desktop/javascript/learn/learn.js:8:10) 55 */

複製代碼

1.3 匿名函數

咱們常常在js的代碼中看見下面這種寫法:

(function(){
	...
	...
	...
})();
複製代碼

將一個匿名函數直接執行,若是剛接觸js的同窗可能以爲這是脫褲子放屁。

可是這個匿名函數的最大做用在於做用域隔離,不污染全局做用域。

若是沒有匿名函數包裹,代碼中聲明的全部變量都會出如今全局做用域中,形成沒必要要的變量覆蓋麻煩和性能上的損失。

ES6中這種寫法能夠拋棄了,由於ES6引入了塊做用域

{
	...
	...
	...
}

複製代碼

做用和上面的匿名函數相同。

另外ES6中增長了一種匿名函數的寫法:

//ES6之前的寫法
function Teacher(name){
	this.name = name;
	var self = this;
	setTimeout(function(){
		console.log('Teacher.name = ' + self.name);
	}, 3000);
}

//如今這樣寫
function Student(name){
	this.name = name;
	setTimeout(() => {
		console.log('Student.name = ' + this.name);
	}, 3000);
}

複製代碼

新的匿名函數的在寫法上有2處不一樣:

  • 去掉了function關鍵字
  • 在參數列表和函數體之間增長了=>符號

而它也帶來了一個巨大的好處:

匿名函數中的this對象老是指向聲明時所在的做用域的this,再也不指向調用時候的this對象了。

這樣咱們就能夠像上面的例子那樣,很直觀地使用this,不用擔憂出現任何問題。

因此比較強烈推薦使用新的匿名函數寫法。

1.4 構造函數和this

1.4.1 基本面向對象語法

下面來介紹構造函數,js沒有傳統面向對象的語法,可是它可使用函數來模擬。

瞭解js面向對象機制以前,能夠先看一下,其餘標準面嚮對象語言的寫法,好比java,咱們聲明一個類。

class Person{
	//構造函數
	Person(String name, int age){
		this.name = name;
		this.age = age;
		Person.count++;
	}
	//屬性
	String name;
	int age;
	//setter&getter方法
	String getName(){
		return this.name;
	}
	void setName(String name){
		this.name = name;
	}
	int getAge(){
		return this.age;
	}
	void setAge(int age){
		this.age = age;
	}
	//靜態變量
	static int count = 0;
	//靜態方法
	public int getInstanceCount(){
		return Person.count;
	}
}
複製代碼

由此可知,一個類主要包含以下元素:構造函數屬性方法靜態屬性靜態方法

在js中,咱們可使用js的構造函數,來完成js中的面向對象。

js的構造函數就是用來作面向對象聲明(聲明)的。

構造函數的聲明語法同普通函數徹底相同。

//構造函數
function Person(name, age){
	//屬性
	this.name = name;
	this.age = age;
	
	//setter&getter
	this.getName = function(){
		return this.name;
	}
	this.setName = function(name){
		this.name = name;
	}
	this.getAge = function(){
		return this.age;
	}
	this.setAge = function(age){
		this.age = age;
	}
	
	Person.count++;
}

//靜態變量
Person.count = 0;

//靜態方法
Person.getInstanceCount = function(){
	return Person.count;
}
複製代碼

能夠發現,構造函數中同普通函數相比,特別的地方在於使用了this,同其餘面向對象的語言同樣,this表示當前的實例對象。

把咱們用js聲明的類與java的類相對比,兩者除了寫法不一樣以外,上述關鍵元素也都包含了。

1.4.2 prototype

js使用上面的方法聲明瞭類以後,就可使用new關鍵字來建立對象了。

let person = new Person("kaso", 20);
console.log("person.name=" + person.getName() + ", person.age=" + person.getAge());
//輸出:person.name=kaso, person.age=20
let person1 = new Person("jason", 25);
console.log("person.name=" + person.getName() + ", person.age=" + person.getAge());
//輸出:person.name=jason, person.age=25
複製代碼

建立對象,訪問屬性,訪問方法,都沒問題,看起來挺好的。

可是當咱們執行一下這段代碼,會發現有些不對:

console.log(person.getName === person1.getName);
//輸出:false
複製代碼

原來構造函數在執行的時候,會將全部成員方法,爲每一個對象生成一份copy,而對於類成員函數來講,保留一份copy就足夠了,而不一樣的對象能夠用this來區分。上面的作法很明顯,內存被白白消耗了。

基於上述問題,js引入了prototype關鍵字並規定:

存儲在prototype中的方法和變量能夠在類的全部對象中共享。

所以,上面的構造函數能夠修改爲這樣:

function Person(name, age){
	this.name = name;
	this.age = age;
	
	Person.count++;
}

Person.prototype.getName = function(){
	return this.name;
}

Person.prototype.setName = function(name){
	this.name = name;
}

Person.prototype.getAge = function(){
	return this.age;
}

Person.prototype.setAge = function(age){
	this.age = age;
}

Person.count = 0;

Person.getInstanceCount = function(){
	return Person.count;
}

複製代碼

運行效果和以前的寫法相同,只是此次建立不一樣的對象時,成員方法再也不建立多個副本了。

須要注意的是,成員變量不須要放到prototype中,能夠想一想爲何。

1.4.3 apply和call

js函數中繞不過的一個問題就是,方法裏面的this到底指向哪裏?

最官方的說法是:this指向調用此方法的對象。

對於相似於java這種面向對象的語言來說,this永遠指向所在類的對象實例。

對於js中也是這樣,若是咱們規規矩矩地像上一節介紹的那樣使用,this也會指向所在類的對象實例。

可是,js也提供了更爲靈活的語法,它可讓一個方法被不一樣的對象調用,即便不是同一個類的對象,也就是能夠將同一個函數的this,設爲不一樣的值。

這是一個極爲靈活的語法,能夠完成其餘語言相似接口(interface)擴展(extension)模版(template)的功能。

實現此功能的方法有2個:applycall,兩者實現的功能徹底相同,即改變函數的this指向,只是函數傳遞參數方式不一樣。

call接受可變參數,同函數調用同樣,需將參數一一列出。
apply只接受2個參數,第一個就是新的this指向的對象,第二個參數是原參數用數組保存起來。
代碼以下:

let obj = {
	print(a, b, c){
		console.log(`this is obj.print(${a}, ${b}, ${c})`);
	}
}

let obj1 = {
	print(a, b, c){
		console.log(`this is obj1.print(${a}, ${b}, ${c})`);
	}
}

function test(a, b, c){
	this.print(a, b, c);
}

test.apply(obj, [1, 2, 3]);
test.call(obj, 4, 5, 7);

test.apply(obj1, [1, 2, 3]);
test.call(obj1, 4, 5, 7);

/* 輸出:
this is obj.print(1, 2, 3)
this is obj.print(4, 5, 7)
this is obj1.print(1, 2, 3)
this is obj1.print(4, 5, 7)
*/
複製代碼

1.4.4 繼承

面向對象3大特徵:封裝,繼承,多態,其中最重要的就是繼承,多態也依賴於繼承的實現。能夠說實現了繼承,就實現了面向對象。

java中的繼承很簡單:

class Student extends Person{
    ... ...
}
複製代碼

Student繼承以後自動得到Person的全部成員變量和成員方法。

所以,咱們在實現js繼承的時候,主要就是獲取到父類的成員變量和成員方法。

最簡單的實現就是,將父類的成員變量和方法直接copy到子類中。

這須要作2件事:

  • 爲了copy成員方法,能夠將Student的prototype指向父類的prototype
  • 爲了copy成員屬性,子類構造函數須要調用父類構造函數
function Student(name, age){
	Person.call(self, name, age);
}

Student.prototype = Person.prototype;

複製代碼

上面代碼能夠達到繼承的目的,可是會產生兩個問題

  • 若是我向Student中添加新的成員方法時,會同時加入到父類中
  • 多層次繼承沒法實現,即當所調用的方法在父類中找不到的時候,不會去父類的父類中去查找

因此咱們不能直接將Person.prototype直接給Student.prototype。

通過思考,一個可行方案是,令子類prototype指向父類的一個對象,即像這樣:

Student.prototype = new Person();
複製代碼

這樣作,能夠解決上面的2個問題。

可是它仍然有些瑕疵:會調用2次父類構造函數,形成必定的性能損失。

因此咱們的終極繼承方案是這樣的:

function Student(name, age){
	Person.call(self, name, age);
}

function HelpClass(){}
HelpClass.prototype = Person.prototype;
Student.prototype = new HelpClass();
複製代碼

上面關鍵代碼的意義在於,用一個空的構造函數代替父類構造函數,這樣調用了一個空構造函數的代價會小於調用父類構造函數。

另外上述代碼能夠用Object.create函數簡化:

function Student(name, age){
	Person.call(self, name, age);
}

Student.prototype = Object.create(Person.prototype);
複製代碼

這就是咱們最終的繼承方案了。能夠寫成下面的通用模式。

function extend(superClass){
	function subClass(){
		superClass.apply(self, arguments);
	}
	subClass.prototype = Object.create(superClass.prototype);
	
	return subClass;
}

let Student = extend(Person);

let s = new Student('jackson', '34');

console.log("s.getName() = " + s.getName() + ", s.getAge() = " + s.getAge());

//輸出爲:s.getName() = jackson, s.getAge() = 34

複製代碼

固然實現一個完整的繼承還須要完善其餘諸多功能,在這裏咱們已經解決了最根本的問題。

1.5 generator函數和co

generator是ES6中提供的一種異步編程的方案。有點像其餘語言(lua, c#)中的協程。

它可讓程序在不一樣函數中跳轉,並傳遞數據。

1.5.1 基本用法介紹

看下面的代碼:

function *generatorFunc(){
   console.log("before yield 1");
   yield 1;
   console.log("before yield 2");
   yield 2;
   console.log("before yield 3");
   let nextTransferValue = yield 3;
   console.log("nextTransferValue = " + nextTransferValue);
}

let g = generatorFunc();
console.log("before next()");
console.log(g.next());
console.log(g.next());
console.log(g.next());
console.log(g.next(1024));
/*輸出: before next() before yield 1 { value: 1, done: false } before yield 2 { value: 2, done: false } before yield 3 { value: 3, done: false } nextTransferValue = 1024 { value: undefined, done: true } */
複製代碼

能夠看到generator函數有3要素:

  • 須要在函數名字前面,加上*
  • 須要在函數體中使用 yield
  • 調用的時候須要使用 next()函數

另外還有一些其餘規則:

  • generator函數內的第一行代碼,須要在第一個next()執行後執行
  • 函數在執行next()時,停頓在yield處,並返回yield後面的值,yield後的代碼再也不執行。
  • next() 返回的形式是一個對象:{value: XXX, done: false},這個對象中,value表示yield後面的值,done表示是否generator函數已經執行完畢,即全部的yield都執行過了。
  • next() 能夠帶參數,表示將此參數傳遞給上一個yield,由於上次執行next()的時候,代碼停留在上次yield的位置了,再執行next()的時候,會從上次yield的位置繼續執行代碼,同時能夠令yield表達式有返回值。

從上述介紹中能夠看出,generator除了在函數中跳轉以外,還能夠經過next()來返回不一樣的值。

瞭解過ES6的同窗應該知道,這種next()序列,特別符合迭代器的定義。

所以,咱們能夠很容易把generator的函數的返回值組裝成數組,還能夠用for..of表達式來遍歷。

function *generatorFunc(){
   yield 1;
   yield 2;
   yield 3;
}

let g = generatorFunc();
for(let i of g){
	console.log(i);
}

/* 輸出: 1 2 3 */
複製代碼
function *generatorFunc(){
   yield 1;
   yield 2;
   yield 3;
}

let g = generatorFunc();
console.log(Array.from(g));

/* 輸出: [1, 2, 3] */
複製代碼

除了上述規則外,generator還有一個語法yield *,它能夠鏈接另外一個generator函數,相似於普通函數間調用。用於一個generator函數調用另外一個generator函數,也可用於遞歸。

function *generatorFunc(){
    yield 3;
    yield 4;
    yield 5;
}

function *generatorFunc1(){
    yield 1;
    yield 2;
    yield * generatorFunc();
    yield 6;
}
 
let g = generatorFunc1();
console.log(Array.from(g));

/* 輸出: [1, 2, 3, 4, 5, 6] */

複製代碼

除了獲取數組外,咱們還可使用generator的yieldnext特性,來作異步操做。

js中的異步操做咱們通常使用Promise來實現。

請看下列代碼及註釋。

let g = null;
function *generatorFunc(){
	//第一個請求,模擬3s後臺操做
    let request1Data = yield new Promise((resolve, reject) => {
        setTimeout(()=>{
            resolve("123");
        }, 3000);
    }).then((d) => {
    	 //令函數繼續運行,並把promise返回的數據經過next傳給上一個yield,代碼會運行到下一個yield
        g.next(d);
    });

	 //輸出第一個請求的結果
    console.log('request1Data = ' + request1Data);

	 //同上,開始第二個請求
    let request2Data = yield new Promise((resolve, reject) => {
        setTimeout(()=>{
            resolve("456");
        }, 3000);
    }).then((d) => {
        g.next(d);
    });
	
	 //第二個請求
    console.log('request2Data = ' + request2Data);
 }
 
 g = generatorFunc();
 g.next();
 console.log('completed');
 /* 輸出: completed(立刻輸出) request1Data = 123(3s後輸出) request2Data = 456(6s後輸出) */
複製代碼

咱們換一種寫法:

let g = null;

function *request1(){
    return yield new Promise((resolve, reject) => {
        setTimeout(()=>{
            resolve("123");
        }, 3000);
    }).then((d) => {
        g.next(d);
    });
}

function *request2(){
    return yield new Promise((resolve, reject) => {
        setTimeout(()=>{
            resolve("456");
        }, 3000);
    }).then((d) => {
        g.next(d);
    });
}

function *generatorFunc(){
    let request1Data = yield *request1();
    console.log('request1Data = ' + request1Data);
    let request2Data = yield *request2();
    console.log('request2Data = ' + request2Data);
 }
 
 g = generatorFunc();
 g.next();
 console.log('completed');
 /* 輸出同上 */
複製代碼

運行結果是相同的,因此咱們能夠看到,generator函數可以把異步操做寫成同步形式,從而避免了回調地獄的問題。

異步變成同步,不知道可以避免多少由於回調,做用域產生的問題,代碼邏輯也能急劇簡化。

1.5.2 generator函數的自動運行

雖然咱們能夠經過generator消除異步代碼,可是使用起來仍是不太方便的。

須要把generator對象提早聲明保存,而後還要在異步的結果處寫next()

通過觀察發現,這些方法的出現都是有規律的,因此能夠經過代碼封裝來將這些操做封裝起來,從而讓generator函數的運行,就像普通函數同樣。

提供這樣功能的是co.js(能夠點這裏跳轉),大神寫的插件,用於generator函數的自動運行,簡單的說它會幫你自動執行next()函數,因此藉助co.js,你只須要編寫yield和異步函數便可。

使用co.js,上面的異步代碼能夠寫成這樣:

let co = require('./co');

function *request1(){
    return yield new Promise((resolve, reject) => {
        setTimeout(()=>{
            resolve("123");
        }, 3000);
    });
}

function *request2(){
    return yield new Promise((resolve, reject) => {
        setTimeout(()=>{
            resolve("456");
        }, 3000);
    });
}

function *generatorFunc(){
    let request1Data = yield *request1();
    console.log('request1Data = ' + request1Data);
    let request2Data = yield *request2();
    console.log('request2Data = ' + request2Data);
 }
 co(generatorFunc);
 console.log('completed');
 /* 輸出同上 */
複製代碼

能夠看到,藉助co.js你只須要寫yield就可以把異步操做寫成同步調用的形式。

注意,請使用promise來進行異步操做。

1.6 async和await

使用generator + Promise + co.js能夠較爲方便地實現異步轉同步。

而js的新標準中,上面的操做已經提供了語法層面的支持,並將異步轉同步的寫法,簡化成了2個關鍵字:awaitasync

一樣實現上節中的異步調用功能,代碼以下:

async function request1(){
    return await new Promise((resolve, reject) => {
        setTimeout(()=>{
            resolve("123");
        }, 3000);
    });
}

async function request2(){
    return await new Promise((resolve, reject) => {
        setTimeout(()=>{
            resolve("456");
        }, 3000);
    });
}

async function generatorFunc(){
    let request1Data = await request1();
    console.log('request1Data = ' + request1Data);
    let request2Data = await request2();
    console.log('request2Data = ' + request2Data);
 }

 generatorFunc();
 
 console.log('completed');
 
 /* 輸出同上 */
複製代碼

await/async使用規則以下:

  • await只能用在async函數中。
  • await後面能夠接任何對象。
  • 若是await後面接的是普通對象(非Promise,非async),則會立刻返回,至關於沒寫await。
  • 若是await後面是Promise對象,await會等待Promise的resolve執行後,纔會繼續向下執行,而後await會返回resolve傳遞的參數。
  • 若是await後面是另外一個async函數,則會等待另外一個async完成後繼續執行。
  • 調用一個async函數會返回一個Promise對象,async函數中的返回值至關於調用了Promise的resolve方法,async函數中拋出異常至關於調用了Promise的reject方法。
  • 經過上一條規則可知,雖然await/async使用了Promise來執行異步,可是咱們卻能夠在使用這兩個個關鍵字的時候,不寫任何的Promise。
  • 另外,若是await後面的表達式可能拋出異常,則須要在await語句上增長try-catch語句,不然異常會致使程序執行中斷。

await/async自己就是用來作異步操做轉同步寫法的,它的規則和用法也很明確,只要牢記上面幾點,你就能用好它們。

//拋出異常的async方法
async function generatorFunc1(){
    console.log("begin generatorFunc1");
    throw 1001;
}

//async方法返回的是Promise對象,使用Promise.catch捕獲異常
generatorFunc1().catch((e) => {
    console.log(`catch error '${e}' in Promise.catch`);
})

//正常帶返回值的async方法
async function generatorFunc2(){
    console.log("begin generatorFunc2");
    return 1002;
}

//async方法返回的是Promise對象,使用Promise.then獲取返回的數據
generatorFunc2().then((data)=>{
    console.log(`data = ${data}`);
})

//await後帶的async方法若拋出異常,能夠在await語句增長try-catch捕獲異常
async function generatorFunc3(){
    console.log("begin generatorFunc3");
    try{
        await generatorFunc1();
    }catch(e){
        console.log(`catch error '${e}' in generatorFunc3`);
    }
}

generatorFunc3();

console.log('completed');
/* 輸出: begin generatorFunc1 begin generatorFunc2 begin generatorFunc3 begin generatorFunc1 completed catch error '1001' in Promise.catch data = 1002 catch error '1001' in generatorFunc3 */
複製代碼

--完--

相關文章
相關標籤/搜索