在 ES6 中,箭頭函數是其中最有趣也最受歡迎的新增特性。javascript
本文會分爲三個部分來介紹:前端
第一部主要介紹箭頭函數的基本語法與使用方式,其中關於this的指向
問題會着重介紹。java
第二部分探究一下箭頭函數在自執行函數
中的奇怪現象。node
第三部分將提供一些面試題目
,用於幫助你們理解。git
顧名思義,箭頭函數是一種使用 (=>) 定義函數的新語法,它與傳統的 ES5 函數有些許不一樣。es6
這是一個用 ES5 語法編寫的函數:github
function addTen(num){
return num + 10;
}
addTen(5); // 15
複製代碼
有了 ES6 的箭頭函數後,咱們能夠用箭頭函數這樣表示:面試
var addTen = num => num + 10
addTen(5); // 15
複製代碼
箭頭函數的寫法短的多!因爲隱式返回,咱們能夠省略花括號和 return 語句。瀏覽器
與常規 ES5 函數相比,瞭解箭頭函數的行爲方式很是重要。微信
基礎語法以下:
(參數)=> { statements }
複製代碼
接下來,拆解一下箭頭函數的各類書寫形式:
當沒有參數時,使用一個圓括號表明參數部分
let f = ()=> 5;
f(); // 5
複製代碼
當只有一個參數時,能夠省略圓括號。
let f = num => num + 5;
f(10); // 15
複製代碼
當有多個參數時,在圓括號內定義多個參數用逗號分隔。
let f = (a,b) => a + b;
f(1,2); // 3
複製代碼
當箭頭函數的代碼塊部分多餘一條語句,就須要使用大括號括起來,而且使用 return 語句。
// 沒有大括號,默認返回表達式結果
let f1 = (a,b) => a + b
f1(1,2) // 3
// 有大括號,無return語句,沒有返回值
let f2 = (a,b) => {a + b}
f2(1,2) // undefined
// 有大括號,有return語句,返回結果
let f3 = (a,b) => {return a + b}
f3(1,2) // 3
複製代碼
因爲大括號被解釋爲代碼塊,因此若是箭頭函數直接返回一個對象,必須在對象外面加上括號,不然會報錯。
//報錯
let f1 = num => {num:num}
//不報錯
let f2 = num => ({num:num})
複製代碼
箭頭函數沒有[[Construct]]方法,因此不能被用做構造函數。
let F = ()=>{};
// 報錯 TypeError: F is not a constructor
let f = new F();
複製代碼
因爲不能夠經過 new 關鍵字調用,於是沒有構建原型的需求,因此箭頭函數不存在 prototype 這個屬性。
let F = ()=>{};
console.log(F.prototype) // undefined
複製代碼
在箭頭函數中,不可使用 yield 命令,所以箭頭函數不能用做 Generator 函數。
箭頭函數中是沒有 arguments、super、new.target 的綁定,這些值由外圍最近一層非箭頭函數決定。
以 arguments 爲例,看以下代碼:
let f = ()=>console.log(arguments);
//報錯
f(); // arguments is not defined
複製代碼
因爲在全局環境下,定義箭頭函數 f,對於 f 來講,沒法獲取到外圍非箭頭函數的 arguments 值,因此此處報錯。
再看一個例子:
function fn(){
let f = ()=> console.log(arguments)
f();
}
fn(1,2,3) // [1,2,3]
複製代碼
上面的代碼,箭頭函數 f 內部的 arguments,實際上是函數 fn 的 arguments 變量。
若想在箭頭函數中獲取不定長度的參數列表,可使用 ES6 中的 rest 參數解決:
let f = (...args)=>console.log(args)
f(1,2,3,4,5) // [1,2,3,4,5]
複製代碼
在理解箭頭函數中的this指向問題以前,咱們先來回看在 ES5 中的一個例子:
var obj = {
value:0,
fn:function(){
this.value ++
}
}
obj.fn();
console.log(obj.value); // 1
複製代碼
這段代碼很簡單,在每次調用 obj.fn() 時,指望的是將 obj.value 加 1。
如今咱們將代碼修改一下:
var obj = {
value:0,
fn:function(){
var f = function(){
this.value ++
}
f();
}
}
obj.fn();
console.log(obj.value); // 0
複製代碼
咱們將代碼修改了一下,在 obj.fn 方法內增長了一個函數 f ,並將 obj.value 加 1 的動做放到了函數 f 中。可是因爲 javascript 語言設計上的一個錯誤,函數 f 中的 this 並非 方法 obj.fn 中的 this,致使咱們無法獲取到 obj.value 。
爲了解決此類問題,在 ES5 中,咱們一般會將外部函數中的 this 賦值給一個臨時變量(一般命名爲 that、_this、self),在內層函數中若但願使用外層函數的 this 時,經過這個臨時變量來獲取。修改代碼以下:
var obj = {
value:0,
fn:function(){
// 本人喜歡定義爲 _this,也有不少人喜歡定義成 that 或 self
var _this = this;
var f = function(){
_this.value ++
}
f();
}
}
obj.fn();
console.log(obj.value); // 1
複製代碼
從這個例子中,咱們知道了在 ES5 中如何解決內部函數獲取外部函數 this 的辦法。
而後咱們來看看箭頭函數相對於 ES5 中的函數來講,它的 this 指向有和不一樣。
先看一段定義,來源於ES6標準入門
箭頭函數體內的 this 對象就是定義時所在的對象,而不是使用時所在的對象。
那麼,如何來理解這句話呢?
咱們嘗試用babel來將以下代碼轉換成 ES5 格式的代碼,看看它都作了什麼。
function fn(){
let f = ()=>{
console.log(this)
}
}
複製代碼
來看看轉化後的結果,直接上圖:
咱們發現了什麼,竟然和咱們以前在 ES5 中解決內層函數獲取外層函數 this 的方法同樣,定義一個臨時變量 _this ~
那麼,箭頭函數本身的 this 哪裏去了?
答案是,箭頭函數根本沒有本身的 this !
那麼,咱們能夠總結一下,將晦澀難懂的定義轉化成白話文:
讓咱們用幾個例子,來驗證一下咱們總結的規則:
let obj = {
fn:function(){
console.log('我是普通函數',this === obj)
return ()=>{
console.log('我是箭頭函數',this === obj)
}
}
}
console.log(obj.fn()())
// 我是普通函數 true
// 我是箭頭函數 true
複製代碼
從上面的例子,咱們可以看出,箭頭函數的 this 與外層函數的 this 是相等的。
在看一個多層箭頭函數嵌套的例子:
let obj = {
fn:function(){
console.log('我是普通函數',this === obj)
return ()=>{
console.log('第一個箭頭函數',this === obj)
return ()=>{
console.log('第二個箭頭函數',this === obj)
return ()=>{
console.log('第三個箭頭函數',this === obj)
}
}
}
}
}
console.log(obj.fn()()()())
// 我是普通函數 true
// 第一個箭頭函數 true
// 第二個箭頭函數 true
// 第三個箭頭函數 true
複製代碼
在這個例子中,咱們可以知道,對於箭頭函數來講,箭頭函數的 this 與外層的第一個普通函數的 this 相等,與嵌套了幾層箭頭函數無關。
再來看一個沒有外層函數的例子:
let obj = {
fn:()=>{
console.log(this === window);
}
}
console.log(obj.fn())
// true
複製代碼
這個例子,證實了,在箭頭函數外層沒有普通函數時,箭頭函數的 this 與全局對象相等。
須要注意的是,瀏覽器環境下全局對象爲 window,node 環境下全局對象爲 global,驗證的時候須要區分一下。
看到這裏,相信你們已經知道了,箭頭函數中根本沒有本身的 this ,那麼當箭頭函數碰到 call、apply、bind 時,會發生什麼呢?
咱們知道,call 和 apply 的做用是改變函數 this 的指向,傳遞參數,並將函數執行, 而 bind 的做用是生成一個綁定 this 並預設函數參數的新函數。
然而因爲箭頭函數根本沒有本身的 this ,因此:
咱們來驗證一下:
window.name = 'window_name';
let f1 = function(){return this.name}
let f2 = ()=> this.name
let obj = {name:'obj_name'}
f1.call(obj) // obj_name
f2.call(obj) // window_name
f1.apply(obj) // obj_name
f2.apply(obj) // window_name
f1.bind(obj)() // obj_name
f2.bind(obj)() // window_name
複製代碼
上面代碼中,聲明瞭普通函數 f1,箭頭函數 f2。
普通函數的 this 指向是動態可變的,因此在對 f1 使用 call、apply、bind 時,f1 內部的 this 指向會發生改變。
箭頭函數的 this 指向在其定義時就已肯定,永遠不會發生改變,因此在對 f2 使用 call、apply、bind 時,會忽略傳入的上下文參數。
在 ES6 的箭頭函數出現以前,自執行函數通常會寫成這樣:
(function(){
console.log(1)
})()
複製代碼
或者寫成這樣:
(function(){
console.log(1)
}())
複製代碼
箭頭函數固然也能夠被用做自執行函數,能夠這樣寫:
(() => {
console.log(1)
})()
複製代碼
可是,令大多數人想不到的是,下面這種寫法會報錯:
(() => {
console.log(1)
}())
複製代碼
那麼,爲何會報錯呢?
這個問題,曾困擾了我好久,直到我翻閱了ECMAScript® 2015 規範,從中得知箭頭函數是屬於 AssignmentExpression 的一種,而函數調用屬於 CallExpression,規範中要求當 CallExpression 時,左邊的表達式必須是 MemberExpression 或其餘的 CallExpression,而箭頭函數不屬於這兩種表達式,因此在編譯時就會報錯。
原理就是這樣了,具體可參見ECMAScript® 2015 規範
在面試中關於箭頭函數的考察,主要集中在 arguments 關鍵字的指向和箭頭函數的this指向上,下面幾道題目,由淺入深,供你們參考一下。
function foo(n) {
var f = () => arguments[0] + n;
return f();
}
let res = foo(2);
console.log(res); // 問 輸出結果
複製代碼
答案: 4
箭頭函數沒有本身的 arguments ,因此題中的 arguments 指代的是 foo 函數的 arguments 對象。因此 arguments[0] 等於 2 ,n 等於 2,結果爲 4。
function A() {
this.foo = 1
}
A.prototype.bar = () => console.log(this.foo)
let a = new A()
a.bar() // 問 輸出結果
複製代碼
答案: undefined
箭頭函數沒有本身的 this,因此箭頭函數的 this 等價於外層非箭頭函數做用域的this。 因爲箭頭函數的外層沒有普通函數,因此箭頭函數中的 this 等價於全局對象,因此輸出爲 undefined。
let res = (function pt() {
return (() => this.x).bind({ x: 'inner' })();
}).call({ x: 'outer' });
console.log(res) // 問 輸出結果
複製代碼
答案:'outer'
此題稍微複雜一點,求 res 的輸出結果。
分析以下:
window.name = 'window_name';
let obj1 = {
name:'obj1_name',
print:()=>console.log(this.name)
}
let obj2 = {name:'obj2_name'}
obj1.print() // 問 輸出結果
obj1.print.call(obj2) // 問 輸出結果
複製代碼
答案:'window_name' 'window_name'
箭頭函數沒有本身的 this ,也沒法經過 call、apply、bind 改變箭頭函數中的 this。 箭頭函數的 this 取決於外層是否有普通函數,有普通函數 this 指向普通函數中的this,外層沒有普通函數,箭頭函數中的 this 就是全局對象。
此題中,箭頭函數外層沒有普通函數,因此 this 指向全局對象,因此結果爲 'window_name'、'window_name'。
let obj1 = {
name:'obj1_name',
print:function(){
return ()=>console.log(this.name)
}
}
let obj2 = {name:'obj2_name'}
obj1.print()() // 問 輸出結果
obj1.print().call(obj2) // 問 輸出結果
obj1.print.call(obj2)() // 問 輸出結果
複製代碼
答案: 'obj1_name' 'obj1_name' 'obj2_name'
箭頭函數的 this 與其外層的普通函數的 this 一致,與 call、apply、bind 無關。
此題,obj1.print 返回一個箭頭函數,此箭頭函數中的 this 就是 obj1.print 調用時的 this。
歡迎關注微信公衆號
【前端小黑屋】
,每週1-3篇精品優質文章推送,助你走上進階之旅