不用call和apply方法模擬實現ES5的bind方法

本文首發個人我的博客:前端小密圈,評論交流送1024邀請碼,嘿嘿嘿😄。前端

來自朋友去某信用卡管家的作的一道面試題,用原生JavaScript模擬ES5bind方法,不許用callbind方法。java

至於結果嘛。。。那我的固然是沒寫出來,我就本身嘗試研究了一番,其實早就寫了,一直沒有組織好語言發出來。git

額。。。這個題有點刁鑽,這是對JavaScript基本功很好的一個檢測,看你JavaScript掌握的怎麼樣以及平時有沒有去深刻研究一些方法的實現,簡而言之,就是有沒有折騰精神。github

不許用不用callapply方法,這個沒啥好說的,不許用咱們就用原生JavaScript先來模擬一個apply方法,感興趣的童鞋也能夠看看chromev8怎麼實現這個方法的,這裏我只按照本身的思惟實現,在模擬以前咱們先要明白和了解原生callapply方法是什麼。面試

簡單粗暴地來講,callapplybind是用於綁定this指向的。(若是你還不瞭解JS中this的指向問題,以及執行環境上下文的奧祕,這篇文章暫時就不太適合閱讀)。chrome

什麼是call和apply方法

咱們單獨看看ECMAScript規範對apply的定義,看個大概就行:數組

15.3.4.3 Function.prototype.apply (thisArg, argArray) 瀏覽器

順便貼一貼中文版,省得翻譯一下,中文版地址緩存

經過定義簡單說一下call和apply方法,他們就是參數不一樣,做用基本相同。babel

一、每一個函數都包含兩個非繼承而來的方法:apply()和call()。
二、他們的用途相同,都是在特定的做用域中調用函數。
三、接收參數方面不一樣,apply()接收兩個參數,一個是函數運行的做用域(this),另外一個是參數數組。
四、call()方法第一個參數與apply()方法相同,但傳遞給函數的參數必須列舉出來。

知道定義而後,直接看個簡單的demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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以後的輸出:

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

結果都相同。從寫法上咱們就能看出兩者之間的異同。相同之處在於,第一個參數都是要綁定的上下文,後面的參數是要傳遞給調用該方法的函數的。不一樣之處在於,call方法傳遞給調用函數的參數是逐個列出的,而apply則是要寫在數組中。

總結一句話介紹callapply

call()方法在使用一個指定的this值和若干個指定的參數值的前提下調用某個函數或方法。
apply()方法在使用一個指定的this值和參數值必須是數組類型的前提下調用某個函數或方法。

分析call和apply的原理

上面代碼,咱們注意到了兩點:

  1. callapply改變了this的指向,指向到lulin
  2. sayHello函數執行了

這裏默認你們都對this有一個基本的瞭解,知道何時this該指向誰,咱們結合這兩句話來分析這個通用函數:f.apply(o),咱們直接看一本書對其中原理的解讀,具體什麼書,我也不知道,參數咱們先無論,先了解其中的大體原理。

注意紅色框中的部分,f.call(o)其原理就是先經過 o.m = f 將 f做爲o的某個臨時屬性m存儲,而後執行m,執行完畢後將m屬性刪除。

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

1
2
3
4
5
6
// 第一步
lulin.fn = jawil.sayHello
// 第二步
lulin.fn()
// 第三步
delete lulin.fn

上面的說的是原理,可能你看的還有點抽象,下面咱們用代碼模擬實現apply一下。

實現aplly方法

模擬實現第一步

根據這個思路,咱們能夠嘗試着去寫初版的 applyOne 函數:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 初版
Function.prototype.applyOne = function(context) {
// 首先要獲取調用call的函數,用this能夠獲取
context.fn = this;
context.fn();
delete context.fn;
}
 
//簡單寫一個不帶參數的demo
var jawil = {
name: "jawil",
sayHello: function (age) {
console.log(this.name);
}
};
 
var lulin = {
name: "lulin",
};
 
//看看結果:
jawil.sayHello.applyOne(lulin) //lulin

