JS 系列五:深刻 call、apply、bind

引言

JS系列暫定 27 篇,從基礎,到原型,到異步,到設計模式,到架構模式等。javascript

本篇是JS系列中第 5 篇,文章主講 JS 中 call 、 applybind 、箭頭函數以及柯里化,着重介紹它們之間的區別、對比使用,深刻了解 call 、 applybind前端

1、Function.prototype.call()

call() 方法調用一個函數, 其具備一個指定的 this 值和多個參數(參數的列表)。java

func.call(thisArg, arg1, arg2, ...)
複製代碼

它運行 func,提供的第一個參數 thisArg 做爲 this,後面的做爲參數。git

1. func 與 func.call

先看一個例子:github

func(1, 2, 3);
func.call(obj, 1, 2, 3)
複製代碼

他們都調用的是 func,參數是 12 和 3設計模式

惟一的區別是 func.call 也將 this 設置爲 obj數組

須要注意的是,設置的 thisArg 值並不必定是該函數執行時真正的 this 值,若是這個函數處於非嚴格模式下,則指定爲 nullundefinedthis 值會自動指向全局對象(瀏覽器中就是 window 對象),同時值爲原始值(數字,字符串,布爾值)的 this 會指向該原始值的自動包裝對象。瀏覽器

2. func.call 綁定上下文

例如,在下面的代碼中,咱們在對象的上下文中調用 sayWord.call(bottle) 運行 sayWord ,並 bottle 傳遞爲 sayWordthis緩存

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
複製代碼

3. 使用 func.call 時未指定 this

非嚴格模式
// 非嚴格模式下
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
複製代碼

4. call 在 JS 繼承中的使用: 構造繼承

基本思想:在子類型的構造函數內部調用父類型構造函數。閉包

注意:函數只不過是在特定環境中執行代碼的對象,因此這裏使用 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綁定到子類
複製代碼

5. 解決 var 做用域問題

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] 對象在數組中的正確索引號。

2、Function.prototype.apply()

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 仍然不接受類數組對象。若是傳入類數組對象,它們會拋出異常。

1. call、apply 與 擴展運算符

咱們已經知道了JS 基礎之: var、let、const、解構、展開、函數 一章中的擴展運算符 ...,它能夠將數組(或任何可迭代的)做爲參數列表傳遞。所以,若是咱們將它與 call 一塊兒使用,就能夠實現與 apply 幾乎相同的功能。

這兩個調用結果幾乎相同:

let args = [1, 2, 3];

func.call(context, ...args); // 使用 spread 運算符將數組做爲參數列表傳遞
func.apply(context, args);   // 與使用 call 相同
複製代碼

若是咱們仔細觀察,那麼 callapply 的使用會有一些細微的差異。

  • 擴展運算符 ... 容許將 可迭代的 參數列表 做爲列表傳遞給 call
  • apply 只接受 類數組同樣的 參數列表

2. apply 函數轉移

apply 最重要的用途之一是將調用傳遞給另外一個函數,以下所示:

let wrapper = function() {
  return anotherFunction.apply(this, arguments);
};
複製代碼

wrapper 經過 anotherFunction.apply 得到了上下文 thisanotherFunction 的參數並返回其結果。

當外部代碼調用這樣的 wrapper 時,它與原始函數的調用沒法區分。

3. apply 鏈接數組

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]
複製代碼

4. apply 來連接構造器

Function.prototype.constructor = function (aArgs) {
  var oNew = Object.create(this.prototype);
  this.apply(oNew, aArgs);
  return oNew;
};
複製代碼

5. apply 和內置函數

/* 找出數組中最大/小的數字 */
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 一樣也是如此
複製代碼

3、Function.prototype.bind()

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!, 問題二完美解決!

1. bind 與 new

再看一個例子:

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 值將被忽略,而前置參數將提供給模擬函數。

2. 二次 bind

function sayHello() {
  console.log('Hello, ', this.nickname)
}

sayHello = sayHello.bind( {nickname: "Bottle"} ).bind( {nickname: "AnGe" } );
sayHello();
// Hello, Bottle
複製代碼

輸出依然是 Hello, Bottle ,這是由於 func.bind(...) 返回的外來的綁定函數對象僅在建立的時候記憶上下文(若是提供了參數)。

一個函數不能做爲重複綁定。

2. 偏函數

當咱們肯定一個函數的一些參數時,返回的函數(更加特定)被稱爲偏函數。咱們可使用 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]
複製代碼

當咱們不想一遍又一遍重複相同的參數時,偏函數很方便。

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
複製代碼

4、柯里化

在計算機科學中,柯里化(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 實現版本一般保留函數被正常調用和在參數數量不夠的狀況下返回偏函數這兩個特性。

5、擴展:箭頭函數

1. 沒有 this

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=WindowWindow.nicknameundefined

但箭頭函數就沒事,由於箭頭函數沒有 this。在外部上下文中,this 的查找與普通變量搜索徹底相同。this 指向定義時的環境。

2. 不可 new 實例化

不具備 this 天然意味着另外一個限制:箭頭函數不能用做構造函數。他們不能用 new 調用。

3. 箭頭函數 vs bind

箭頭函數 => 和正常函數經過 .bind(this) 調用有一個微妙的區別:

  • .bind(this) 建立該函數的 「綁定版本」。
  • 箭頭函數 => 不會建立任何綁定。該函數根本沒有 this。在外部上下文中,this 的查找與普通變量搜索徹底相同。

4. 沒有 arguments 對象

箭頭函數也沒有 arguments 變量。

由於咱們須要用當前的 thisarguments 轉發一個調用,全部這對於裝飾者來講很是好。

例如,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);
  };
}
複製代碼

在這裏,咱們必須建立額外的變量 argsctx,以便 setTimeout 內部的函數能夠接收它們。

5. 總結

  • this 指向定義時的環境
  • 不可 new 實例化
  • this 不可變
  • 沒有 arguments 對象

6、參考

裝飾和轉發,call/apply

7、系列文章

想看更過系列文章,點擊前往 github 博客主頁

8、走在最後

1. ❤️玩得開心,不斷學習,並始終保持編碼。👨💻

2. 若有任何問題或更獨特的看法,歡迎評論或直接聯繫瓶子君(公衆號回覆 123 便可)!👀👇

3. 👇歡迎關注:前端瓶子君,每日更新!👇

前端瓶子君
相關文章
相關標籤/搜索