context的概念
在知道我們為什么要使用call、apply、bind方法之前,我覺得有必要先了解一下context的相關概念,通常context的作用是取決于函數將如何被調用,當函數作為對象的方法調用時,this就會被設置為調用方法的對象:
var object = {
foo: function(){
console.log(this === object);
}
};
object.foo(); // true
當通過new一個對象的實例的方式來調用一個函數的時候,this的值將被設置為新創建的實例:
function foo(){
console.log(this);
}
foo() // window
new foo() // foo{}
當作為未綁定對象被調用時,this默認指向全局上下文或者瀏覽器中的window對象。然而,如果函數在嚴格模式下被執行,上下文將被默認為undefined
動態改變this的值
然而call、apply、bind方法允許你在自定義的context中執行函數,什么意思呢,通俗的說就是可以不遵循上面給出的部分定義,可以動態的改變this,我們直接引出MDN關于call方法的定義:
call() 方法調用一個函數, 其具有一個指定的this值和分別地提供的參數(參數的列表)
舉個例子:
// 非嚴格模式下
function add(args) {
console.log(this);
}
function sub(args) {
console.log(this);
}
add(); // Window {frames: Window, postMessage: ?, blur: ?, focus: ?, close: ?, …}
sub(); // Window {frames: Window, postMessage: ?, blur: ?, focus: ?, close: ?, …}
結果是符合預期的:
當作為未綁定對象被調用時,this默認指向全局上下文或者瀏覽器中的window對象
我們給他加上call方法:
// 非嚴格模式下
function add(args) {
console.log(this);
}
function sub(args) {
console.log(this);
}
add(); // Window {frames: Window, postMessage: ?, blur: ?, focus: ?, close: ?, …}
sub(); // Window {frames: Window, postMessage: ?, blur: ?, focus: ?, close: ?, …}
add.call(sub,1); // ? sub(args) { ... }
sub.call(add,1); // ? add(args) { ... }
這時候this的值有點類似于第一種情況:
當函數作為對象的方法調用時,this就會被設置為調用方法的對象
看起來我們調用call方法的時候好像實際上是進行了對象方法的調用:
sub.call(add,1); //sub
=>
var sub = {
add: function(){
console.log(this);
}
};
sub.add(); // sub
這就是動態的改變了this
還有一點要注意的是,這里call和apply方法第一個參數的this值并不一定是該函數執行時真正的this值,比如在非嚴格模式下,我們傳入null
或undefined
會自動指向全局對象,在瀏覽器環境中就是window
,在node環境中就是global
,另外我們還可以傳入一些原始值(數字,字符串,布爾值),這時候的this會指向這些原始值的包裝對象,比如傳入的是數字,會指向Number。
call和apply的區別
既然call和apply方法能夠動態的改變this的值,我們可以利用這個特性來實現簡單的繼承,比如此時我們有一個父構造函數:
function Product(name, age) {
this.name = name;
this.age = age;
this.say = function() {
console.log(this.name+"is"+this.age+"years old");
}
}
如果我們想寫一個子構造函數,只需要在子構造函數里面調用父構造函數的call方法就可以實現繼承:
function Toy(name, price) {
Product.call(this, name, price);
this.self = "single";
}
實現繼承的同時父子構造函數還都能分別有自己的屬性,比如這里的self屬性。
這里使用了call方法作為演示,其實apply方法的使用和call方法并無多大區別,重點在參數上,他們的第一個參數都是一個指定的this值,這個值可以是任意js對象,但是第二個參數有區別,call方法接受的是若干個參數的列表,而apply方法接受的是一個包含多個參數的數組,如果上面的例子用apply來寫的話就是這樣:
function Product(name, age) {
this.name = name;
this.age = age;
this.say = function() {
console.log(this.name+"is"+this.age+"years old");
}
}
function Toy(name, price) {
Product.apply(this, [name, price]);
this.self = "single";
}
bind方法的特殊性
之所以把bind方法單獨放出來是因為bind方法和前面兩者還是有不小的區別的,雖然都是動態改變this的值,舉個例子:
var obj = {
x: 81,
};
var foo = {
getX: function() {
return this.x;
}
}
console.log(foo.getX.bind(obj)()); //81
console.log(foo.getX.call(obj)); //81
console.log(foo.getX.apply(obj)); //81
有沒有注意到使用bind方法時候后面還要多加上一對括號,因為使用bind只是返回了對應函數并沒有立即執行,而call和apply方法是立即執行的,并且MDN還提到,當使用new操作符調用綁定函數時指定的this參數會變無效:
var obj = {
x: 81,
};
var foo = {
getX: function() {
return this.x;
}
}
var result = foo.getX.bind(obj);
console.log(new result()); //getX
bind方法的另一個應用是使一個函數擁有預設的初始函數,這些參數作為bind的第二個參數跟在this(或其他對象)后面,之后它們會被插入到目標函數的參數列表的開始位置,傳遞給綁定函數的參數會跟在它們的后面:
function list() {
return Array.prototype.slice.call(arguments);
}
var list1 = list(1, 2, 3); // [1, 2, 3]
// Create a function with a preset leading argument
var leadingThirtysevenList = list.bind(undefined, 37);
var list2 = leadingThirtysevenList(); // [37]
var list3 = leadingThirtysevenList(1, 2, 3); // [37, 1, 2, 3]