可能遇到假的面試題:不用call和apply方法模擬實現(xiàn)ES5的bind方法

本文首發(fā)我的個人博客:前端小密圈,評論交流送1024邀請碼,嘿嘿嘿??映挂。

來自朋友去某信用卡管家的做的一道面試題泽篮,用原生JavaScript模擬ES5bind方法,不準用callbind方法袖肥。

至于結(jié)果嘛咪辱。。椎组。那個人當然是沒寫出來油狂,我就自己嘗試研究了一番,其實早就寫了寸癌,一直沒有組織好語言發(fā)出來专筷。

額。蒸苇。磷蛹。這個題有點刁鉆,這是對JavaScript基本功很好的一個檢測溪烤,看你JavaScript掌握的怎么樣以及平時有沒有去深入研究一些方法的實現(xiàn)味咳,簡而言之,就是有沒有折騰精神檬嘀。

不準用不用callapply方法槽驶,這個沒啥好說的,不準用我們就用原生JavaScript先來模擬一個apply方法鸳兽,感興趣的童鞋也可以看看chromev8怎么實現(xiàn)這個方法的掂铐,這里我只按照自己的思維實現(xiàn),在模擬之前我們先要明白和了解原生callapply方法是什么揍异。

簡單粗暴地來說全陨,callapply衷掷,bind是用于綁定this指向的辱姨。(如果你還不了解JS中this的指向問題,以及執(zhí)行環(huán)境上下文的奧秘戚嗅,這篇文章暫時就不太適合閱讀)炮叶。

什么是call和apply方法

我們單獨看看ECMAScript規(guī)范對apply的定義,看個大概就行:

15.3.4.3 Function.prototype.apply (thisArg, argArray)

順便貼一貼中文版渡处,免得翻譯一下镜悉,中文版地址

通過定義簡單說一下call和apply方法,他們就是參數(shù)不同医瘫,作用基本相同侣肄。

1、每個函數(shù)都包含兩個非繼承而來的方法:apply()和call()醇份。
2稼锅、他們的用途相同吼具,都是在特定的作用域中調(diào)用函數(shù)。
3矩距、接收參數(shù)方面不同拗盒,apply()接收兩個參數(shù),一個是函數(shù)運行的作用域(this)锥债,另一個是參數(shù)數(shù)組陡蝇。
4、call()方法第一個參數(shù)與apply()方法相同哮肚,但傳遞給函數(shù)的參數(shù)必須列舉出來登夫。

知道定義然后,直接看個簡單的demo

var jawil = {
    name: "jawil",
    sayHello: function (age) {
         console.log("hello, i am ", this.name + " " + age + " years old");
     }
};

var  lulin = {
    name: "lulin",
};

jawil.sayHello(24);

// hello, i am jawil 24 years old

然后看看使用applycall之后的輸出:

jawil.sayHello.call(lulin, 24);// hello, i am lulin 24 years old

jawil.sayHello.apply(lulin, [24]);// hello, i am lulin 24 years old

結(jié)果都相同允趟。從寫法上我們就能看出二者之間的異同恼策。相同之處在于,第一個參數(shù)都是要綁定的上下文潮剪,后面的參數(shù)是要傳遞給調(diào)用該方法的函數(shù)的涣楷。不同之處在于,call方法傳遞給調(diào)用函數(shù)的參數(shù)是逐個列出的抗碰,而apply則是要寫在數(shù)組中醋奠。

總結(jié)一句話介紹callapply

call()方法在使用一個指定的this值和若干個指定的參數(shù)值的前提下調(diào)用某個函數(shù)或方法悯周。
apply()方法在使用一個指定的this值和參數(shù)值必須是數(shù)組類型的前提下調(diào)用某個函數(shù)或方法塘辅。

分析call和apply的原理

上面代碼民泵,我們注意到了兩點:

  1. callapply改變了this的指向迄汛,指向到lulin
  2. sayHello函數(shù)執(zhí)行了

這里默認大家都對this有一個基本的了解捍壤,知道什么時候this該指向誰,我們結(jié)合這兩句話來分析這個通用函數(shù):f.apply(o),我們直接看一本書對其中原理的解讀鞍爱,具體什么書鹃觉,我也不知道,參數(shù)我們先不管睹逃,先了解其中的大致原理盗扇。

注意紅色框中的部分,f.call(o)其原理就是先通過 o.m = f 將 f作為o的某個臨時屬性m存儲沉填,然后執(zhí)行m疗隶,執(zhí)行完畢后將m屬性刪除。

