回味JS基礎:call apply 與 bind

小弟調調™發表於2016-09-17

原文:回味JS基礎:call apply 與 bind

在JavaScript中,callapplybindFunction物件自帶的三個方法,本文將通過幾個場景的應用,來詳細理解三個方法。

call()

call() 方法在使用一個指定的this值和若干個指定的引數值的前提下呼叫某個函式或方法。

當呼叫一個函式時,可以賦值一個不同的 this 物件。this 引用當前物件,即 call 方法的第一個引數。

通過 call 方法,你可以在一個物件上借用另一個物件上的方法,比如Object.prototype.toString.call([]),就是一個Array物件借用了Object物件上的方法。

語法 fun.call(thisArg[, arg1[, arg2[, ...]]])

thisArg

在fun函式執行時指定的this值。需要注意的是下面幾種情況

(1)不傳,或者傳nullundefined, 函式中的this指向window物件
(2)傳遞另一個函式的函式名,函式中的this指向這個函式的引用,並不一定是該函式執行時真正的this值 (3)值為原始值(數字,字串,布林值)的this會指向該原始值的自動包裝物件,如 StringNumberBoolean
(4)傳遞一個物件,函式中的this指向這個物件

arg1, arg2, ...

指定的引數列表。

例子

初級應用例子

function a(){
    //輸出函式a中的this物件
    console.log(this); 
}
//定義函式b
function b(){} 

var obj = {name:'這是一個屌絲'}; //定義物件obj
a.call(); //window
a.call(null); //window
a.call(undefined);//window
a.call(1); //Number
a.call(''); //String
a.call(true); //Boolean
a.call(b);// function b(){}
a.call(obj); //Object複製程式碼

使用call方法呼叫匿名函式並且指定上下文的this

在下面的例子中,當呼叫 greet 方法的時候,該方法的 this 值會繫結到 i物件。

function greet() {
  var reply = [this.person, '是一個輕量的', this.role].join(' ');
  console.log(reply);
}

var i = {
  person: 'JSLite.io', role: 'Javascript 庫。'
};

greet.call(i); 
// JSLite.io 是一個輕量的 Javascript 庫。複製程式碼

使用call方法呼叫匿名函式

在下例中的for迴圈體內,我們建立了一個匿名函式,然後通過呼叫該函式的call方法,將每個陣列元素作為指定的this值執行了那個匿名函式。這個匿名函式的主要目的是給每個陣列元素物件新增一個print方法,這個print方法可以列印出各元素在陣列中的正確索引號。當然,這裡不是必須得讓陣列元素作為this值傳入那個匿名函式(普通引數就可以),目的是為了演示call的用法。

var animals = [
  {species: 'Lion', name: 'King'},
  {species: 'Whale', name: 'Fail'}
];

for (var i = 0; i < animals.length; i++) {
  (function (i) { 
    this.print = function () { 
      console.log('#' + i  + ' ' + this.species + ': ' + this.name); 
    } 
    this.print();
  }).call(animals[i], i);
}
//#0 Lion: King
//#1 Whale: Fail複製程式碼

使用call方法呼叫函式傳引數

var a = {
    name:'JSLite.io', //定義a的屬性
    say:function(){ //定義a的方法
        console.log("Hi,I'm function a!");
    }
};
function b(name){
    console.log("Post params: "+ name);
    console.log("I'm "+ this.name);
    this.say();
}

b.call(a,'test');
//Post params: test
//I'm JSLite.io
//I'm function a!複製程式碼

apply()

語法與 call() 方法的語法幾乎完全相同,唯一的區別在於,apply的第二個引數必須是一個包含多個引數的陣列(或類陣列物件)。apply的這個特性很重要,

在呼叫一個存在的函式時,你可以為其指定一個 this 物件。 this 指當前物件,也就是正在呼叫這個函式的物件。 使用 apply, 你可以只寫一次這個方法然後在另一個物件中繼承它,而不用在新物件中重複寫該方法。

語法:fun.apply(thisArg[, argsArray])

注意: 需要注意:Chrome 14 以及 Internet Explorer 9 仍然不接受類陣列物件。如果傳入類陣列物件,它們會丟擲異常。

引數

thisArg

同上callthisArg引數。

argsArray

一個陣列或者類陣列物件,其中的陣列元素將作為單獨的引數傳給 fun 函式。如果該引數的值為nullundefined,則表示不需要傳入任何引數。從ECMAScript 5 開始可以使用類陣列物件。

例子

function jsy(x,y,z){
    console.log(x,y,z);
}

jsy.apply(null,[1,2,3]); 
// 1 2 3複製程式碼

使用apply來連結構造器的例子

你可以使用apply來給一個物件連結構造器,類似於Java. 在接下來的例子中我們會建立一個叫做construct的全域性的Function函式,來使你能夠在構造器中使用一個類陣列物件而非引數列表。

Function.prototype.construct = function(aArgs) {
  var fConstructor = this, 
  fNewConstr = function() { 
    fConstructor.apply(this, aArgs); 
  };
  fNewConstr.prototype = fConstructor.prototype;
  return new fNewConstr();
};
function MyConstructor () {
    for (var nProp = 0; nProp < arguments.length; nProp++) {
        console.log(arguments,this)
        this["property" + nProp] = arguments[nProp];
    }
}
var myArray = [4, "Hello world!", false];
var myInstance = MyConstructor.construct(myArray);