正好能夠打印lulin而不是以前的jawil了,哎,不容易啊!😄

模擬實現第二步

最一開始也講了,apply函數還能給定參數執行函數。舉個例子:

1
2
3
4
5
6
7
8
9
10
11
12
var jawil = {
name: "jawil",
sayHello: function (age) {
console.log(this.name,age);
}
};
 
var lulin = {
name: "lulin",
};
 
jawil.sayHello.apply(lulin,[ 24])//lulin 24

注意:傳入的參數就是一個數組,很簡單,咱們能夠從Arguments對象中取值,Arguments不知道是何物,趕忙補習,此文也不太適合初學者,第二個參數就是數組對象,可是執行的時候要把數組數值傳遞給函數當參數,而後執行,這就須要一點小技巧。

參數問題其實很簡單,咱們先偷個懶,咱們接着要把這個參數數組放到要執行的函數的參數裏面去。

1
2
3
4
5
6
7
Function.prototype.applyTwo = function(context) {
// 首先要獲取調用call的函數,用this能夠獲取
context.fn = this;
var args = arguments[1] //獲取傳入的數組參數
context.fn(args.join( ',');
delete context.fn;
}

很簡單是否是,那你就錯了,數組join方法返回的是啥?

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

Too young,too simple啊,最後是一個 「1,2,3,4」 的字符串,其實就是一個參數,確定不行啦。

也許有人會想到用ES6的一些奇淫方法,不過applyES3的方法,咱們爲了模擬實現一個ES3的方法,要用到ES6的方法,反正面試官也沒說不許這樣。可是咱們此次用eval方法拼成一個函數,相似於這樣:

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

先簡單瞭解一下eval函數吧
定義和用法

eval() 函數可計算某個字符串,並執行其中的的 JavaScript 代碼。

語法:
eval(string)

string必需。要計算的字符串,其中含有要計算的 JavaScript 表達式或要執行的語句。該方法只接受原始字符串做爲參數,若是 string 參數不是原始字符串,那麼該方法將不做任何改變地返回。所以請不要爲 eval() 函數傳遞 String 對象來做爲參數。

簡單來講吧,就是用JavaScript的解析引擎來解析這一堆字符串裏面的內容,這麼說吧,你能夠這麼理解,你把eval當作是<script>標籤。

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

就是至關於這樣

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

第二版代碼大體以下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Function.prototype.applyTwo = function(context) {
var args = arguments[1]; //獲取傳入的數組參數
context.fn = this; //假想context對象預先不存在名爲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執行
eval(fnStr); //仍是eval強大
delete context.fn; //執行完畢以後刪除這個屬性
}
//測試一下
var jawil = {
name: "jawil",
sayHello: function (age) {
console.log(this.name,age);
}
};
 
var lulin = {
name: "lulin",
};
 
jawil.sayHello.applyTwo(lulin,[ 24])//lulin 24

好像就好了是否是,其實這只是最粗糙的版本,能用,可是不完善,完成了大約百分之六七十了。

模擬實現第三步

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

1.this參數能夠傳null或者不傳,當爲null的時候,視爲指向window

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

1
2
3
4
5
6
7
var name = 'jawil';
 
function sayHello() {
console.log(this.name);
}
 
sayHello.apply( null); // 'jawil'

demo2:

1
2
3
4
5
6
7
var name = 'jawil';
 
function sayHello() {
console.log(this.name);
}
 
sayHello.apply(); // 'jawil'

2.函數是能夠有返回值的.

舉個簡單栗子🌰:

1
2
3
4
5
6
7
8
9
10
11
12
var obj = {
name: 'jawil'
}
 
function sayHello(age) {
return {
name: this.name,
age: age
}
}
 
console.log(sayHello.apply(obj,[24]));// {name: "jawil", age: 24}

這些都是小問題,想到了,就很好解決。咱們來看看此時的第三版apply模擬方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//原生JavaScript封裝apply方法,第三版
Function.prototype.applyThree = function(context) {
var context = context || window
var args = arguments[1] //獲取傳入的數組參數
context.fn = this //假想context對象預先不存在名爲fn的屬性
if (args == void 0) { //沒有傳入參數直接執行
return context.fn()
}
var fnStr = 'context.fn('
for (var i = 0; i < args.length; i++) {
//獲得"context.fn(arg1,arg2,arg3...)"這個字符串在,最後用eval執行
fnStr += i == args.length - 1 ? args[i] : args[i] + ','
}
fnStr += ')'
var returnValue = eval(fnStr) //仍是eval強大
delete context.fn //執行完畢以後刪除這個屬性
return returnValue
}

好緊張,再來作個小測試,demo,應該不會出問題:

1
2
3
4
5
6
7
8
9
10
11
12
var obj = {
name: 'jawil'
}
 
function sayHello(age) {
return {
name: this.name,
age: age
}
}
 
console.log(sayHello.applyThree(obj,[24]));// 完美輸出{name: "jawil", age: 24}

完美?perfact?這就行了,不存在的,咱們來看看第四步的實現。

模擬實現第四步

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

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

就是這句話, context.fn = this //假想context對象預先不存在名爲fn的屬性,這就是一開始的隱患,咱們只是假設,可是並不能防止contenx對象一開始就沒有這個屬性,要想作到完美,就要保證這個context.fn中的fn的惟一性。

因而我天然而然的想到了強大的ES6,這玩意仍是好用啊,幸虧早就瞭解並一直在使用ES6,尚未學習過ES6的童鞋趕忙學習一下,沒有壞處的。

從新複習下新知識:
基本數據類型有6種:UndefinedNull布爾值(Boolean)字符串(String)數值(Number)對象(Object)

ES5對象屬性名都是字符串容易形成屬性名的衝突。
舉個栗子🌰:

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

ES6引入了一種新的原始數據類型Symbol,表示獨一無二的值。

注意,Symbol函數前不能使用new命令,不然會報錯。這是由於生成的Symbol是一個原始類型的值,不是對象

Symbol函數能夠接受一個字符串做爲參數,表示對Symbol實例的描述,主要是爲了在控制檯顯示,或者轉爲字符串時,比較容易區分。

1
2
3
4
5
6
7
8
9
10
11
// 沒有參數的狀況
var s1 = Symbol();
var s2 = Symbol();
 
s1 === s2 // false
 
// 有參數的狀況
var s1 = Symbol("foo");
var s2 = Symbol("foo");
 
s1 === s2 // false

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

做爲屬性名的Symbol

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var mySymbol = Symbol();
 
// 第一種寫法
var a = {};
a[mySymbol] = 'Hello!';
 
// 第二種寫法
var a = {
[mySymbol]: 'Hello!'
};
 
// 第三種寫法
var a = {};
Object.defineProperty(a, mySymbol, { value: 'Hello!' });
 
// 以上寫法都獲得一樣結果
a[mySymbol] // "Hello!"

注意,Symbol值做爲對象屬性名時,不能用點運算符。

看看下面這個栗子🌰:

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

Symbol值做爲屬性名時,該屬性仍是公開屬性,不是私有屬性。

這個有點相似於java中的protected屬性(protected和private的區別:在類的外部都是不能夠訪問的,在類內的子類能夠繼承protected不能夠繼承private)

可是這裏的Symbol在類外部也是能夠訪問的,只是不會出如今for...infor...of循環中,也不會被Object.keys()Object.getOwnPropertyNames()返回。但有一個Object.getOwnPropertySymbols方法,能夠獲取指定對象的全部Symbol屬性名。

看看第四版的實現demo,想必你們瞭解上面知識已經猜獲得怎麼寫了,很簡單。
直接加個var fn = Symbol()就好了,,,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//原生JavaScript封裝apply方法,第四版
Function.prototype.applyFour = function(context) {
var context = context || window
var args = arguments[1] //獲取傳入的數組參數
var fn = Symbol()
context[fn] = this //假想context對象預先不存在名爲fn的屬性
if (args == void 0) { //沒有傳入參數直接執行
return context[fn]()
}
var fnStr = 'context[fn]('
for (var i = 0; i < args.length; i++) {
//獲得"context.fn(arg1,arg2,arg3...)"這個字符串在,最後用eval執行
fnStr += i == args.length - 1 ? args[i] : args[i] + ','
}
fnStr += ')'
var returnValue = eval(fnStr) //仍是eval強大
delete context[fn] //執行完畢以後刪除這個屬性
return returnValue
}

模擬實現第五步

呃呃呃額額,慢着,ES3就出現的方法,你用ES6來實現,你好意思麼?你可能會說,無論黑貓白貓,只要能抓住老鼠的貓就是好貓,面試官直說不許用callapply方法可是沒說不許用ES6語法啊。

反正公說公有理婆說婆有理,這裏仍是不用Symbol方法實現一下,咱們知道,ES6其實都是語法糖,ES6能寫的,咋們ES5都能實現,這就致使了babel這類把ES6語法轉化成ES5的代碼了。

至於babelSymbol屬性轉換成啥代碼了,我也沒去看,有興趣的能夠看一下稍微研究一下,這裏我說一下簡單的模擬。

ES5 沒有 Sybmol,屬性名稱只多是一個字符串,若是咱們能作到這個字符串不可預料,那麼就基本達到目標。要達到不可預期,一個隨機數基本上就解決了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//簡單模擬Symbol屬性
function jawilSymbol(obj) {
var unique_proper = "00" + Math.random();
if (obj.hasOwnProperty(unique_proper)) {
arguments.callee(obj)//若是obj已經有了這個屬性,遞歸調用,直到沒有這個屬性
} else {
return unique_proper;
}
}
//原生JavaScript封裝apply方法,第五版
Function.prototype.applyFive = function(context) {
var context = context || window
var args = arguments[1] //獲取傳入的數組參數
var fn = jawilSymbol(context);
context[fn] = this //假想context對象預先不存在名爲fn的屬性
if (args == void 0) { //沒有傳入參數直接執行
return context[fn]()
}
var fnStr = 'context[fn]('
for (var i = 0; i < args.length; i++) {
//獲得"context.fn(arg1,arg2,arg3...)"這個字符串在,最後用eval執行
fnStr += i == args.length - 1 ? args[i] : args[i] + ','
}
fnStr += ')'
var returnValue = eval(fnStr) //仍是eval強大
delete context[fn] //執行完畢以後刪除這個屬性
return returnValue
}

好緊張,再來作個小測試,demo,應該不會出問題:

1
2
3
4
5
6
7
8
9
10
11
12
var obj = {
name: 'jawil'
}
 
function sayHello(age) {
return {
name: this.name,
age: age
}
}
 
console.log(sayHello.applyFive(obj,[24]));// 完美輸出{name: "jawil", age: 24}

到此,咱們完成了apply的模擬實現,給本身一個贊 b( ̄▽ ̄)d

實現Call方法

這個不須要講了吧,道理都同樣,就是參數同樣,這裏我給出我實現的一種方式,看不懂,本身寫一個去。

1
2
3
4
5
//原生JavaScript封裝call方法
Function.prototype.callOne = function(context) {
return this.applyFive(([].shift.applyFive(arguments), arguments)
//巧妙地運用上面已經實現的applyFive函數
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Function.prototype.applyFive = function(context) {//剛纔寫的一大串}
 
Function.prototype.callOne = function(context) {
return this.applyFive(([].shift.applyFive(arguments)), arguments)
//巧妙地運用上面已經實現的applyFive函數
}
 
//測試一下
var obj = {
name: 'jawil'
}
 
function sayHello(age) {
return {
name: this.name,
age: age
}
}
 
console.log(sayHello.callOne(obj,24));// 完美輸出{name: "jawil", age: 24}

實現bind方法

養兵千日,用兵一時。

什麼是bind函數

若是掌握了上面實現apply的方法,我想理解起來模擬實現bind方法也是垂手可得,原理都差很少,咱們仍是來看看bind方法的定義。

咱們仍是簡單的看下ECMAScript規範對bind方法的定義,暫時看不懂沒關係,獲取幾個關鍵信息就行。

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

注意一點,ECMAScript規範提到: Function.prototype.bind 建立的函數對象不包含 prototype 屬性或 [[Code]], [[FormalParameters]], [[Scope]] 內部屬性。

bind() 方法會建立一個新函數,當這個新函數被調用時,它的 this 值是傳遞給 bind() 的第一個參數, 它的參數是 bind() 的其餘參數和其本來的參數,bind返回的綁定函數也能使用new操做符建立對象:這種行爲就像把原函數當成構造器。提供的this值被忽略,同時調用時的參數被提供給模擬函數。。

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

呃呃呃,是否是似曾相識,這不是call方法的語法一個樣子麼,,,但它們是同樣的嗎?

bind方法傳遞給調用函數的參數能夠逐個列出,也能夠寫在數組中。bind方法與call、apply最大的不一樣就是前者返回一個綁定上下文的函數,然後二者是直接執行了函數。因爲這個緣由,上面的代碼也能夠這樣寫:

1
2
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).

用一句話總結bind的用法:該方法建立一個新函數,稱爲綁定函數,綁定函數會以建立它時傳入bind方法的第一個參數做爲this,傳入bind方法的第二個以及之後的參數加上綁定函數運行時自己的參數按照順序做爲原函數的參數來調用原函數。

bind在實際中的應用

實際使用中咱們常常會碰到這樣的問題:

1
2
3
4
5
6
7
8
9
10
11
12
13
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指向是在運行函數時肯定的,而不是定義函數時候肯定的,再由於setTimeout在全局環境下執行,因此this指向setTimeout的上下文:window。關於this指向問題,這裏就不細扯

之前解決這個問題的辦法一般是緩存this,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
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函數中能夠訪問Person的上下文。可是看起來稍微一種蛋蛋的憂傷。

可是如今有一個更好的辦法!您可使用bind。上面的例子中被更新爲:

1
2
3
4
5
6
7
8
9
10
11
12
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() 最簡單的用法是建立一個函數,使這個函數不論怎麼調用都有一樣的 this 值。JavaScript新手常常犯的一個錯誤是將一個方法從對象中拿出來,而後再調用,但願方法中的 this 是原來的對象。(好比在回調中傳入這個方法。)若是不作特殊處理的話,通常會丟失原來的對象。從原來的函數和原來的對象建立一個綁定函數,則能很漂亮地解決這個問題:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
this.x = 9;
var module = {
x: 81,
getX: function() { return this.x; }
};
 
module.getX(); // 81
 
var getX = module.getX;
getX(); // 9, 由於在這個例子中,"this"指向全局對象
 
// 建立一個'this'綁定到module的函數
var boundGetX = getX.bind(module);
boundGetX(); // 81

很不幸,Function.prototype.bind 在IE8及如下的版本中不被支持,因此若是你沒有一個備用方案的話,可能在運行時會出現問題。bind 函數在 ECMA-262 第五版才被加入;它可能沒法在全部瀏覽器上運行。你能夠部份地在腳本開頭加入如下代碼,就能使它運做,讓不支持的瀏覽器也能使用 bind() 功能。

幸運的是,咱們能夠本身來模擬bind功能:

初級實現

瞭解了以上內容,咱們來實現一個初級的bind函數Polyfill:

1
2
3
4
5
6
7
Function.prototype.bind = function (context) {
var me = this;
var argsArray = Array.prototype.slice.call(arguments);
return function () {
return me.apply(context, argsArray.slice(1))
}
}

咱們先簡要解讀一下:
基本原理是使用apply進行模擬。函數體內的this,就是須要綁定this的實例函數,或者說是原函數。最後咱們使用apply來進行參數(context)綁定,並返回。
同時,將第一個參數(context)之外的其餘參數,做爲提供給原函數的預設參數,這也是基本的「顆粒化(curring)」基礎。

初級實現的加分項

上面的實現(包括後面的實現),實際上是一個典型的「Monkey patching(猴子補丁)」,即「給內置對象擴展方法」。因此,若是面試者能進行一下「嗅探」,進行兼容處理,就是錦上添花了。

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

顆粒化(curring)實現

對於函數的柯里化不太瞭解的童鞋,能夠先嚐試讀讀這篇文章:前端基礎進階(八):深刻詳解函數的柯里化
上述的實現方式中,咱們返回的參數列表裏包含:atgsArray.slice(1),他的問題在於存在預置參數功能丟失的現象。
想象咱們返回的綁定函數中,若是想實現預設傳參(就像bind所實現的那樣),就面臨尷尬的局面。真正實現顆粒化的「完美方式」是:

1
2
3
4
5
6
7
8
9
Function.prototype.bind = Function.prototype.bind || function (context) {
var me = this;
var args = Array.prototype.slice.call(arguments, 1);
return function () {
var innerArgs = Array.prototype.slice.call(arguments);
var finalArgs = args.concat(innerArgs);
return me.apply(context, finalArgs);
}
}

上面什麼是bind函數還介紹到:bind返回的函數若是做爲構造函數,搭配new關鍵字出現的話,咱們的綁定this就須要「被忽略」。

構造函數場景下的兼容

有了上邊的講解,不難理解須要兼容構造函數場景的實現:

1
2
3
4
5
6
7
8
9
10
11
12
13
Function.prototype.bind = Function.prototype.bind || function (context) {
var me = this;
var args = Array.prototype.slice.call(arguments, 1);
var F = function () {};
F.prototype = this.prototype;
var bound = function () {
var innerArgs = Array.prototype.slice.call(arguments);
var finalArgs = args.concat(innerArgs);
return me.apply(this instanceof F ? this : context || this, finalArgs);
}
bound.prototype = new F();
return bound;
}

更嚴謹的作法

咱們須要調用bind方法的必定要是一個函數,因此能夠在函數體內作一個判斷:

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

作到全部這一切,基本算是完成了。其實MDN上有個本身實現的polyfill,就是如此實現的。
另外,《JavaScript Web Application》一書中對bind()的實現,也是如此。

最終答案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
//簡單模擬Symbol屬性
function jawilSymbol(obj) {
var unique_proper = "00" + Math.random();
if (obj.hasOwnProperty(unique_proper)) {
arguments.callee(obj)//若是obj已經有了這個屬性,遞歸調用,直到沒有這個屬性
} else {
return unique_proper;
}
}
//原生JavaScript封裝apply方法,第五版
Function.prototype.applyFive = function(context) {
var context = context || window
var args = arguments[1] //獲取傳入的數組參數
var fn = jawilSymbol(context);
context[fn] = this //假想context對象預先不存在名爲fn的屬性
if (args == void 0) { //沒有傳入參數直接執行
return context[fn]()
}
var fnStr = 'context[fn]('
for (var i = 0; i < args.length; i++) {
//獲得"context.fn(arg1,arg2,arg3...)"這個字符串在,最後用eval執行
fnStr += i == args.length - 1 ? args[i] : args[i] + ','
}
fnStr += ')'
var returnValue = eval(fnStr) //仍是eval強大
delete context[fn] //執行完畢以後刪除這個屬性
return returnValue
}
//簡單模擬call函數
Function.prototype.callOne = function(context) {
return this.applyFive(([].shift.applyFive(arguments)), arguments)
//巧妙地運用上面已經實現的applyFive函數
}
 
//簡單模擬bind函數
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.call(arguments);
var finalArgs = args.concat(innerArgs);
return me.applyFive(this instanceof F ? this : context || this, finalArgs);
}
bound.prototype = new F();
return bound;
}

好緊張,最後來作個小測試,demo,應該不會出問題:

1
2
3
4
5
6
7
8
9
10
11
12
var obj = {
name: 'jawil'
}
 
function sayHello(age) {
return {
name: this.name,
age: age
}
}
 
console.log(sayHello.bind(obj,24)());// 完美輸出{name: "jawil", age: 24}

看了這篇文章,之後再遇到相似的問題,應該可以順利經過吧~

相關文章
相關標籤/搜索