call/apply/bind
日常編碼中被開發者用來實現 “對象冒充”,也即 “顯示綁定 this
“。
面試題:“call/apply/bind源碼實現”,事實上是對 JavaScript 基礎知識的一個綜合考覈。
相關知識點:
- 作用域;
- this 指向;
- 函數柯里化;
- 原型與原型鏈;
call/apply/bind 的區別
- 三者都可用於顯示綁定
this
; -
call/apply
的區別方式在於參數傳遞方式的不同;-
fn.call(obj, arg1, arg2, ...)
, 傳參數列表,以逗號隔開; -
fn.call(obj, [arg1, arg2, ...])
, 傳參數數組;
-
-
bind
返回的是一個待執行函數,是函數柯里化的應用,而call/apply
則是立即執行函數
思路初探
Function.prototype.myCall = function(context) {
// 原型中 this 指向的是實例對象,所以這裏指向 [Function: bar]
console.log(this); // [Function: bar]
// 在傳入的上下文對象中,創建一個屬性,值指向方法 bar
context.fn = this; // foo.fn = [Function: bar]
// 調用這個方法,此時調用者是 foo,this 指向 foo
context.fn();
// 執行後刪除它,僅使用一次,避免該屬性被其它地方使用(遍歷)
delete context.fn;
};
let foo = {
value: 2
};
function bar() {
console.log(this.value);
}
// bar 函數的聲明等同於:var bar = new Function("console.log(this.value)");
bar.call(foo); // 2;
call 的源碼實現
初步思路有個大概,剩下的就是完善代碼。
// ES6 版本
Function.prototype.myCall = function(context, ...params) {
// ES6 函數 Rest 參數,使其可指定一個對象,接收函數的剩餘參數,合成數組
if (typeof context === 'object') {
context = context || window;
} else {
context = Object.create(null);
}
// 用 Symbol 來作屬性 key 值,保持唯一性,避免衝突
let fn = Symbol();
context[fn] = this;
// 將參數數組展開,作爲多個參數傳入
const result = context[fn](...params);
// 刪除避免永久存在
delete(context[fn]);
// 函數可以有返回值
return result;
}
// 測試
var mine = {
name: '以樂之名'
}
var person = {
name: '無名氏',
sayHi: function(msg) {
console.log('我的名字:' + this.name + ',', msg);
}
}
person.sayHi.myCall(mine, '很高興認識你!');
// 我的名字:以樂之名,很高興認識你!
知識點補充:
- ES6 新的原始數據類型
Symbol
,表示獨一無二的值; -
Object.create(null)
創建一個空對象
// 創建一個空對象的方式
// eg.A
let emptyObj = {};
// eg.B
let emptyObj = new Object();
// eg.C
let emptyObj = Object.create(null);
使用 Object.create(null)
創建的空對象,不會受到原型鏈的干擾。原型鏈終端指向 null
,不會有構造函數,也不會有 toString
、 hasOwnProperty
、valueOf
等屬性,這些屬性來自 Object.prototype
。有原型鏈基礎的夥伴們,應該都知道,所有普通對象的原型鏈都會指向 Object.prototype
。
所以 Object.create(null)
創建的空對象比其它兩種方式,更乾淨,不會有 Object
原型鏈上的屬性。
ES5 版本:
- 自行處理參數;
- 自實現
Symobo
// ES5 版本
// 模擬Symbol
function getSymbol(obj) {
var uniqAttr = '00' + Math.random();
if (obj.hasOwnProperty(uniqAttr)) {
// 如果已存在,則遞歸自調用函數
arguments.callee(obj);
} else {
return uniqAttr;
}
}
Function.prototype.myCall = function() {
var args = arguments;
if (!args.length) return;
var context = [].shift.apply(args);
context = context || window;
var fn = getSymbol(context);
context[fn] = this;
// 無其它參數傳入
if (!arguments.length) {
return context[fn];
}
var param = args[i];
// 類型判斷,不然 eval 運行會出錯
var paramType = typeof param;
switch(paramType) {
case 'string':
param = '"' + param + '"'
break;
case 'object':
param = JSON.stringify(param);
break;
}
fnStr += i == args.length - 1 ? param : param + ',';
// 藉助 eval 執行
var result = eval(fnStr);
delete context[fn];
return result;
}
// 測試
var mine = {
name: '以樂之名'
}
var person = {
name: '無名氏',
sayHi: function(msg) {
console.log('我的名字:' + this.name + ',', msg);
}
}
person.sayHi.myCall(mine, '很高興認識你!');
// 我的名字:以樂之名,很高興認識!
apply 的源碼實現
call
的源碼實現,那麼 apply
就簡單,兩者只是傳遞參數方式不同而已。
Function.prototype.myApply = function(context, params) {
// apply 與 call 的區別,第二個參數是數組,且不會有第三個參數
if (typeof context === 'object') {
context = context || window;
} else {
context = Object.create(null);
}
let fn = Symbol();
context[fn] = this;
const result context[fn](...params);
delete context[fn];
return result;
}
bind 的源碼實現
-
bind
與call/apply
的區別就是返回的是一個待執行的函數,而不是函數的執行結果; -
bind
返回的函數作爲構造函數與new
一起使用,綁定的this
需要被忽略;
調用綁定函數時作爲this參數傳遞給目標函數的值。 如果使用new運算符構造綁定函數,則忽略該值。 —— MDN
Function.prototype.bind = function(context, ...initArgs) {
// bind 調用的方法一定要是一個函數
if (typeof this !== 'function') {
throw new TypeError('not a function');
}
let self = this;
let F = function() {};
F.prototype = this.prototype;
let bound = function(...finnalyArgs) {
// 將前後參數合併傳入
return self.call(this instanceof F ? this : context || this, ...initArgs, ...finnalyArgs);
}
bound.prototype = new F();
return bound;
}
不少夥伴還會遇到這樣的追問,不使用 call/apply
,如何實現 bind
?
騷年先別慌,不用 call/apply
,不就是相當於把 call/apply
換成對應的自我實現方法,算是偷懶取個巧吧。
本篇 call/apply/bind
源碼實現,算是對之前文章系列知識點的一次加深鞏固。
“心中有碼,前路莫慌。”
參考文檔:
更多前端基石搭建,盡在 Github,期待 Star!
https://github.com/ZengLingYong/blog
作者:以樂之名
本文原創,有不當的地方歡迎指出。轉載請指明出處。