知道了這個基本原來我們再來看看剛才jawil.sayHello.call(lulin, 24)執(zhí)行的過程:

// 第一步
lulin.fn = jawil.sayHello
// 第二步
lulin.fn()
// 第三步
delete lulin.fn

上面的說的是原理翼闹,可能你看的還有點抽象斑鼻,下面我們用代碼模擬實現(xiàn)apply一下。

實現(xiàn)aplly方法

模擬實現(xiàn)第一步

根據(jù)這個思路猎荠,我們可以嘗試著去寫第一版的 applyOne 函數(shù):

// 第一版
Function.prototype.applyOne = function(context) {
    // 首先要獲取調(diào)用call的函數(shù)坚弱,用this可以獲取
    context.fn = this;
    context.fn();
    delete context.fn;
}

//簡單寫一個不帶參數(shù)的demo
var jawil = {
    name: "jawil",
    sayHello: function (age) {
         console.log(this.name);
     }
};

var  lulin = {
    name: "lulin",
};

//看看結(jié)果:
jawil.sayHello.applyOne(lulin)//lulin

正好可以打印lulin而不是之前的jawil了蜀备,哎,不容易盎囊丁碾阁!??

模擬實現(xiàn)第二步

最一開始也講了,apply函數(shù)還能給定參數(shù)執(zhí)行函數(shù)些楣。舉個例子:

var jawil = {
    name: "jawil",
    sayHello: function (age) {
         console.log(this.name,age);
     }
};

var  lulin = {
    name: "lulin",
};

jawil.sayHello.apply(lulin,[24])//lulin 24

注意:傳入的參數(shù)就是一個數(shù)組脂凶,很簡單,我們可以從Arguments對象中取值戈毒,Arguments不知道是何物艰猬,趕緊補習(xí),此文也不太適合初學(xué)者埋市,第二個參數(shù)就是數(shù)組對象冠桃,但是執(zhí)行的時候要把數(shù)組數(shù)值傳遞給函數(shù)當參數(shù),然后執(zhí)行道宅,這就需要一點小技巧食听。

參數(shù)問題其實很簡單,我們先偷個懶污茵,我們接著要把這個參數(shù)數(shù)組放到要執(zhí)行的函數(shù)的參數(shù)里面去樱报。

Function.prototype.applyTwo = function(context) {
    // 首先要獲取調(diào)用call的函數(shù),用this可以獲取
    context.fn = this;
    var args = arguments[1] //獲取傳入的數(shù)組參數(shù)
    context.fn(args.join(',');
    delete context.fn;
}

很簡單是不是泞当,那你就錯了迹蛤,數(shù)組join方法返回的是啥?

typeof [1,2,3,4].join(',')//string

Too young,too simple啊襟士,最后是一個 "1,2,3,4" 的字符串盗飒,其實就是一個參數(shù),肯定不行啦陋桂。

也許有人會想到用ES6的一些奇淫方法逆趣,不過applyES3的方法,我們?yōu)榱四M實現(xiàn)一個ES3的方法嗜历,要用到ES6的方法宣渗,反正面試官也沒說不準這樣。但是我們這次用eval方法拼成一個函數(shù)梨州,類似于這樣:

eval('context.fn(' + args +')')

先簡單了解一下eval函數(shù)吧
定義和用法

eval() 函數(shù)可計算某個字符串痕囱,并執(zhí)行其中的的 JavaScript 代碼。

語法:
eval(string)

string必需暴匠。要計算的字符串鞍恢,其中含有要計算的 JavaScript 表達式或要執(zhí)行的語句。該方法只接受原始字符串作為參數(shù),如果 string 參數(shù)不是原始字符串有序,那么該方法將不作任何改變地返回抹腿。因此請不要為 eval() 函數(shù)傳遞 String 對象來作為參數(shù)。

簡單來說吧旭寿,就是用JavaScript的解析引擎來解析這一堆字符串里面的內(nèi)容警绩,這么說吧,你可以這么理解盅称,你把eval看成是<script>標簽肩祥。

eval('function Test(a,b,c,d){console.log(a,b,c,d)};Test(1,2,3,4)')

就是相當于這樣

<script>
function Test(a,b,c,d){
console.log(a,b,c,d)
};
Test(1,2,3,4)
</script>

第二版代碼大致如下:

