1. 程式人生 > >前端面試之手寫一個bind方法

前端面試之手寫一個bind方法

bind 函式對於寫react的人來說並不陌生。哦!是的,沒錯我的朋友,它的一個用處就是用來改變函式this指向的。如果細究一下bind的實現,發現裡面還是有不少東西的,我們今天展開討論一下。

在說bind之前呢,我們還要先來講講我們的老熟人 **this。今天我們再來看看它的四種繫結規則

This的四種繫結規則

1.預設繫結

獨立函式呼叫時, this 指向全域性物件,如果使用嚴格模式,那麼全域性物件無法使用預設繫結, this 繫結至 undefined 並拋錯(TypeError: this is undefined)

2.隱式繫結

當函式作為引用屬性被新增到物件中,隱式繫結規則會把函式呼叫中的 this

繫結到這個上下文物件

3.顯示繫結

運用apply call 方法,在呼叫函式時候繫結this,也就是指定呼叫的函式的this值

4.new繫結

就是使用new操作符的時候的this繫結

上述四條規則優先順序由上到下依次遞增。

由於js多樣的繫結規則,帶來了 繫結隱式丟失問題, 即函式中的 this 丟失繫結物件,即它會應用第 1 條的 預設繫結 規則,從而將 this 繫結到全域性物件或者 undefined 上。

例如:繫結至上下文物件的函式被賦值給一個新的函式,然後呼叫這個新的函式時

var obj = {
  a: 2,
 foo: function () {
   console.log(this.a)
  }
}
var a = 2
setTimeout(obj.foo, 0) // 2

還記得我們當年我們是怎麼做的嗎?

...
var me = this;
return function () {
  me.xxx()
}
...

還有就是用call 或者apply來顯示的繫結:

function foo() {
  console.log( this.a);
  }
var obj = {
  a: 2
  };
var bar = function() {
  foo.call(obj);
  };
bar();  // 2
setTimeout(bar, 100);  // 2

由於這種用法太多了,所以呢ES5的時候給出了一個方法 Function.prototype.bind()

,自從有了它,前端工程師的生活似乎好過了很多,一個bind可以解決很多問題。

先看bind定義

MDN 給出的定義是:

bind() 方法建立一個新的函式, 當這個新函式被呼叫時其this置為提供的值,其引數列表前幾項置為建立時指定的引數序列。

fun.bind(thisArg[, arg1[, arg2[, ...]]])

bind() 函式會建立一個新 繫結函式繫結函式 與被調函式具有相同的函式體(在 ECMAScript 5 中)。呼叫 繫結函式 通常會導致執行 包裝函式 繫結函式 也可以使用new運算子構造:這樣做就好像已經構造了目標函式一樣。提供的 this 值將被忽略,而前置引數將提供給模擬函式

總的來說bind有如下三個功能點:

  1. 改變原函式的 this 指向,即繫結上下文,返回原函式的拷貝
  2. 繫結函式 被呼叫時,bind的額外引數將置於實參之前傳遞給被繫結的方法。
  3. 注意,一個 繫結函式 也能使用 new 操作符建立物件,這種行為就像把原函式當成構造器,thisArg 引數無效。也就是 new 操作符修改 this 指向的優先順序更高。

說了那麼多,我們現在可不可以實現一個bind呢?

其實有時候去研究這些JS API的實現還是蠻好玩的,你能學到很多知識。今天我們就手摸手寫一下吧。

從bind的定義描述中可以看到,我們要寫的這個函式的輸入輸出基本確定了:

  • 輸入:接受一個或者多個引數,第一個是要繫結的上下文,額外引數當作繫結函式的前置引數。
  • 輸出:返回原函式的拷貝,即返回一個函式,這個函式呢具備原函式的功能
// 定義這個方法為myBind
Function.prototype.myBind = function(thisArg) {
  if (typeof this !== 'function') {
    return;
  }
  var _self = this;
  var args = Array.prototype.slice.call(arguments, 1) //從第二個引數擷取
  return function() {
    return _self.apply(thisArg, args.concat(Array.prototype.slice.call(arguments))); // 注意引數的處理
  }
}

我們來測試一下:

function foo(name) {
this.name = name;
}

var obj = {}

//上下文 功能  done
var bar = foo.myBind(obj)
bar('jack')
console.log(obj.name) //'jack'

// 引數 功能   done
var tar = foo.myBind(obj, 'rose');
tar()
console.log(obj.name) //'rose'
// new 功能   error
var alice = new bar('alice')
console.log(obj.name) //alice   obj name should be 'jack'
console.log(alice.name) //undefined, alice name should be 'alice'

可以看到使用 new 例項化被繫結的方法,上下文還指向了傳入的obj,這個方法有點問題,我們需要考慮到的是在myBind的實現裡面,需要檢測new的操作

我們先考慮一下new操作符在呼叫建構函式時做了哪些操作?

比如說 var a = new b()

{}
.__proto__ = b.prototype

所以我們做了如下修改:

Function.prototype.myBind = function(thisArg) {
  if (typeof this !== 'function') {
    return;
  }
  var _self = this;
  var args = Array.prototype.slice.call(arguments, 1)
  var fnBound = function () {
    // 檢測 New
    // 如果當前函式的this指向的是建構函式中的this 則判定為new 操作
    var _this = this instanceof _self ? this : thisArg;
    return _self.apply(_this, args.concat(Array.prototype.slice.call(arguments)));
  }
  // 為了完成 new操作
  // 還需要做一件事情 執行原型 連結 (思考題,為什麼?
  fnBound.prototype = this.prototype;
  return fnBound;
}

測試OK了:

function foo(name) {
this.name = name;
}
var obj = {};
var bar = foo.myBind(obj);
bar('Jack');
console.log(obj.name);  // Jack
var alice = new bar('Alice');
console.log(obj.name);  // Jack
console.log(alice.name);    // Alice

這個用例中我們來討論一下

bar的this指向

  • 變數 bar 是繫結之後的函式,也就是 fnBound_self原函式 foo 的引用
  • 如果直接 bar(‘jack’) 它指向window 或undefined。
  • new bar(‘alice’) (相當於 new foo(‘alice’) )過程中, fnBound 的this指向了new表示式返回的物件alice
  • 如果是 new 呼叫繫結函式,此時繫結函式中的 this 是由 new 呼叫繫結函式返回的例項物件,這個物件的建構函式是 fnBound
  • 當我們忽略掉原型連線那行程式碼時,其原型物件並不等於原函式 self 的原型,所以 this instanceof self ? this : oThis 得到的值還是指定的傳入的物件,而不是 new 返回的物件

所以fnBound.prototype = this.prototype 是有必要的,!!但注意 這個原型賦值是有問題的:

原因我在這裡先不說了留給各位討論啦。

Function.prototype.myBind = function(thisArg) {
  if (typeof this !== 'function') {
    return
  }
  var _self = this
  var args = Array.prototype.slice.call(arguments, 1)
  var fnNop = function () {} // 定義一個空函式
  var fnBound = function () {
    var _this = this instanceof _self ? this : thisArg

    return _self.apply(_this, args.concat(Array.prototype.slice.call(arguments)))
  }
  // 維護原型關係
  if (this.prototype) {
    fnNop.prototype = this.prototype;
  }

  fnBound.prototype = new fnNop();

  return fnBound;
}

這裡我們建立了一個空函式來做中間人,承接原函式的原型丟給繫結函式,好了問題就搞完了,是不是很多知識點慢慢看吧。
如果你覺得不錯,或者發現文章中的錯誤,或者有更好的建議,歡迎評論或進前端全棧群:866109386,來交流討論吹水。