console.log(myInstance.property1);                // logs "Hello world!"
console.log(myInstance instanceof MyConstructor); // logs "true"
console.log(myInstance.constructor);              // logs "MyConstructor"複製程式碼

使用apply和內建函式

聰明的apply用法允許你在某些本來需要寫成遍歷陣列變數的任務中使用內建的函式。在接下里的例子中我們會使用Math.max/Math.min來找出一個陣列中的最大/最小值。

//裡面有最大最小數字值的一個陣列物件
var numbers = [5, 6, 2, 3, 7];

/* 使用 Math.min/Math.max 在 apply 中應用 */
var max = Math.max.apply(null, numbers);
// 一般情況是用 Math.max(5, 6, ..) 或者 Math.max(numbers[0], ...) 來找最大值
var min = Math.min.apply(null, numbers);

//通常情況我們會這樣來找到數字的最大或者最小值
//比對上面的栗子,是不是下面的看起來沒有上面的舒服呢?
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];
}複製程式碼

引數陣列切塊後迴圈傳入

function minOfArray(arr) {
  var min = Infinity;
  var QUANTUM = 32768;

  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)));
    console.log(submin, min)
    min = Math.min(submin, min);
  }

  return min;
}

var min = minOfArray([5, 6, 2, 3, 7]);複製程式碼

bind

bind() 函式會建立一個新函式(稱為繫結函式)

  • bind是ES5新增的一個方法
  • 傳參和call或apply類似
  • 不會執行對應的函式,call或apply會自動執行對應的函式
  • 返回對函式的引用

語法 fun.bind(thisArg[, arg1[, arg2[, ...]]])

下面例子:當點選網頁時,EventClick被觸發執行,輸出JSLite.io p1 p2, 說明EventClick中的thisbind改變成了obj物件。如果你將EventClick.bind(obj,'p1','p2') 變成 EventClick.call(obj,'p1','p2') 的話,頁面會直接輸出 JSLite.io p1 p2

var obj = {name:'JSLite.io'};
/**
 * 給document新增click事件監聽,並繫結EventClick函式
 * 通過bind方法設定EventClick的this為obj,並傳遞引數p1,p2
 */
document.addEventListener('click',EventClick.bind(obj,'p1','p2'),false);
//當點選網頁時觸發並執行
function EventClick(a,b){
    console.log(
            this.name, //JSLite.io
            a, //p1
            b  //p2
    )
}
// JSLite.io p1 p2複製程式碼

相容

if (!Function.prototype.bind) {
  Function.prototype.bind = function (oThis) {
    if (typeof this !== "function") {
      // closest thing possible to the ECMAScript 5
      // internal IsCallable function
      throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
    }

    var aArgs = Array.prototype.slice.call(arguments, 1), 
        fToBind = this, // this在這裡指向的是目標函式
        fNOP = function () {},
        fBound = function () {
          return fToBind.apply(this instanceof fNOP
                 ? this //此時的this就是new出的obj
                 : oThis || this,//如果傳遞的oThis無效,就將fBound的呼叫者作為this

               //將通過bind傳遞的引數和呼叫時傳遞的引數進行合併,並作為最終的引數傳遞
               aArgs.concat(Array.prototype.slice.call(arguments)));
        };
    fNOP.prototype = this.prototype;
    //將目標函式的原型物件拷貝到新函式中,因為目標函式有可能被當作建構函式使用
    fBound.prototype = new fNOP();
    //返回fBond的引用,由外部按需呼叫
    return fBound;
  };
}複製程式碼

相容例子來源於:developer.mozilla.org/zh-CN/docs/…

應用場景:繼承

function Animal(name,weight){
   this.name = name;
   this.weight = weight;
}
function Cat(){
    // 在call中將this作為thisArgs引數傳遞
    // Animal方法中的this就指向了Cat中的this
    // 所以Animal中的this指向的就是cat物件
    // 在Animal中定義了name和weight屬性,就相當於在cat中定義了這些屬性
    // cat物件便擁有了Animal中定義的屬性,從而達到了繼承的目的
    Animal.call(this,'cat','50');
    //Animal.apply(this,['cat','50']);
    this.say = function(){
       console.log("I am " + this.name+",my weight is " + this.weight);
    }
}
//當通過new運算子產生了cat時,Cat中的this就指向了cat物件
var cat = new Cat();
cat.say();
//輸出=> I am cat,my weight is 50複製程式碼

原型擴充套件

在原型函式上擴充套件和自定義方法,從而不汙染原生函式。例如:我們在 Array 上擴充套件一個 forEach

function test(){
    // 檢測arguments是否為Array的例項
    console.log(
        arguments instanceof Array, //false
        Array.isArray(arguments)  //false
    );
    // 判斷arguments是否有forEach方法
    console.log(arguments.forEach); 
    // undefined
    // 將陣列中的forEach應用到arguments上
    Array.prototype.forEach.call(arguments,function(item){
        console.log(item); // 1 2 3 4
    });
}
test(1,2,3,4);複製程式碼

相關文章