Function.prototype.applyTwo = function(context) {
    var args = arguments[1]; //獲取傳入的數(shù)組參數(shù)
    context.fn = this; //假想context對象預(yù)先不存在名為fn的屬性
    var fnStr = 'context.fn(';
    for (var i = 0; i < args.length; i++) {
        fnStr += i == args.length - 1 ? args[i] : args[i] + ',';
    }
    fnStr += ')';//得到"context.fn(arg1,arg2,arg3...)"這個字符串在,最后用eval執(zhí)行
    eval(fnStr); //還是eval強大
    delete context.fn; //執(zhí)行完畢之后刪除這個屬性
}
//測試一下
var jawil = {
    name: "jawil",
    sayHello: function (age) {
         console.log(this.name,age);
     }
};

var  lulin = {
    name: "lulin",
};

jawil.sayHello.applyTwo(lulin,[24])//lulin 24

好像就行了是不是缩膝,其實這只是最粗糙的版本混狠,能用,但是不完善疾层,完成了大約百分之六七十了将饺。

模擬實現(xiàn)第三步

其實還有幾個小地方需要注意:

1.this參數(shù)可以傳null或者不傳,當為null的時候痛黎,視為指向window

舉個兩個簡單栗子栗子??:
demo1:

var name = 'jawil';

function sayHello() {
    console.log(this.name);
}

sayHello.apply(null); // 'jawil'

demo2:

var name = 'jawil';

function sayHello() {
    console.log(this.name);
}

sayHello.apply(); // 'jawil'

2.函數(shù)是可以有返回值的.

舉個簡單栗子??:

var obj = {
    name: 'jawil'
}

function sayHello(age) {
    return {
        name: this.name,
        age: age
    }
}

console.log(sayHello.apply(obj,[24]));// {name: "jawil", age: 24}

這些都是小問題予弧,想到了,就很好解決湖饱。我們來看看此時的第三版apply模擬方法掖蛤。

//原生JavaScript封裝apply方法,第三版
Function.prototype.applyThree = function(context) {
    var context = context || window
    var args = arguments[1] //獲取傳入的數(shù)組參數(shù)
    context.fn = this //假想context對象預(yù)先不存在名為fn的屬性
    if (args == void 0) { //沒有傳入?yún)?shù)直接執(zhí)行
        return context.fn()
    }
    var fnStr = 'context.fn('
    for (var i = 0; i < args.length; i++) {
        //得到"context.fn(arg1,arg2,arg3...)"這個字符串在井厌,最后用eval執(zhí)行
        fnStr += i == args.length - 1 ? args[i] : args[i] + ','
    }
    fnStr += ')'
    var returnValue = eval(fnStr) //還是eval強大
    delete context.fn //執(zhí)行完畢之后刪除這個屬性
    return returnValue
}

好緊張蚓庭,再來做個小測試,demo仅仆,應(yīng)該不會出問題:

var obj = {
    name: 'jawil'
}

function sayHello(age) {
    return {
        name: this.name,
        age: age
    }
}

console.log(sayHello.applyThree(obj,[24]));// 完美輸出{name: "jawil", age: 24}

完美器赞?perfact?這就好了,不存在的蝇恶,我們來看看第四步的實現(xiàn)拳魁。

模擬實現(xiàn)第四步

其實一開始就埋下了一個隱患惶桐,我們看看這段代碼:

Function.prototype.applyThree = function(context) {
    var context = context || window
    var args = arguments[1] //獲取傳入的數(shù)組參數(shù)
    context.fn = this //假想context對象預(yù)先不存在名為fn的屬性
    ......
}

就是這句話撮弧, context.fn = this //假想context對象預(yù)先不存在名為fn的屬性,這就是一開始的隱患,我們只是假設(shè),但是并不能防止contenx對象一開始就沒有這個屬性姚糊,要想做到完美贿衍,就要保證這個context.fn中的fn的唯一性。

于是我自然而然的想到了強大的ES6,這玩意還是好用啊救恨,幸好早就了解并一直在使用ES6,還沒有學(xué)習(xí)過ES6的童鞋趕緊學(xué)習(xí)一下贸辈,沒有壞處的。

重新復(fù)習(xí)下新知識:
基本數(shù)據(jù)類型有6種:Undefined肠槽、Null擎淤、布爾值(Boolean)奢啥、字符串(String)數(shù)值(Number)嘴拢、對象(Object)桩盲。

ES5對象屬性名都是字符串容易造成屬性名的沖突。
舉個栗子??:

