call/apply/bind
日常編碼中被開發(fā)者用來實現(xiàn) “對象冒充”喷斋,也即 “顯示綁定 this
“。
面試題:“call/apply/bind源碼實現(xiàn)”茧彤,事實上是對 JavaScript 基礎(chǔ)知識的一個綜合考核骡显。
相關(guān)知識點:
- 作用域;
- this 指向曾掂;
- 函數(shù)柯里化惫谤;
- 原型與原型鏈;
call/apply/bind 的區(qū)別
- 三者都可用于顯示綁定
this
; -
call/apply
的區(qū)別方式在于參數(shù)傳遞方式的不同珠洗;-
fn.call(obj, arg1, arg2, ...)
溜歪, 傳參數(shù)列表,以逗號隔開许蓖; -
fn.apply(obj, [arg1, arg2, ...])
蝴猪, 傳參數(shù)數(shù)組调衰;
-
-
bind
返回的是一個待執(zhí)行函數(shù),是函數(shù)柯里化的應(yīng)用自阱,而call/apply
則是立即執(zhí)行函數(shù)
思路初探
Function.prototype.myCall = function(context) {
// 原型中 this 指向的是實例對象嚎莉,所以這里指向 [Function: bar]
console.log(this); // [Function: bar]
// 在傳入的上下文對象中,創(chuàng)建一個屬性沛豌,值指向方法 bar
context.fn = this; // foo.fn = [Function: bar]
// 調(diào)用這個方法趋箩,此時調(diào)用者是 foo,this 指向 foo
context.fn();
// 執(zhí)行后刪除它加派,僅使用一次叫确,避免該屬性被其它地方使用(遍歷)
delete context.fn;
};
let foo = {
value: 2
};
function bar() {
console.log(this.value);
}
// bar 函數(shù)的聲明等同于:var bar = new Function("console.log(this.value)");
bar.call(foo); // 2;
call 的源碼實現(xiàn)
初步思路有個大概,剩下的就是完善代碼芍锦。
// ES6 版本
Function.prototype.myCall = function(context, ...params) {
// ES6 函數(shù) Rest 參數(shù)竹勉,使其可指定一個對象,接收函數(shù)的剩余參數(shù)娄琉,合成數(shù)組
if (typeof context === 'object') {
context = context || window;
} else {
context = Object.create(null);
}
// 用 Symbol 來作屬性 key 值饶米,保持唯一性,避免沖突
let fn = Symbol();
context[fn] = this;
// 將參數(shù)數(shù)組展開车胡,作為多個參數(shù)傳入
const result = context[fn](...params);
// 刪除避免永久存在
delete(context[fn]);
// 函數(shù)可以有返回值
return result;
}
// 測試
var mine = {
name: '以樂之名'
}
var person = {
name: '無名氏',
sayHi: function(msg) {
console.log('我的名字:' + this.name + '檬输,', msg);
}
}
person.sayHi.myCall(mine, '很高興認(rèn)識你!');
// 我的名字:以樂之名匈棘,很高興認(rèn)識你丧慈!
知識點補(bǔ)充:
- ES6 新的原始數(shù)據(jù)類型
Symbol
,表示獨一無二的值; -
Object.create(null)
創(chuàng)建一個空對象
// 創(chuàng)建一個空對象的方式
// eg.A
let emptyObj = {};
// eg.B
let emptyObj = new Object();
// eg.C
let emptyObj = Object.create(null);
使用 Object.create(null)
創(chuàng)建的空對象主卫,不會受到原型鏈的干擾逃默。原型鏈終端指向 null
,不會有構(gòu)造函數(shù)簇搅,也不會有 toString
完域、 hasOwnProperty
、valueOf
等屬性瘩将,這些屬性來自 Object.prototype
吟税。有原型鏈基礎(chǔ)的伙伴們,應(yīng)該都知道姿现,所有普通對象的原型鏈都會指向 Object.prototype
肠仪。
所以 Object.create(null)
創(chuàng)建的空對象比其它兩種方式,更干凈备典,不會有 Object
原型鏈上的屬性异旧。
ES5 版本:
- 自行處理參數(shù);
- 自實現(xiàn)
Symobo
// ES5 版本
// 模擬Symbol
function getSymbol(obj) {
var uniqAttr = '00' + Math.random();
if (obj.hasOwnProperty(uniqAttr)) {
// 如果已存在提佣,則遞歸自調(diào)用函數(shù)
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;
// 無其它參數(shù)傳入
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 執(zhí)行
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, '很高興認(rèn)識你!');
// 我的名字:以樂之名潮针,很高興認(rèn)識术荤!
apply 的源碼實現(xiàn)
call
的源碼實現(xiàn),那么 apply
就簡單然低,兩者只是傳遞參數(shù)方式不同而已喜每。
Function.prototype.myApply = function(context, params) {
// apply 與 call 的區(qū)別务唐,第二個參數(shù)是數(shù)組雳攘,且不會有第三個參數(shù)
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 的源碼實現(xiàn)
-
bind
與call/apply
的區(qū)別就是返回的是一個待執(zhí)行的函數(shù),而不是函數(shù)的執(zhí)行結(jié)果; -
bind
返回的函數(shù)作為構(gòu)造函數(shù)與new
一起使用枫笛,綁定的this
需要被忽略;
調(diào)用綁定函數(shù)時作為this參數(shù)傳遞給目標(biāo)函數(shù)的值吨灭。 如果使用new運算符構(gòu)造綁定函數(shù),則忽略該值刑巧。 —— MDN
Function.prototype.bind = function(context, ...initArgs) {
// bind 調(diào)用的方法一定要是一個函數(shù)
if (typeof this !== 'function') {
throw new TypeError('not a function');
}
let self = this;
let F = function() {};
F.prototype = this.prototype;
let bound = function(...finnalyArgs) {
// 將前后參數(shù)合并傳入
return self.call(this instanceof F ? this : context || this, ...initArgs, ...finnalyArgs);
}
bound.prototype = new F();
return bound;
}
不少伙伴還會遇到這樣的追問喧兄,不使用 call/apply
,如何實現(xiàn) bind
啊楚?
騷年先別慌吠冤,不用 call/apply
,不就是相當(dāng)于把 call/apply
換成對應(yīng)的自我實現(xiàn)方法恭理,算是偷懶取個巧吧拯辙。
本篇 call/apply/bind
源碼實現(xiàn),算是對之前文章系列知識點的一次加深鞏固颜价。
“心中有碼涯保,前路莫慌≈苈祝”
參考文檔:
更多前端基石搭建夕春,盡在 Github,期待 Star专挪!
https://github.com/ZengLingYong/blog
作者:以樂之名
本文原創(chuàng)及志,有不當(dāng)?shù)牡胤綒g迎指出。轉(zhuǎn)載請指明出處寨腔。