關於this、call、applay和bind

前言

this關鍵字是一個非常重要的語法點。毫不誇張地說,不理解它的含義,大部分開發任務都無法完成。而this可以動態切換,爲JavaScript創造了巨大的靈活性的同時,也使得編程變得困難和模糊。有時,需要把this固定下來,避免出現意想不到的情況。JavaScript 提供了call、apply、bind這三個方法,來切換/固定this的指向。

基本概念

this可以用在構造函數之中,表示實例對象。表示實例對象。除此之外,this還可以用在別的場合,作爲對象屬性執行、作爲普通函數執行。但不管是什麼場合,this都有一個共同點:它總是返回一個對象! 它總是返回一個對象! 它總是返回一個對象!重要的事情說三遍。

簡單說,this就是屬性或方法“當前”所在的對象。

this.name

上面代碼中,this就代表name屬性當前所在的對象。

使用this

下面從一個簡單例子開始


var person = {
  name: '張三',
  describe: function () {
    return '姓名:'+ this.name;
  }
};

console.log(person.describe());
// "姓名:張三"

上面代碼中,this.name表示name屬性所在的那個對象。由於this.name是在describe方法中調用,而describe方法所在的當前對象是person,因此this指向personthis.name就是person.name

再來個例子清楚的理解this動態的指向


var A = {
  name: '張三',
  describe: function () {
    return '姓名:'+ this.name;
  }
};

var B = {
  name: '李四'
};

B.describe = A.describe;
console.log(B.describe());
// "姓名:李四"

上面代碼中,A.describe屬性被賦給B,於是B.describe就表示describe方法所在的當前對象是B,所以this.name就指向B.name

如果還看不明白,整合第一個和第二個例子,來更加清晰的理解一下下。


function f() {
  return '姓名:'+ this.name;
}

var A = {
  name: '張三',
  describe: f
};

var B = {
  name: '李四',
  describe: f
};

console.log(A.describe()); // "姓名:張三"
console.log(B.describe()); // "姓名:李四"

上面代碼中,函數f內部使用了this關鍵字,隨着函數f所在的對象不同,this的指向也不同。

總而言之,JavaScript語言之中,一切皆對象,運行環境也是對象,所以函數都是在某個對象之中運行,this就是函數運行時所在的對象(環境)。這本來並不會讓用戶糊塗,但是JavaScript支持運行環境動態切換,也就是說,this的指向是動態的,沒有辦法事先確定到底指向哪個對象。

函數中this指向

JavaScript函數是一個單獨的值,它可以在不同的環境(上下文)中執行,同時JavaScript 允許在函數體內部,引用當前環境的其他變量。

爲了能夠在函數體內部獲得當前的運行環境(context)。所以,this就出現了,它的設計目的就是在函數體內部,指代函數當前的運行環境。

JavaScript 語言之所以有this的設計,跟內存裏面的數據結構有關係。基本數據類型是按值訪問的,引用類型存儲在內存中。

var obj = { foo:  5 };

1、對於對象裏面的屬性存儲德是基本數據類型情況,變量obj是一個地址(reference)。後面如果要讀取obj.foo,引擎先從obj拿到內存地址,然後再從該地址讀出原始的對象,返回它的foo屬性。

JavaScript 引擎會先在內存裏面,生成一個對象{foo:5},然後把這個對象的內存地址賦值給變量obj

{
  foo: {
    [[value]]: 5
    [[writable]]: true
    [[enumerable]]: true
    [[configurable]]: true
  }
}

注意,foo屬性的值保存在屬性描述對象的value屬性裏面。

這樣的結構是很清晰的,問題在於屬性的值可能是一個函數。

2、this總是指向函數的直接調用者(而非間接調用者)


var obj = { foo: function () {} };

這時,引擎會將函數單獨保存在內存中,然後再將函數的地址賦值給foo屬性的value屬性。


{
  foo: {
    [[value]]: 函數的地址
    ...
  }
}

由於函數是一個單獨的值,所以它可以在不同的環境(上下文)執行。


var f = function () {};
var obj = { f: f };

// 單獨執行
f()

// obj 環境執行
obj.f()
JavaScript 允許在函數體內部,引用當前環境的其他變量。

var f = function () {
  console.log(x);
};

上面代碼中,函數體裏面使用了變量x。該變量由運行環境提供。

現在問題就來了,由於函數可以在不同的運行環境執行,所以需要有一種機制,能夠在函數體內部獲得當前的運行環境(對象)(context)。所以,this就出現了,它的設計目的就是在函數體內部,指代函數當前的運行環境。

總之,this總是指向函數的直接調用者(而非間接調用者)

改變this指向

