JS
系列暫定 27 篇,從基礎,到原型,到異步,到設計模式,到架構模式等。javascript
本篇是JS
系列中第 5 篇,文章主講 JS 中 call
、 apply
、 bind
、箭頭函數以及柯里化,着重介紹它們之間的區別、對比使用,深刻了解 call
、 apply
、 bind
。前端
call()
方法調用一個函數, 其具備一個指定的 this
值和多個參數(參數的列表)。java
func.call(thisArg, arg1, arg2, ...)
複製代碼
它運行 func
,提供的第一個參數 thisArg
做爲 this
,後面的做爲參數。git
先看一個例子:github
func(1, 2, 3);
func.call(obj, 1, 2, 3)
複製代碼
他們都調用的是 func
,參數是 1
,2
和 3
。設計模式
惟一的區別是 func.call
也將 this
設置爲 obj
。數組
須要注意的是,設置的 thisArg 值並不必定是該函數執行時真正的 this
值,若是這個函數處於非嚴格模式下,則指定爲 null
和 undefined
的 this
值會自動指向全局對象(瀏覽器中就是 window 對象),同時值爲原始值(數字,字符串,布爾值)的 this
會指向該原始值的自動包裝對象。瀏覽器
例如,在下面的代碼中,咱們在對象的上下文中調用 sayWord.call(bottle)
運行 sayWord
,並 bottle
傳遞爲 sayWord
的 this
:緩存
function sayWord() {
var talk = [this.name, 'say', this.word].join(' ');
console.log(talk);
}
var bottle = {
name: 'bottle',
word: 'hello'
};
// 使用 call 將 bottle 傳遞爲 sayWord 的 this
sayWord.call(bottle);
// bottle say hello
複製代碼
// 非嚴格模式下
var bottle = 'bottle'
function say(){
// 注意:非嚴格模式下,this 爲 window
console.log('name is %s',this.bottle)
}
say.call()
// name is bottle
複製代碼
// 嚴格模式下
'use strict'
var bottle = 'bottle'
function say(){
// 注意:在嚴格模式下 this 爲 undefined
console.log('name is %s',this.bottle)
}
say.call()
// Uncaught TypeError: Cannot read property 'bottle' of undefined
複製代碼
基本思想:在子類型的構造函數內部調用父類型構造函數。閉包
注意:函數只不過是在特定環境中執行代碼的對象,因此這裏使用 apply/call 來實現。
使用父類的構造函數來加強子類實例,等因而複製父類的實例屬性給子類(沒用到原型)
// 父類
function SuperType (name) {
this.name = name; // 父類屬性
}
SuperType.prototype.sayName = function () { // 父類原型方法
return this.name;
};
// 子類
function SubType () {
// 調用 SuperType 構造函數
// 在子類構造函數中,向父類構造函數傳參
SuperType.call(this, 'SuperType');
// 爲了保證子父類的構造函數不會重寫子類的屬性,須要在調用父類構造函數後,定義子類的屬性
this.subName = "SubType";
// 子類屬性
};
// 子類實例
let instance = new SubType();
// 運行子類構造函數,並在子類構造函數中運行父類構造函數,this綁定到子類
複製代碼
var bottle = [
{name: 'an', age: '24'},
{name: 'anGe', age: '12'}
];
for (var i = 0; i < bottle.length; i++) {
// 匿名函數
(function (i) {
setTimeout(() => {
// this 指向了 bottle[i]
console.log('#' + i + ' ' + this.name + ': ' + this.age);
}, 1000)
}).call(bottle[i], i);
// 調用 call 方法,同時解決了 var 做用域問題
}
複製代碼
打印結果:
#0 an: 24
#1 anGe: 12
複製代碼
在上面例中的 for
循環體內,咱們建立了一個匿名函數,而後經過調用該函數的 call
方法,將每一個數組元素做爲指定的 this
值當即執行了那個匿名函數。這個當即執行的匿名函數的做用是打印出 bottle[i]
對象在數組中的正確索引號。
apply()
方法調用一個具備給定 this
值的函數,以及做爲一個數組(或[相似數組對象)提供的參數。
func.apply(thisArg, [argsArray])
複製代碼
它運行 func
設置 this = context
並使用類數組對象 args
做爲參數列表。
例如,這兩個調用幾乎相同:
func(1, 2, 3);
func.apply(context, [1, 2, 3])
複製代碼
兩個都運行 func
給定的參數是 1,2,3
。可是 apply
也設置了 this = context
。
call
和 apply
之間惟一的語法區別是 call
接受一個參數列表,而 apply
則接受帶有一個類數組對象。
須要注意:Chrome 14 以及 Internet Explorer 9 仍然不接受類數組對象。若是傳入類數組對象,它們會拋出異常。
咱們已經知道了JS 基礎之: var、let、const、解構、展開、函數 一章中的擴展運算符 ...
,它能夠將數組(或任何可迭代的)做爲參數列表傳遞。所以,若是咱們將它與 call
一塊兒使用,就能夠實現與 apply
幾乎相同的功能。
這兩個調用結果幾乎相同:
let args = [1, 2, 3];
func.call(context, ...args); // 使用 spread 運算符將數組做爲參數列表傳遞
func.apply(context, args); // 與使用 call 相同
複製代碼
若是咱們仔細觀察,那麼 call
和 apply
的使用會有一些細微的差異。
...
容許將 可迭代的 參數列表
做爲列表傳遞給 call
。apply
只接受 類數組同樣的 參數列表
。apply
最重要的用途之一是將調用傳遞給另外一個函數,以下所示:
let wrapper = function() {
return anotherFunction.apply(this, arguments);
};
複製代碼
wrapper
經過 anotherFunction.apply
得到了上下文 this
和 anotherFunction
的參數並返回其結果。
當外部代碼調用這樣的 wrapper
時,它與原始函數的調用沒法區分。
array.push.apply
將數組添加到另外一數組上:
var array = ['a', 'b']
var elements = [0, 1, 2]
array.push.apply(array, elements)
console.info(array) // ["a", "b", 0, 1, 2]
複製代碼
Function.prototype.constructor = function (aArgs) {
var oNew = Object.create(this.prototype);
this.apply(oNew, aArgs);
return oNew;
};
複製代碼
/* 找出數組中最大/小的數字 */
let numbers = [5, 6, 2, 3, 7]
/* 應用(apply) Math.min/Math.max 內置函數完成 */
let max = Math.max.apply(null, numbers)
/* 基本等同於 Math.max(numbers[0], ...) 或 Math.max(5, 6, ..) */
let min = Math.min.apply(null, numbers)
console.log('max: ', max)
// max: 7
console.log('min: ', min)
// min: 2
複製代碼
它至關於:
/* 代碼對比: 用簡單循環完成 */
let numbers = [5, 6, 2, 3, 7]
let max = -Infinity, min = +Infinity
for (var i = 0; i < numbers.length; i++) {
if (numbers[i] > max)
max = numbers[i]
if (numbers[i] < min)
min = numbers[i]
}
console.log('max: ', max)
// max: 7
console.log('min: ', min)
// min: 2
複製代碼
可是:若是用上面的方式調用 apply
,會有超出 JavaScript 引擎的參數長度限制的風險。更糟糕的是其餘引擎會直接限制傳入到方法的參數個數,致使參數丟失。
因此,當數據量較大時
function minOfArray(arr) {
var min = Infinity
var QUANTUM = 32768 // JavaScript 核心中已經作了硬編碼 參數個數限制在65536
for (var i = 0, len = arr.length; i < len; i += QUANTUM) {
var submin = Math.min.apply(null, arr.slice(i, Math.min(i + QUANTUM, len)))
min = Math.min(submin, min)
}
return min
}
var min = minOfArray([5, 6, 2, 3, 7])
// max 一樣也是如此
複製代碼
JavaScript 新手常常犯的一個錯誤是將一個方法從對象中拿出來,而後再調用,但願方法中的 this
是原來的對象(好比在回調中傳入這個方法)。若是不作特殊處理的話,通常 this
就丟失了。
例如:
let bottle = {
nickname: "bottle",
sayHello() {
console.log(`Hello, ${this.nickname}!`)
},
sayHi(){
setTimeout(function(){
console.log('Hello, ', this.nickname)
}, 1000)
}
};
// 問題一
bottle.sayHi();
// Hello, undefined!
// 問題二
setTimeout(bottle.sayHello, 1000);
// Hello, undefined!
複製代碼
問題一的 this.nickname 是 undefined ,緣由是 this 指向是在運行函數時肯定的,而不是定義函數時候肯定的,再由於 sayHi 中 setTimeout 在全局環境下執行,因此 this 指向 setTimeout 的上下文:window。
問題二的 this.nickname 是 undefined ,是由於 setTimeout 僅僅只是獲取函數 bottle.sayHello 做爲 setTimeout 回調函數,this 和 bottle 對象分離了。
問題二能夠寫爲:
// 在這種狀況下,this 指向全局做用域
let func = bottle.sayHello;
setTimeout(func, 1000);
// 用戶上下文丟失
// 瀏覽器上,訪問的其實是 Window 上下文
複製代碼
那麼怎麼解決這兩個問題喃?
解決方案一: 緩存 this 與包裝
首先經過緩存 this 解決問題一 bottle.sayHi();
:
let bottle = {
nickname: "bottle",
sayHello() {
console.log(`Hello, ${this.nickname}!`)
},
sayHi(){
var _this = this // 緩存this
setTimeout(function(){
console.log('Hello, ', _this.nickname)
}, 1000)
}
};
bottle.sayHi();
// Hello, bottle
複製代碼
那問題二 setTimeout(bottle.sayHello, 1000);
喃?
let bottle = {
nickname: "bottle",
sayHello() {
console.log(`Hello, ${this.nickname}!`);
}
};
// 加一個包裝層
setTimeout(() => {
bottle.sayHello()
}, 1000);
// Hello, bottle!
複製代碼
這樣看似解決了問題二,但若是咱們在 setTimeout
異步觸發以前更新 bottle
值又會怎麼樣呢?
var bottle = {
nickname: "bottle",
sayHello() {
console.log(`Hello, ${this.nickname}!`);
}
};
setTimeout(() => {
bottle.sayHello()
}, 1000);
// 更新 bottle
bottle = {
nickname: "haha",
sayHello() {
console.log(`Hi, ${this.nickname}!`)
}
};
// Hi, haha!
複製代碼
bottle.sayHello()
最終打印爲 Hi, haha!
,那麼怎麼解決這種事情發生喃?
解決方案二: bind
bind()
最簡單的用法是建立一個新綁定函數,當這個新綁定函數被調用時,this
鍵值爲其提供的值,其參數列表前幾項值爲建立時指定的參數序列,綁定函數與被調函數具備相同的函數體(ES5中)。
let bottle = {
nickname: "bottle",
sayHello() {
console.log(`Hello, ${this.nickname}!`);
}
};
// 未綁定,「this」 指向全局做用域
let sayHello = bottle.sayHello
console.log(sayHello())
// Hello, undefined!
// 綁定
let bindSayHello = sayHello.bind(bottle)
// 建立一個新函數,將 this 綁定到 bottle 對象
console.log(bindSayHello())
// Hello, bottle!
複製代碼
因此,從原來的函數和原來的對象建立一個綁定函數,則能很漂亮地解決上面兩個問題:
let bottle = {
nickname: "bottle",
sayHello() {
console.log(`Hello, ${this.nickname}!`);
},
sayHi(){
// 使用 bind
setTimeout(function(){
console.log('Hello, ', this.nickname)
}.bind(this), 1000)
// 或箭頭函數
setTimeout(() => {
console.log('Hello, ', this.nickname)
}, 1000)
}
};
// 問題一:完美解決
bottle.sayHi()
// Hello, bottle
// Hello, bottle
let sayHello = bottle.sayHello.bind(bottle); // (*)
sayHello();
// Hello, bottle!
// 問題二:完美解決
setTimeout(sayHello, 1000);
// Hello, bottle!
// 更新 bottle
bottle = {
nickname: "haha",
sayHello() {
console.log(`Hi, ${this.nickname}!`)
}
};
複製代碼
問題一,能夠經過 bind
或箭頭函數完美解決。
最終更新 bottle
後, setTimeout(sayHello, 1000);
打印依然是 Hello, bottle!
, 問題二完美解決!
再看一個例子:
this.nickname = 'window'
let bottle = {
nickname: 'bottle'
}
function sayHello() {
console.log('Hello, ', this.nickname)
}
let bindBottle = sayHello.bind(bottle) // this 指向 bottle
console.log(bindBottle())
// Hello, bottle
console.log(new bindBottle()) // this 指向 sayHello {}
// Hello, undefined
複製代碼
上面例子中,運行結果 this.nickname
輸出爲 undefined
,這不是全局 nickname
, 也不是 bottle
對象中的 nickname
,這說明 bind
的 this
對象失效了,new
的實現中生成一個新的對象,這個時候的 this
指向的是 sayHello
。
注意 :綁定函數也可使用 new
運算符構造:這樣作就好像已經構造了目標函數同樣。提供的 this 值將被忽略,而前置參數將提供給模擬函數。
function sayHello() {
console.log('Hello, ', this.nickname)
}
sayHello = sayHello.bind( {nickname: "Bottle"} ).bind( {nickname: "AnGe" } );
sayHello();
// Hello, Bottle
複製代碼
輸出依然是 Hello, Bottle
,這是由於 func.bind(...)
返回的外來的綁定函數對象僅在建立的時候記憶上下文(若是提供了參數)。
一個函數不能做爲重複綁定。
當咱們肯定一個函數的一些參數時,返回的函數(更加特定)被稱爲偏函數。咱們可使用 bind
來獲取偏函數:
function list() {
return Array.prototype.slice.call(arguments);
}
var list1 = list(1, 2, 3); // [1, 2, 3]
var leadingThirtysevenList = list.bind(undefined, 37);
var list2 = leadingThirtysevenList(); // [37]
var list3 = leadingThirtysevenList(1, 2, 3); // [37, 1, 2, 3]
複製代碼
當咱們不想一遍又一遍重複相同的參數時,偏函數很方便。
function Bottle(nickname) {
this.nickname = nickname;
}
Bottle.prototype.sayHello = function() {
console.log('Hello, ', this.nickname)
};
let bottle = new Bottle('bottle');
let BindBottle = Bottle.bind(null, 'bindBottle');
let b1 = new BindBottle('b1');
b1 instanceof Bottle; // true
b1 instanceof BindBottle; // true
new Bottle('bottle1') instanceof BindBottle; // true
b1.sayHello()
// Hello, bindBottle
複製代碼
在計算機科學中,柯里化(Currying)是把接受多個參數的函數變換成接受一個單一參數(最初函數的第一個參數)的函數,而且返回接受餘下的參數且返回結果的新函數的技術。這個技術由 Christopher Strachey 以邏輯學家 Haskell Curry 命名的,儘管它是 Moses Schnfinkel 和 Gottlob Frege 發明的。
var add = function(x) {
return function(y) {
return x + y;
};
};
var increment = add(1);
var addTen = add(10);
increment(2);
// 3
addTen(2);
// 12
add(1)(2);
// 3
複製代碼
這裏定義了一個 add
函數,它接受一個參數並返回一個新的函數。調用 add
以後,返回的函數就經過閉包的方式記住了 add
的第一個參數。因此說 bind
自己也是閉包的一種使用場景。
柯里化是將 f(a,b,c)
能夠被以 f(a)(b)(c)
的形式被調用的轉化。JavaScript 實現版本一般保留函數被正常調用和在參數數量不夠的狀況下返回偏函數這兩個特性。
let bottle = {
nickname: "bottle",
sayHi(){
setTimeout(function(){
console.log('Hello, ', this.nickname)
}, 1000)
// 或箭頭函數
setTimeout(() => {
console.log('Hi, ', this.nickname)
}, 1000)
}
};
bottle.sayHi()
// Hello, undefined
// Hi, bottle
複製代碼
報錯是由於 Hello, undefined
是由於運行時 this=Window
, Window.nickname
爲 undefined
。
但箭頭函數就沒事,由於箭頭函數沒有 this
。在外部上下文中,this
的查找與普通變量搜索徹底相同。this
指向定義時的環境。
不具備 this
天然意味着另外一個限制:箭頭函數不能用做構造函數。他們不能用 new
調用。
箭頭函數 =>
和正常函數經過 .bind(this)
調用有一個微妙的區別:
.bind(this)
建立該函數的 「綁定版本」。=>
不會建立任何綁定。該函數根本沒有 this
。在外部上下文中,this
的查找與普通變量搜索徹底相同。箭頭函數也沒有 arguments
變量。
由於咱們須要用當前的 this
和 arguments
轉發一個調用,全部這對於裝飾者來講很是好。
例如,defer(f, ms)
獲得一個函數,並返回一個包裝函數,以 毫秒
爲單位延遲調用:
function defer(f, ms) {
return function() {
setTimeout(() => f.apply(this, arguments), ms)
};
}
function sayHi(who) {
alert('Hello, ' + who);
}
let sayHiDeferred = defer(sayHi, 2000);
sayHiDeferred("John"); // 2 秒後打印 Hello, John
複製代碼
沒有箭頭功能的狀況以下所示:
function defer(f, ms) {
return function(...args) {
let ctx = this;
setTimeout(function() {
return f.apply(ctx, args);
}, ms);
};
}
複製代碼
在這裏,咱們必須建立額外的變量 args
和 ctx
,以便 setTimeout
內部的函數能夠接收它們。
想看更過系列文章,點擊前往 github 博客主頁
1. ❤️玩得開心,不斷學習,並始終保持編碼。👨💻
2. 若有任何問題或更獨特的看法,歡迎評論或直接聯繫瓶子君(公衆號回覆 123 便可)!👀👇
3. 👇歡迎關注:前端瓶子君,每日更新!👇