本文總結了js中函數相關的大部分用法,對函數用法不是特別清晰的同窗能夠了解一下。javascript
同其餘語言不一樣的是,js中的函數有2種含義。java
普通函數:同其餘語言的函數同樣,是用於封裝語句塊,執行多行語句的語法結構。node
構造函數:不要把它看成函數,把它看成class,內部可使用this表示當前對象。git
【注】後續代碼基於ES6&ES7標準,筆者是在nodejs v10.7.0環境下運行(你也能夠選擇其餘支持ES6的node版本)。es6
雖然普通函數和構造函數,含義有所不一樣,但是聲明方法卻徹底同樣。github
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;
}
複製代碼
let sort = function (arr) {
let ret = [...arr];
...
...
return ret;
}
複製代碼
函數表達式和普通函數聲明的區別在於,普通函數聲明會提高
,函數表達式不會提高
。編程
「提高」的意思是說: 在函數聲明前就能夠調用這個函數。沒必要先聲明後調用。c#
js會在運行時,將文件內全部的
函數聲明
,都提高到文件最頂部,這樣你能夠在代碼任意位置訪問這個函數。數組而如今根據ES6標準,使用
var
修飾的函數表達式會提高
,使用let
修飾的則不會提高。promise
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中的聲明徹底相同。
這種聲明方式,沒有發現有什麼優勢,並不推薦使用。
閉包,簡單說就是在函數中聲明的函數,也就是嵌套函數。它可以延長父做用域部分變量的生命週期。
閉包能夠直接使用其所在函數的任何變量,這種使用是引用傳遞
,而不是值傳遞
,這一點很重要。
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()
可直接訪問並修改所在函數中的變量arr
和idx
。
通常說來,閉包須要實現尾遞歸優化。
尾遞歸是指,若是一個函數,它的最後一行代碼是一個閉包的時候,會在函數返回時,釋放父函數的棧空間。
這樣一來,依賴閉包的遞歸函數就不怕棧溢出了(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 */
複製代碼
咱們常常在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,不用擔憂出現任何問題。
因此比較強烈推薦使用新的匿名函數寫法。
下面來介紹構造函數,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
的類相對比,兩者除了寫法不一樣以外,上述關鍵元素也都包含了。
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中,能夠想一想爲何。
js函數中繞不過的一個問題就是,方法裏面的this到底指向哪裏?
最官方的說法是:this指向調用此方法的對象。
對於相似於java這種面向對象的語言來說,this永遠指向所在類的對象實例。
對於js中也是這樣,若是咱們規規矩矩地像上一節介紹的那樣使用,this也會指向所在類的對象實例。
可是,js也提供了更爲靈活的語法,它可讓一個方法被不一樣的對象調用,即便不是同一個類的對象,也就是能夠將同一個函數的this,設爲不一樣的值。
這是一個極爲靈活的語法,能夠完成其餘語言相似接口(interface)
,擴展(extension)
,模版(template)
的功能。
實現此功能的方法有2個:apply
和call
,兩者實現的功能徹底相同,即改變函數的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)
*/
複製代碼
面向對象3大特徵:封裝,繼承,多態,其中最重要的就是繼承,多態也依賴於繼承的實現。能夠說實現了繼承,就實現了面向對象。
java中的繼承很簡單:
class Student extends Person{
... ...
}
複製代碼
Student繼承以後自動得到Person的全部成員變量和成員方法。
所以,咱們在實現js繼承的時候,主要就是獲取到父類的成員變量和成員方法。
最簡單的實現就是,將父類的成員變量和方法直接copy到子類中。
這須要作2件事:
function Student(name, age){
Person.call(self, name, age);
}
Student.prototype = Person.prototype;
複製代碼
上面代碼能夠達到繼承的目的,可是會產生兩個問題
因此咱們不能直接將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
複製代碼
固然實現一個完整的繼承還須要完善其餘諸多功能,在這裏咱們已經解決了最根本的問題。
generator是ES6中提供的一種異步編程的方案。有點像其餘語言(lua, c#)中的協程。
它可讓程序在不一樣函數中跳轉,並傳遞數據。
看下面的代碼:
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()
函數另外還有一些其餘規則:
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的yield
和next
特性,來作異步操做。
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函數可以把異步操做寫成同步形式,從而避免了回調地獄的問題。
異步變成同步,不知道可以避免多少由於回調,做用域產生的問題,代碼邏輯也能急劇簡化。
雖然咱們能夠經過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來進行異步操做。
使用generator
+ Promise
+ co.js
能夠較爲方便地實現異步轉同步。
而js的新標準中,上面的操做已經提供了語法層面的支持,並將異步轉同步的寫法,簡化成了2個關鍵字:await
和async
。
一樣實現上節中的異步調用功能,代碼以下:
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使用規則以下:
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 */
複製代碼
--完--