this的動態切換,固然爲 JavaScript 創造了巨大的靈活性,但也使得編程變得困難和模糊。有時,需要把this固定下來,避免出現意想不到的情況。JavaScript提供了call、apply、bind這三個方法,來切換/固定this的指向。

call

1、使用call函數

函數實例的 call方法,可以指定函數內部this的指向(即函數執行時所在的作用域),然後在所指定的作用域中,調用該函數。


var obj = {};

var f = function () {
  return this;
};

f() === window // true
f.call(obj) === obj // true

2、call方法的參數

call方法的參數,應該是一個對象。如果參數爲空、null和undefined,則默認傳入全局對象


var n = 123;
var obj = { n: 456 };

function a() {
  console.log(this.n);
}

a.call() // 123
a.call(null) // 123
a.call(undefined) // 123
a.call(window) // 123
a.call(obj) // 456

如果call方法的參數是一個原始值,那麼這個原始值會自動轉成對應的包裝對象,然後傳入call方法。


var f = function () {
  return this;
};

f.call(5)
// Number {[[PrimitiveValue]]: 5}

call方法還可以接受多個參數。call的第一個參數就是this所要指向的那個對象,後面的參數則是函數調用時所需的參數。

func.call(thisValue, arg1, arg2, ...)

3、call方法應用

call方法的一個應用是調用對象的原生方法。hasOwnPropertyobj對象繼承的方法,如果這個方法一旦被覆蓋,就不會得到正確結果。call方法可以解決這個問題,它將hasOwnProperty方法的原始定義放到obj對象上執行,這樣無論obj上有沒有同名方法,都不會影響結果。


var obj = {};
obj.hasOwnProperty('toString') // false

// 覆蓋掉繼承的 hasOwnProperty 方法
obj.hasOwnProperty = function () {
  return true;
};
obj.hasOwnProperty('toString') // true

Object.prototype.hasOwnProperty.call(obj, 'toString') // false

apply

1、使用apply


  var a = {
        name : "Cherry",

        func1: function () {
            console.log(this.name)
        },

        func2: function () {
            setTimeout(  function () {
                this.func1()
            }.apply(a),100);
        }

    };

    a.func2()            // Cherry

2、apply參數

它接收一個數組作爲函數執行時的參數,使用格式如下。

func.apply(thisValue, [arg1, arg2, ...])

3、apply應用

(1)找出數組最大元素

JavaScript 不提供找出數組最大元素的函數。結合使用apply方法和Math.max方法,就可以返回數組的最大元素。


var a = [10, 2, 4, 15, 9];
Math.max.apply(null, a) // 15

(2)將數組的空元素變爲undefined

通過apply方法,利用Array構造函數將數組的空元素變成undefined


Array.apply(null, ['a', ,'b'])
// [ 'a', undefined, 'b' ]

bind

1、使用bind()

bind方法用於將函數體內的this綁定到某個對象,然後返回一個新函數。


var d = new Date();
d.getTime() // 1481869925657

var print = d.getTime;
print() // Uncaught TypeError: this is not a Date object.

上面代碼中,我們將d.getTime方法賦給變量print,然後調用print就報錯了。這是因爲getTime方法內部的this,綁定Date對象的實例,賦給變量print以後,內部的this已經不指向Date對象的實例了。

bind方法可以解決這個問題。


var print = d.getTime.bind(d);
print() // 1481869925657

2、bind參數

bind方法的參數就是所要綁定this的對象,下面是一個更清晰的例子。

var counter = {
  count: 0,
  inc: function () {
    this.count++;
  }
};

var func = counter.inc.bind(counter);
func();
counter.count // 1

bind還可以接受更多的參數,將這些參數綁定原函數的參數。

var add = function (x, y) {
  return x * this.m + y * this.n;
}

var obj = {
  m: 2,
  n: 2
};

var newAdd = add.bind(obj, 5);
newAdd(5) // 20

如果bind方法的第一個參數是nullundefined,等於將this綁定到全局對象,函數運行時this指向頂層對象(瀏覽器爲window)。

function add(x, y) {
  return x + y;
}

var plus5 = add.bind(null, 5);
plus5(10) // 15

3、bind的應用
結合call方法使用

利用bind方法,可以改寫一些JavaScript原生方法的使用形式,以數組的slice方法爲例。


[1, 2, 3].slice(0, 1) // [1]
// 等同於
Array.prototype.slice.call([1, 2, 3], 0, 1) // [1]

call方法實質上是調用Function.prototype.call方法,因此上面的表達式可以用bind方法改寫。


var slice = Function.prototype.call.bind(Array.prototype.slice);
slice([1, 2, 3], 0, 1) // [1]
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章