一、call和apply介紹
call()
方法使用一個指定的 this
值和單獨給出的一個或多個參數來調用一個函數。
apply()
方法調用一個具有給定this
值的函數,以及作爲一個數組(或類似數組對象)提供的參數。
該方法的語法和作用與 apply() 方法類似,只有一個區別,就是 call() 方法接受的是一個參數列表,而 apply() 方法接受的是一個包含多個參數的數組。
二、call的使用
call是Function
默認實現的函數,使用方式爲:fn.call(context, arg0, arg1, arg2...)
。fn函數的上下文環境變成context,並且還會使用傳入的參數。
var foo = { value: 1 };
function fn() {
console.log(this.value);
}
fn.call(foo); // value => 1
三、模擬call的實現
要模擬call的實現,首先明白call做了那些事情。
- 只有使用call時,fn的上下文環境纔會改變
- fn的實際參數來自於call的第二個參數一直到最後一個參數就是
- call方法不傳入上下文的時候,默認使用全局上下文
上面的3點大概就是非嚴格模式下call的行爲。
先看第一點,如何改變fn的上下文。
改變fn的上下文,只需要三步:
- 打開冰箱(將fn掛在foo上,命名爲_fn)
- 大象放進去(執行foo._fn)
- 關上冰箱(刪除foo._fn)
寫成代碼就是如下
var foo = {
value: 1
};
function fn() {
console.log(this.value);
}
Function.propotype.myCall = function(that) {
that._fn = this;
that._fn();
delete that._fn;
}
第一步就輕鬆的完成了,開不開心~
考慮有參數傳入的情況
首先得看收集參數,然後將參數一個一個傳遞給fn。畢竟參數的數量不清楚,所以使用arguments收集參數當然會好一點。問題就在於fn接收參數的時候是fn(context, arg0, arg1, arg2...),有什麼方法可以將參數變成這種形式呢。
**注意:**參數不是數組,不是收集起來直接放進去就可以的
api翻了一下,能滿足要求的就只有下面兩種方法:
- 使用eval函數
- 使用Function的構造函數
使用eval的方法會比較簡單,直接用字符串拼接一下參數,然後傳入eval運行就好。但是這種方法官方不推薦,存在被惡意攻擊的風險。這裏採用Function的方案實現。
Function.prototype.myCall = function(that) {
var target = this;
var args = arguments, argArr = [];
var args = arguments, argNameArr = [], argArr = [];
for (var i = 0; i < args.length; i++) {
argNameArr.push('arguments[1][' + i + ']');
argArr.push(arguments[i]);
}
argNameArr.pop(); // 去除多的arguments名字
argArr.shift(); // 去除第一個參數 context
that.fn = target; // 給that添加函數 fn
var result = new Function('fn', 'return fn(' + argNameArr.join(',') + ')')(that.fn, argArr); // 立即調用執行方法
delete that.fn; // 移除函數 fn
return result;
}
考慮一下特殊情況,傳入的上下文爲空的時候,實現代碼如下:
value = 111;
var foo = {
value: 1
}
function bar(p1, p2) {
console.log(this.value + ',' + p1 + ',' + p2);
}
Function.prototype.myCall = function (that) {
var target = this;
// 如果target不是function
if (typeof target !== 'function') {
throw new TypeError(target + ' is not type of Function');
}
// 如果that爲空,將全局的this賦值給that
if (!that) {
that = function() {
return this;
}()
}
// 獲取參數
var args = arguments, argNameArr = [], argArr = [];
for (var i = 0; i < args.length; i++) {
argNameArr.push('arguments[1][' + i + ']');
argArr.push(arguments[i]);
}
argNameArr.pop(); // 去除多的arguments名字
argArr.shift(); // 去除第一個參數 context
that.fn = target; // 給that添加函數 fn
var result = new Function('fn', 'return fn(' + argNameArr.join(',') + ')')(that.fn, argArr); // 立即調用執行方法
delete that.fn; // 移除函數 fn
return result;
}
bar.myCall(foo, 2, 3);
這樣一來,一個call的模擬實現就完成了。
四、apply的用法
apply是Function
默認實現的函數,使用方式爲:fn.apply(thisArg, [argsArray])
。實例代碼如下:
var foo = { value: 1 };
function fn() {
console.log(this.value);
}
fn.apply(foo); // value => 1
五、模擬實現apply
apply和call的唯一區別在於接收的參數不同,那需要改的地方就只有對參數的處理就好了。
value = 111;
var foo = {
value: 1
}
function bar(p1, p2) {
console.log(this.value + ',' + p1 + ',' + p2);
}
Function.prototype.myApply = function (that) {
var target = this;
// 如果target不是function
if (typeof target !== 'function') {
throw new TypeError(target + ' is not type of Function');
}
// 如果that爲空,將全局的this賦值給that
if (!that) {
that = function () {
return this;
}()
}
// apply第二個參數是數組
var args = arguments.length > 1 ? arguments[1] : [], argNameArr = [];
// 拼接參數名稱 arguments[1][0]、arguments[1][1]...
for (var i = 0; i < args.length; i++) {
argNameArr.push('arguments[1][' + i + ']');
}
// args 不是數組類型
if (args instanceof Array) {
that.fn = target;
result = new Function('fn', 'return fn(' + argNameArr.join(',') + ')')(that.fn, args);
} else {
throw new ReferenceError('arguments 1 must be type of Array');
}
delete that.fn;
return result;
}
bar.myApply(null, [2, 3]);
六、總結
想要深入的瞭解call和apply還是得實現一下,不然有很多地方總是會想不清楚。比如說函數的上下文是怎麼改變的。只是實現了這兩個函數,收穫如下:
- 如何獲取全局的this
- Function的構造函數使用
- arguments參數的使用