var a = { name: 'jawil'};
a.name = 'lulin';
//這樣就會重寫屬性

ES6引入了一種新的原始數(shù)據(jù)類型Symbol席吴,表示獨一無二的值赌结。

注意,Symbol函數(shù)前不能使用new命令孝冒,否則會報錯柬姚。這是因為生成的Symbol是一個原始類型的值,不是對象

Symbol函數(shù)可以接受一個字符串作為參數(shù)庄涡,表示對Symbol實例的描述量承,主要是為了在控制臺顯示,或者轉(zhuǎn)為字符串時穴店,比較容易區(qū)分宴合。

// 沒有參數(shù)的情況
var s1 = Symbol();
var s2 = Symbol();

s1 === s2 // false

// 有參數(shù)的情況
var s1 = Symbol("foo");
var s2 = Symbol("foo");

s1 === s2 // false

注意:Symbol值不能與其他類型的值進行運算。

作為屬性名的Symbol

var mySymbol = Symbol();

// 第一種寫法
var a = {};
a[mySymbol] = 'Hello!';

// 第二種寫法
var a = {
  [mySymbol]: 'Hello!'
};

// 第三種寫法
var a = {};
Object.defineProperty(a, mySymbol, { value: 'Hello!' });

// 以上寫法都得到同樣結(jié)果
a[mySymbol] // "Hello!"

注意迹鹅,Symbol值作為對象屬性名時卦洽,不能用點運算符。

看看下面這個栗子??:

var a = {};
var name = Symbol();
a.name = 'jawil';
a[name] = 'lulin';
console.log(a.name,a[name]);             //jawil,lulin

Symbol值作為屬性名時斜棚,該屬性還是公開屬性阀蒂,不是私有屬性。

這個有點類似于java中的protected屬性(protected和private的區(qū)別:在類的外部都是不可以訪問的弟蚀,在類內(nèi)的子類可以繼承protected不可以繼承private)

但是這里的Symbol在類外部也是可以訪問的蚤霞,只是不會出現(xiàn)在for...infor...of循環(huán)中义钉,也不會被Object.keys()昧绣、Object.getOwnPropertyNames()返回。但有一個Object.getOwnPropertySymbols方法捶闸,可以獲取指定對象的所有Symbol屬性名夜畴。

看看第四版的實現(xiàn)demo,想必大家了解上面知識已經(jīng)猜得到怎么寫了删壮,很簡單贪绘。
直接加個var fn = Symbol()就行了,央碟,税灌,

//原生JavaScript封裝apply方法,第四版
Function.prototype.applyFour = function(context) {
    var context = context || window
    var args = arguments[1] //獲取傳入的數(shù)組參數(shù)
    var fn = Symbol()
    context[fn] = this //假想context對象預(yù)先不存在名為fn的屬性
    if (args == void 0) { //沒有傳入?yún)?shù)直接執(zhí)行
        return context[fn]()
    }
    var fnStr = 'context[fn]('
    for (var i = 0; i < args.length; i++) {
        //得到"context.fn(arg1,arg2,arg3...)"這個字符串在,最后用eval執(zhí)行
        fnStr += i == args.length - 1 ? args[i] : args[i] + ','
    }
    fnStr += ')'
    var returnValue = eval(fnStr) //還是eval強大
    delete context[fn] //執(zhí)行完畢之后刪除這個屬性
    return returnValue
}

模擬實現(xiàn)第五步

呃呃呃額額菱涤,慢著苞也,ES3就出現(xiàn)的方法,你用ES6來實現(xiàn)粘秆,你好意思么墩朦?你可能會說,不管黑貓白貓翻擒,只要能抓住老鼠的貓就是好貓氓涣,面試官直說不準用callapply方法但是沒說不準用ES6語法啊。

反正公說公有理婆說婆有理陋气,這里還是不用Symbol方法實現(xiàn)一下劳吠,我們知道,ES6其實都是語法糖巩趁,ES6能寫的痒玩,咋們ES5都能實現(xiàn),這就導(dǎo)致了babel這類把ES6語法轉(zhuǎn)化成ES5的代碼了议慰。

至于babelSymbol屬性轉(zhuǎn)換成啥代碼了蠢古,我也沒去看,有興趣的可以看一下稍微研究一下别凹,這里我說一下簡單的模擬草讶。

ES5 沒有 Sybmol,屬性名稱只可能是一個字符串炉菲,如果我們能做到這個字符串不可預(yù)料堕战,那么就基本達到目標。要達到不可預(yù)期拍霜,一個隨機數(shù)基本上就解決了嘱丢。

