實(shí)現(xiàn)自己的call
MDN 定義:
call() 提供新的 this 值給當(dāng)前調(diào)用的函數(shù)/方法汇跨。你可以使用
call
來(lái)實(shí)現(xiàn)繼承:寫(xiě)一個(gè)方法,然后讓另外一個(gè)新的對(duì)象來(lái)繼承它(而不是在新對(duì)象中再寫(xiě)一次這個(gè)方法)妆距。
簡(jiǎn)答的概括就是:
call() 方法在使用一個(gè)指定的 this 值和若干個(gè)指定的參數(shù)值的前提下調(diào)用某個(gè)函數(shù)或方法穷遂。
舉個(gè)例子:
var foo = {
value: 1
};
function bar() {
console.log(this.value);
}
bar.call(foo); // 1
簡(jiǎn)單的解析一下call都做了什么:
第一步:call 改變了 this 的指向,指向到 foo
第二步:bar 函數(shù)執(zhí)行
函數(shù)通過(guò) call 調(diào)用后娱据,結(jié)構(gòu)就如下面代碼:
var foo = {
value: 1,
bar: function() {
console.log(this.value)
}
};
foo.bar(); // 1
這樣this 就指向了 foo蚪黑,但是我們給foo添加了一個(gè)屬性,這并不可取中剩。所以我們還要執(zhí)行一步刪除的動(dòng)作忌穿。
所以我們模擬的步驟可以分為:
第一步:將函數(shù)設(shè)為傳入對(duì)象的屬性
第二步:執(zhí)行該函數(shù)
第三部:刪除該函數(shù)
以上個(gè)例子為例,就是:
// 第一步
foo.fn = bar
// 第二步
foo.fn()
// 第三步
delete foo.fn
注意:fn 是對(duì)象的臨時(shí)屬性结啼,因?yàn)閳?zhí)行過(guò)后要?jiǎng)h除滴掠剑。
根據(jù)這個(gè)思路,我們可以嘗試著去寫(xiě)一個(gè)call
Function.prototype._call = function(context) {
// 首先要獲取調(diào)用call的函數(shù)郊愧,用this可以獲取
context.fn = this;
context.fn();
delete context.fn;
}
// 測(cè)試一下
var foo = {
value: 1
};
function bar() {
console.log(this.value);
}
bar._call(foo); // 1
OK朴译,我們可以在控制臺(tái)看到結(jié)果了,和預(yù)想的一樣属铁。
這樣只是將第一個(gè)參數(shù)作為上下文進(jìn)行執(zhí)行眠寿,但是并沒(méi)用傳入?yún)?shù),下面我們嘗試傳入?yún)?shù)執(zhí)行焦蘑。
舉個(gè)例子:
var foo = {
value: 1
};
function bar(name, age) {
console.log(name)
console.log(age)
console.log(this.value);
}
bar.call(foo, 'chris', 10);
// chris
// 10
// 1
我們會(huì)發(fā)現(xiàn)參數(shù)并不固定盯拱,所以要在 Arguments 對(duì)象的第二個(gè)參數(shù)截取,傳入到數(shù)組中例嘱。
比如這樣:
// 以上個(gè)例子為例狡逢,此時(shí)的arguments為:
// arguments = {
// 0: foo,
// 1: 'kevin',
// 2: 18,
// length: 3
// }
// 因?yàn)閍rguments是類(lèi)數(shù)組對(duì)象,所以可以用for循環(huán)
var args = [];
vae len = arguments.length
for(var i = 1, i < len; i++) {
args.push('arguments[' + i + ']');
}
// 執(zhí)行后 args為 ["arguments[1]", "arguments[2]", "arguments[3]"]
OK拼卵,看到這樣操作第一反應(yīng)會(huì)想到 ES6 的方法奢浑,不過(guò) call 是 ES3 的方法,所以就麻煩一點(diǎn)吧间学。所以我們這次用 eval
方法拼成一個(gè)函數(shù),類(lèi)似于這樣:
eval('context.fn(' + args +')')
這里 args 會(huì)自動(dòng)調(diào)用 Array.toString() 這個(gè)方法。
代碼如下:
Function.prototype._call = function(context) {
context.fn = this;
var args = [];
for(var i = 1, len = arguments.length; i < len; i++) {
args.push('arguments[' + i + ']');
}
eval('context.fn(' + args +')');
delete context.fn;
}
// 測(cè)試一下
var foo = {
value: 1
};
function bar(name, age) {
console.log(name)
console.log(age)
console.log(this.value);
}
bar._call(foo, 'chris', 10);
// chris
// 10
// 1
OK低葫,這樣我們實(shí)現(xiàn)了 80% call的功能详羡。
再看看定義:
根據(jù) MDN 對(duì) call 語(yǔ)法的定義:
第一個(gè)參數(shù):
在 fun 函數(shù)運(yùn)行時(shí)指定的
this
值。需要注意的是嘿悬,指定的this
值并不一定是該函數(shù)執(zhí)行時(shí)真正的this
值实柠,如果這個(gè)函數(shù)在非嚴(yán)格模式
下運(yùn)行,則指定為null
和undefined
的this
值會(huì)自動(dòng)指向全局對(duì)象(瀏覽器中就是 window 對(duì)象)善涨,同時(shí)值為原始值(數(shù)字窒盐,字符串,布爾值)的this
會(huì)指向該原始值的自動(dòng)包裝對(duì)象钢拧。
執(zhí)行參數(shù):
使用調(diào)用者提供的
this
值和參數(shù)調(diào)用該函數(shù)的返回值蟹漓。若該方法沒(méi)有返回值,則返回undefined
源内。
所以我們還需要注意兩個(gè)點(diǎn)
1.this 參數(shù)可以傳 null葡粒,當(dāng)為 null 的時(shí)候,視為指向 window
舉個(gè)例子:
var value = 1;
function bar() {
console.log(this.value);
}
bar.call(null); // 1
雖然這個(gè)例子本身不使用 call膜钓,結(jié)果依然一樣嗽交。
2.函數(shù)是可以有返回值
舉個(gè)例子:
var obj = {
value: 1
}
function bar(name, age) {
return {
value: this.value,
name: name,
age: age
}
}
bar.call(obj, 'chris', 10)
// Object {
// value: 1,
// name: 'chris',
// age: 10
// }
不過(guò)都很好解決,讓我們直接看第三版也就是最后一版的代碼:
Function.prototype._call = function (context = window) {
var context = context;
context.fn = this;
var args = [];
for(var i = 1, len = arguments.length; i < len; i++) {
args.push('arguments[' + i + ']');
}
var result = eval('context.fn(' + args +')');
delete context.fn
return result;
}
// 測(cè)試一下
var value = 2;
var obj = {
value: 1
}
function bar(name, age) {
console.log(this.value);
return {
value: this.value,
name: name,
age: age
}
}
bar._call(null); // 2
console.log(bar._call(obj, 'kevin', 18));
// 1
// Object {
// value: 1,
// name: 'kevin',
// age: 18
// }
這樣我們就成功的完成了一個(gè)call函數(shù)颂斜。
實(shí)現(xiàn)自己的apply
apply 的實(shí)現(xiàn)跟 call 類(lèi)似夫壁,只是后面?zhèn)鞯膮?shù)是一個(gè)數(shù)組或者類(lèi)數(shù)組對(duì)象。
Function.prototype.apply = function (context = window, arr) {
var context = context;
context.fn = this;
var result;
if (!arr) {
result = context.fn();
}
else {
var args = [];
for (var i = 0, len = arr.length; i < len; i++) {
args.push('arr[' + i + ']');
}
result = eval('context.fn(' + args + ')')
}
delete context.fn
return result;
}
實(shí)現(xiàn)自己的bind
根據(jù) MDN 定義:
bind() 方法會(huì)創(chuàng)建一個(gè)新函數(shù)沃疮。當(dāng)這個(gè)新函數(shù)被調(diào)用時(shí)盒让,bind() 的第一個(gè)參數(shù)將作為它運(yùn)行時(shí)的 this,之后的一序列參數(shù)將會(huì)在傳遞的實(shí)參前傳入作為它的參數(shù)忿磅。
由此我們可以首先得出 bind 函數(shù)的三個(gè)特點(diǎn):
- 改變this指向
- 返回一個(gè)函數(shù)
- 可以傳入?yún)?shù)
var foo = {
value: 1
};
function bar() {
console.log(this.value);
}
var bindFoo = bar.bind(foo); // 返回了一個(gè)函數(shù)
bindFoo(); // 1
關(guān)于指定 this 的指向糯彬,我們可以使用 call 或者 apply 實(shí)現(xiàn)。
Function.prototype._bind = function (context) {
var self = this;
return function () {
return self.apply(context);
}
}
之所以是 return self.apply(context)
葱她,是考慮到綁定函數(shù)可能是有返回值的撩扒,依然是這個(gè)例子:
var foo = {
value: 1
};
function bar() {
return this.value;
}
var bindFoo = bar.bind(foo);
console.log(bindFoo()); // 1
第三點(diǎn),可以傳入?yún)?shù)吨些。這個(gè)很困惑是在 bind 時(shí)傳參還是在 bind 之后傳參搓谆。
var foo = {
value: 1
};
function bar(name, age) {
console.log(this.value);
console.log(name);
console.log(age);
}
var bindFoo = bar.bind(foo, 'chris');
bindFoo('18');
// 1
// chris
// 18
通過(guò)實(shí)例,我們發(fā)現(xiàn)兩者參數(shù)是可以累加的豪墅,就是第一次 bind 時(shí)傳的參數(shù)和可以在調(diào)用的時(shí)候傳入泉手。
所以我們還是用 arguments 進(jìn)行處理:
Function.prototype._bind = function (context) {
var self = this;
// 獲取_bind函數(shù)從第二個(gè)參數(shù)到最后一個(gè)參數(shù)
var args = Array.prototype.slice.call(arguments, 1);
return function () {
// 這個(gè)時(shí)候的arguments是指bind返回的函數(shù)傳入的參數(shù)
var bindArgs = Array.prototype.slice.call(arguments);
return self.apply(context, args.concat(bindArgs));
}
}
完成了上面三步,其實(shí)我們還有一個(gè)問(wèn)題沒(méi)有解決偶器。
根據(jù) MDN 定義:
一個(gè)綁定函數(shù)也能使用new操作符創(chuàng)建對(duì)象:這種行為就像把原函數(shù)當(dāng)成構(gòu)造器斩萌。提供的 this 值被忽略缝裤,同時(shí)調(diào)用時(shí)的參數(shù)被提供給模擬函數(shù)。
舉個(gè)例子:
var value = 2;
var foo = {
value: 1
};
function bar(name, age) {
this.habit = 'shopping';
console.log(this.value);
console.log(name);
console.log(age);
}
bar.prototype.friend = 'james';
var bindFoo = bar.bind(foo, 'chris');
var obj = new bindFoo('18');
// undefined
// chris
// 18
console.log(obj.habit);
console.log(obj.friend);
// shopping
// james
盡管在全局和 foo 中都聲明了 value 值颊郎,還是返回了 undefind憋飞,說(shuō)明this已經(jīng)失效了,如果大家了解 new 的實(shí)現(xiàn)姆吭,就會(huì)知道this是指向 obj 的榛做。
所以我們可以通過(guò)修改返回的函數(shù)的原型來(lái)實(shí)現(xiàn),讓我們寫(xiě)一下:
Function.prototype.bind2 = function (context) {
var self = this;
var args = Array.prototype.slice.call(arguments, 1);
var fBound = function () {
var bindArgs = Array.prototype.slice.call(arguments);
// 當(dāng)作為構(gòu)造函數(shù)時(shí)内狸,this 指向?qū)嵗烀校藭r(shí)結(jié)果為 true,將綁定函數(shù)的 this 指向該實(shí)例昆淡,可以讓實(shí)例獲得來(lái)自綁定函數(shù)的值
// 以上面的是 demo 為例锰瘸,如果改成 `this instanceof fBound ? null : context`,實(shí)例只是一個(gè)空對(duì)象瘪撇,將 null 改成 this 获茬,實(shí)例會(huì)具有 habit 屬性
// 當(dāng)作為普通函數(shù)時(shí),this 指向 window倔既,此時(shí)結(jié)果為 false恕曲,將綁定函數(shù)的 this 指向 context
return self.apply(this instanceof fBound ? this : context, args.concat(bindArgs));
}
// 修改返回函數(shù)的 prototype 為綁定函數(shù)的 prototype,實(shí)例就可以繼承綁定函數(shù)的原型中的值
fBound.prototype = this.prototype;
return fBound;
}
但是在這個(gè)寫(xiě)法中渤涌,我們直接將 fBound.prototype = this.prototype佩谣,我們直接修改 fBound.prototype 的時(shí)候,也會(huì)直接修改綁定函數(shù)的 prototype实蓬。這個(gè)時(shí)候茸俭,我們可以需要一個(gè)空函數(shù)來(lái)進(jìn)行中轉(zhuǎn):
Function.prototype._bind = function (context) {
var self = this;
var args = Array.prototype.slice.call(arguments, 1);
var fNOP = function () {};
var fBound = function () {
var bindArgs = Array.prototype.slice.call(arguments);
return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
}
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
return fBound;
}
還存在一些問(wèn)題:
1.調(diào)用 bind 的不是函數(shù)咋辦?
做一個(gè)類(lèi)型判斷唄
if (typeof this !== "function") {
throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
}
2.我要在線上用
做一下兼容性測(cè)試
Function.prototype.bind = Function.prototype.bind || function () {
……
};
好了安皱,這樣就我們就完成了一個(gè) bind
Function.prototype._bind = function (context) {
if (typeof this !== "function") {
throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
}
var self = this;
var args = Array.prototype.slice.call(arguments, 1);
var fNOP = function () {};
var fBound = function () {
var bindArgs = Array.prototype.slice.call(arguments);
return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
}
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
return fBound;
}
補(bǔ)充
eval
根據(jù) MDN 定義:表示JavaScript表達(dá)式调鬓,語(yǔ)句或一系列語(yǔ)句的字符串。表達(dá)式可以包含變量以及已存在對(duì)象的屬性酌伊。
一個(gè)簡(jiǎn)單的例子:
var x = 2;
var y = 39;
function add(x,y){
return x + y
}
eval('add('+ ['x','y'] + ')')//等于add(x,y)
也就說(shuō)eavl調(diào)用函數(shù)后腾窝,字符串會(huì)被解析出變量,達(dá)到去掉字符串調(diào)用變量的目的居砖。
JavaScript基礎(chǔ)系列目錄
JavaScript基礎(chǔ)專(zhuān)題之原型與原型鏈(一)
JavaScript基礎(chǔ)專(zhuān)題之執(zhí)行上下文和執(zhí)行棧(二)
JavaScript基礎(chǔ)專(zhuān)題之深入執(zhí)行上下文(三)
JavaScript基礎(chǔ)專(zhuān)題之閉包(四)
JavaScript基礎(chǔ)專(zhuān)題之參數(shù)傳遞(五)
新手寫(xiě)作虹脯,如果有錯(cuò)誤或者不嚴(yán)謹(jǐn)?shù)牡胤剑?qǐng)大伙給予指正奏候。如果這片文章對(duì)你有所幫助或者有所啟發(fā)循集,還請(qǐng)給一個(gè)贊,鼓勵(lì)一下作者蔗草,在此謝過(guò)咒彤。