//簡單模擬Symbol屬性
function jawilSymbol(obj) {
    var unique_proper = "00" + Math.random();
    if (obj.hasOwnProperty(unique_proper)) {
        arguments.callee(obj)//如果obj已經(jīng)有了這個屬性,遞歸調(diào)用祠饺,直到?jīng)]有這個屬性
    } else {
        return unique_proper;
    }
}
//原生JavaScript封裝apply方法越驻,第五版
Function.prototype.applyFive = function(context) {
    var context = context || window
    var args = arguments[1] //獲取傳入的數(shù)組參數(shù)
    var fn = jawilSymbol(context);
    context[fn] = this //假想context對象預(yù)先不存在名為fn的屬性
    if (args == void 0) { //沒有傳入?yún)?shù)直接執(zhí)行
        return context[fn]()
    }
    var fnStr = 'context[fn]('
    for (var i = 0; i < args.length; i++) {
        //得到"context.fn(arg1,arg2,arg3...)"這個字符串在,最后用eval執(zhí)行
        fnStr += i == args.length - 1 ? args[i] : args[i] + ','
    }
    fnStr += ')'
    var returnValue = eval(fnStr) //還是eval強大
    delete context[fn] //執(zhí)行完畢之后刪除這個屬性
    return returnValue
}

好緊張道偷,再來做個小測試缀旁,demo,應(yīng)該不會出問題:

var obj = {
    name: 'jawil'
}

function sayHello(age) {
    return {
        name: this.name,
        age: age
    }
}

console.log(sayHello.applyFive(obj,[24]));// 完美輸出{name: "jawil", age: 24}

到此试疙,我們完成了apply的模擬實現(xiàn)诵棵,給自己一個贊 b( ̄▽ ̄)d

實現(xiàn)Call方法

這個不需要講了吧抠蚣,道理都一樣祝旷,就是參數(shù)一樣,這里我給出我實現(xiàn)的一種方式,看不懂怀跛,自己寫一個去距贷。

//原生JavaScript封裝call方法
Function.prototype.callOne = function(context) {
    return this.applyFive(([].shift.applyFive(arguments), arguments) 
    //巧妙地運用上面已經(jīng)實現(xiàn)的applyFive函數(shù)
}

看不太明白也不能怪我咯,我就不細講了吻谋,看個demo證明一下忠蝗,這個寫法沒問題。

Function.prototype.applyFive = function(context) {//剛才寫的一大串}

Function.prototype.callOne = function(context) {
    return this.applyFive(([].shift.applyFive(arguments)), arguments)
    //巧妙地運用上面已經(jīng)實現(xiàn)的applyFive函數(shù)
}

//測試一下
var obj = {
    name: 'jawil'
}

function sayHello(age) {
    return {
        name: this.name,
        age: age
    }
}

console.log(sayHello.callOne(obj,24));// 完美輸出{name: "jawil", age: 24}

實現(xiàn)bind方法

養(yǎng)兵千日漓拾,用兵一時阁最。

什么是bind函數(shù)

如果掌握了上面實現(xiàn)apply的方法,我想理解起來模擬實現(xiàn)bind方法也是輕而易舉骇两,原理都差不多速种,我們還是來看看bind方法的定義。

我們還是簡單的看下ECMAScript規(guī)范對bind方法的定義低千,暫時看不懂不要緊配阵,獲取幾個關(guān)鍵信息就行。

15.3.4.5 Function.prototype.bind (thisArg [, arg1 [, arg2, …]])

注意一點示血,ECMAScript規(guī)范提到: Function.prototype.bind 創(chuàng)建的函數(shù)對象不包含 prototype 屬性或 [[Code]], [[FormalParameters]], [[Scope]] 內(nèi)部屬性棋傍。

bind() 方法會創(chuàng)建一個新函數(shù),當這個新函數(shù)被調(diào)用時难审,它的 this 值是傳遞給 bind() 的第一個參數(shù), 它的參數(shù)是 bind() 的其他參數(shù)和其原本的參數(shù)瘫拣,bind返回的綁定函數(shù)也能使用new操作符創(chuàng)建對象:這種行為就像把原函數(shù)當成構(gòu)造器。提供的this值被忽略告喊,同時調(diào)用時的參數(shù)被提供給模擬函數(shù)拂铡。。

語法是這樣樣子的:fun.bind(thisArg[, arg1[, arg2[, ...]]])

呃呃呃葱绒,是不是似曾相識感帅,這不是call方法的語法一個樣子么,地淀,失球,但它們是一樣的嗎?

bind方法傳遞給調(diào)用函數(shù)的參數(shù)可以逐個列出帮毁,也可以寫在數(shù)組中实苞。bind方法與call、apply最大的不同就是前者返回一個綁定上下文的函數(shù)烈疚,而后兩者是直接執(zhí)行了函數(shù)黔牵。由于這個原因,上面的代碼也可以這樣寫:

jawil.sayHello.bind(lulin)(24); //hello, i am lulin 24 years old
jawil.sayHello.bind(lulin)([24]); //hello, i am lulin 24 years old

bind方法還可以這樣寫 fn.bind(obj, arg1)(arg2).

用一句話總結(jié)bind的用法:該方法創(chuàng)建一個新函數(shù)爷肝,稱為綁定函數(shù)猾浦,綁定函數(shù)會以創(chuàng)建它時傳入bind方法的第一個參數(shù)作為this陆错,傳入bind方法的第二個以及以后的參數(shù)加上綁定函數(shù)運行時本身的參數(shù)按照順序作為原函數(shù)的參數(shù)來調(diào)用原函數(shù)。

bind在實際中的應(yīng)用

實際使用中我們經(jīng)常會碰到這樣的問題:

function Person(name){
 this.nickname = name;
 this.distractedGreeting = function() {
 
   setTimeout(function(){
     console.log("Hello, my name is " + this.nickname);
   }, 500);
 }
}
 
var alice = new Person('jawil');
alice.distractedGreeting();
//Hello, my name is undefined

這個時候輸出的this.nickname是undefined金赦,原因是this指向是在運行函數(shù)時確定的音瓷,而不是定義函數(shù)時候確定的,再因為setTimeout在全局環(huán)境下執(zhí)行夹抗,所以this指向setTimeout的上下文:window绳慎。關(guān)于this指向問題,這里就不細扯

以前解決這個問題的辦法通常是緩存this漠烧,例如:

function Person(name){
  this.nickname = name;
  this.distractedGreeting = function() {
    var self = this; // <-- 注意這一行!
    setTimeout(function(){
      console.log("Hello, my name is " + self.nickname); // <-- 還有這一行!
    }, 500);
  }
}
 
var alice = new Person('jawil');
alice.distractedGreeting();
// after 500ms logs "Hello, my name is jawil"

這樣就解決了這個問題杏愤,非常方便,因為它使得setTimeout函數(shù)中可以訪問Person的上下文已脓。但是看起來稍微一種蛋蛋的憂傷声邦。

但是現(xiàn)在有一個更好的辦法!您可以使用bind摆舟。上面的例子中被更新為:

function Person(name){
  this.nickname = name;
  this.distractedGreeting = function() {
    setTimeout(function(){
      console.log("Hello, my name is " + this.nickname);
    }.bind(this), 500); // <-- this line!
  }
}
 
var alice = new Person('jawil');
alice.distractedGreeting();
// after 500ms logs "Hello, my name is jawil"

bind() 最簡單的用法是創(chuàng)建一個函數(shù)亥曹,使這個函數(shù)不論怎么調(diào)用都有同樣的 this 值。JavaScript新手經(jīng)常犯的一個錯誤是將一個方法從對象中拿出來恨诱,然后再調(diào)用媳瞪,希望方法中的 this 是原來的對象。(比如在回調(diào)中傳入這個方法照宝。)如果不做特殊處理的話蛇受,一般會丟失原來的對象。從原來的函數(shù)和原來的對象創(chuàng)建一個綁定函數(shù)厕鹃,則能很漂亮地解決這個問題:

this.x = 9; 
var module = {
  x: 81,
  getX: function() { return this.x; }
};
 
module.getX(); // 81
 
var getX = module.getX;
getX(); // 9, 因為在這個例子中兢仰,"this"指向全局對象
 
// 創(chuàng)建一個'this'綁定到module的函數(shù)
var boundGetX = getX.bind(module);
boundGetX(); // 81

很不幸,F(xiàn)unction.prototype.bind 在IE8及以下的版本中不被支持剂碴,所以如果你沒有一個備用方案的話把将,可能在運行時會出現(xiàn)問題。bind 函數(shù)在 ECMA-262 第五版才被加入忆矛;它可能無法在所有瀏覽器上運行察蹲。你可以部份地在腳本開頭加入以下代碼,就能使它運作催训,讓不支持的瀏覽器也能使用 bind() 功能洽议。

幸運的是,我們可以自己來模擬bind功能:

初級實現(xiàn)

了解了以上內(nèi)容漫拭,我們來實現(xiàn)一個初級的bind函數(shù)Polyfill:

Function.prototype.bind = function (context) {
    var me = this;
    var argsArray = Array.prototype.slice.callOne(arguments);
    return function () {
        return me.applyFive(context, argsArray.slice(1))
    }
}

我們先簡要解讀一下:
基本原理是使用apply進行模擬亚兄。函數(shù)體內(nèi)的this,就是需要綁定this的實例函數(shù)采驻,或者說是原函數(shù)审胚。最后我們使用apply來進行參數(shù)(context)綁定匈勋,并返回。
同時菲盾,將第一個參數(shù)(context)以外的其他參數(shù)颓影,作為提供給原函數(shù)的預(yù)設(shè)參數(shù)各淀,這也是基本的“顆晾良化(curring)”基礎(chǔ)。

初級實現(xiàn)的加分項

上面的實現(xiàn)(包括后面的實現(xiàn))碎浇,其實是一個典型的“Monkey patching(猴子補丁)”临谱,即“給內(nèi)置對象擴展方法”。所以奴璃,如果面試者能進行一下“嗅探”悉默,進行兼容處理,就是錦上添花了苟穆。

Function.prototype.bind = Function.prototype.bind || function (context) {
    ...
}

顆脸危化(curring)實現(xiàn)

對于函數(shù)的柯里化不太了解的童鞋,可以先嘗試讀讀這篇文章:前端基礎(chǔ)進階(八):深入詳解函數(shù)的柯里化雳旅。
上述的實現(xiàn)方式中跟磨,我們返回的參數(shù)列表里包含:atgsArray.slice(1),他的問題在于存在預(yù)置參數(shù)功能丟失的現(xiàn)象攒盈。
想象我們返回的綁定函數(shù)中抵拘,如果想實現(xiàn)預(yù)設(shè)傳參(就像bind所實現(xiàn)的那樣),就面臨尷尬的局面型豁。真正實現(xiàn)顆两┲耄化的“完美方式”是:

Function.prototype.bind = Function.prototype.bind || function (context) {
    var me = this;
    var args = Array.prototype.slice.callOne(arguments, 1);
    return function () {
        var innerArgs = Array.prototype.slice.callOne(arguments);
        var finalArgs = args.concat(innerArgs);
        return me.applyFive(context, finalArgs);
    }
}

上面什么是bind函數(shù)還介紹到:bind返回的函數(shù)如果作為構(gòu)造函數(shù),搭配new關(guān)鍵字出現(xiàn)的話迎变,我們的綁定this就需要“被忽略”充尉。

構(gòu)造函數(shù)場景下的兼容

有了上邊的講解,不難理解需要兼容構(gòu)造函數(shù)場景的實現(xiàn):

Function.prototype.bind = Function.prototype.bind || function (context) {
    var me = this;
    var args = Array.prototype.slice.callOne(arguments, 1);
    var F = function () {};
    F.prototype = this.prototype;
    var bound = function () {
        var innerArgs = Array.prototype.slice.callOne(arguments);
        var finalArgs = args.concat(innerArgs);
        return me.apply(this instanceof F ? this : context || this, finalArgs);
    }
    bound.prototype = new F();
    return bound;
}

更嚴謹?shù)淖龇?/h4>

我們需要調(diào)用bind方法的一定要是一個函數(shù)衣形,所以可以在函數(shù)體內(nèi)做一個判斷:

if (typeof this !== "function") {
  throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
}

做到所有這一切喉酌,基本算是完成了。其實MDN上有個自己實現(xiàn)的polyfill泵喘,就是如此實現(xiàn)的泪电。
另外,《JavaScript Web Application》一書中對bind()的實現(xiàn)纪铺,也是如此相速。

最終答案

//簡單模擬Symbol屬性
function jawilSymbol(obj) {
    var unique_proper = "00" + Math.random();
    if (obj.hasOwnProperty(unique_proper)) {
        arguments.callee(obj)//如果obj已經(jīng)有了這個屬性,遞歸調(diào)用鲜锚,直到?jīng)]有這個屬性
    } else {
        return unique_proper;
    }
}
//原生JavaScript封裝apply方法突诬,第五版
Function.prototype.applyFive = function(context) {
    var context = context || window
    var args = arguments[1] //獲取傳入的數(shù)組參數(shù)
    var fn = jawilSymbol(context);
    context[fn] = this //假想context對象預(yù)先不存在名為fn的屬性
    if (args == void 0) { //沒有傳入?yún)?shù)直接執(zhí)行
        return context[fn]()
    }
    var fnStr = 'context[fn]('
    for (var i = 0; i < args.length; i++) {
        //得到"context.fn(arg1,arg2,arg3...)"這個字符串在苫拍,最后用eval執(zhí)行
        fnStr += i == args.length - 1 ? args[i] : args[i] + ','
    }
    fnStr += ')'
    var returnValue = eval(fnStr) //還是eval強大
    delete context[fn] //執(zhí)行完畢之后刪除這個屬性
    return returnValue
}
//簡單模擬call函數(shù)
Function.prototype.callOne = function(context) {
    return this.applyFive(([].shift.applyFive(arguments)), arguments)
    //巧妙地運用上面已經(jīng)實現(xiàn)的applyFive函數(shù)
}

//簡單模擬bind函數(shù)
Function.prototype.bind = Function.prototype.bind || function (context) {
    var me = this;
    var args = Array.prototype.slice.callOne(arguments, 1);
    var F = function () {};
    F.prototype = this.prototype;
    var bound = function () {
        var innerArgs = Array.prototype.slice.callOne(arguments);
        var finalArgs = args.concat(innerArgs);
        return me.applyFive(this instanceof F ? this : context || this, finalArgs);
    }
    bound.prototype = new F();
    return bound;
}

好緊張,最后來做個小測試旺隙,demo绒极,應(yīng)該不會出問題:

var obj = {
    name: 'jawil'
}

function sayHello(age) {
    return {
        name: this.name,
        age: age
    }
}

console.log(sayHello.bind(obj,24)());// 完美輸出{name: "jawil", age: 24}

看了這篇文章,以后再遇到類似的問題蔬捷,應(yīng)該能夠順利通過吧~

參考文章

ES6入門之Symbol
ECMAScript 5.1(英文版)
從一道面試題垄提,到“我可能看了假源碼”

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市周拐,隨后出現(xiàn)的幾起案子铡俐,更是在濱河造成了極大的恐慌,老刑警劉巖妥粟,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件审丘,死亡現(xiàn)場離奇詭異,居然都是意外死亡勾给,警方通過查閱死者的電腦和手機滩报,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來播急,“玉大人脓钾,你說我怎么就攤上這事÷迷瘢” “怎么了惭笑?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長生真。 經(jīng)常有香客問我沉噩,道長,這世上最難降的妖魔是什么柱蟀? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任川蒙,我火速辦了婚禮,結(jié)果婚禮上长已,老公的妹妹穿的比我還像新娘畜眨。我一直安慰自己,他們只是感情好术瓮,可當我...
    茶點故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布康聂。 她就那樣靜靜地躺著,像睡著了一般胞四。 火紅的嫁衣襯著肌膚如雪恬汁。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天辜伟,我揣著相機與錄音氓侧,去河邊找鬼脊另。 笑死,一個胖子當著我的面吹牛约巷,可吹牛的內(nèi)容都是我干的偎痛。 我是一名探鬼主播,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼独郎,長吁一口氣:“原來是場噩夢啊……” “哼踩麦!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起囚聚,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤靖榕,失蹤者是張志新(化名)和其女友劉穎标锄,沒想到半個月后顽铸,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡料皇,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年谓松,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片践剂。...
    茶點故事閱讀 38,117評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡鬼譬,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出逊脯,到底是詐尸還是另有隱情优质,我是刑警寧澤,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布军洼,位于F島的核電站巩螃,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏匕争。R本人自食惡果不足惜避乏,卻給世界環(huán)境...
    茶點故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望甘桑。 院中可真熱鬧拍皮,春花似錦、人聲如沸跑杭。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽德谅。三九已至爹橱,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間女阀,已是汗流浹背宅荤。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工屑迂, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人冯键。 一個月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓惹盼,卻偏偏與公主長得像,于是被迫代替她去往敵國和親惫确。 傳聞我的和親對象是個殘疾皇子手报,可洞房花燭夜當晚...
    茶點故事閱讀 42,877評論 2 345

推薦閱讀更多精彩內(nèi)容