大家的閱讀是我發帖的動力,本文首發於我的部落格:deerblog.gu-nami.com/,歡迎大家來玩,轉載請註明出處
喵。
💢前言
bind
是一個改變函式this
指標指向的一個常用函式,經常用在涉及this
指標的程式碼中。來看 MDN 的文件:
Function
例項的bind()
方法建立一個新函式,當呼叫該新函式時,它會呼叫原始函式並將其this
關鍵字設定為給定的值,同時,還可以傳入一系列指定的引數,這些引數會插入到呼叫新函式時傳入的引數的前面。
最近搞出了一個很難注意得到的 bug,bind
函式返回的函式中,原函式的屬性消失了,導致了一個工具函式的失效。
function Message (/*...*/) {/*...*/}
Message.success = function (/*...*/) { return Message('success', /*...*/) }
// ...
xxx.Message = Message.bind(/*...*/)
// ...
xxx.Message.success(/*...*/)
// Uncaught TypeError: xxx.success is not a function
解決方法自然是Object.keys()
遍歷一下原函式的屬性,新增到新的函式上面。
來看看文件怎麼說的:
繫結函式還會繼承目標函式的原型鏈。然而,它不會繼承目標函式的其他自有屬性(例如,如果目標函式是一個類,則不會繼承其靜態屬性)。
所以上面Message
的屬性就消失了。
後來去翻看Funtion.__proto__.bind
的文件發現了一些以前從未注意到的內容,感覺挺有意思...
bind
的功能主要有兩點,一個是修改函式的this
指標。
例如在老版本的 React Class 元件中使用回撥函式:
export default class TestButton extends Component {
testClickHandler () {
console.log(this.state)
}
render () {
return (
<Button onClick={this.testClickHandler.bind(this)}>test</Button>
)
}
}
在setTimeout
等回撥中訪問當前函式的this
指標(當然現在我們都可以用箭頭函式實現):
function test () {
var that = this
setTimeout(function () {
console.log(that.test)
})
}
bind
還可以暫存引數:
const func = (a, b) => a + b
const funcBound = func.bind(null, 1)
funcBound(2) // 3
func(1, 2) // 3
我們還可以用這個特性做到函式柯里化,在複雜的場景中(好像業務開發幾乎用不到的樣子)使得函式呼叫更加靈活:
const curry = (func: Function, ...args: any[]) => {
let resArgsCount = func.length - args.length
let tempFunc = func.bind(null, ...args)
const ans = (...childArgs: any[]) => {
resArgsCount -= args.length
return resArgsCount > 0
? ((tempFunc = tempFunc.bind(null, ...childArgs)), ans)
: tempFunc(...childArgs)
}
return ans
}
const test = (a, b, c) => a + b + c
const testCurry = curry(test, 1)
testCurry(2)
testCurry(3)
// 6
在 ES6 尚未普及的年代,我們並不能直接使用bind
這個新特性,這就需要 polyfill,因此產生了很多相關的技巧(現在即使要相容 IE 也可以直接透過 Bable 相容),在 JS 中模擬實現bind
經典面試題了屬於是...
結合文件,這篇部落格將在 JS 中實現一下bind
的功能。
🍵修改 this 指標和記錄入參
眾所周知,bind
可以修改this
的指向,並且記錄入參:
function testFunc (a, b) {
return [a + b, this]
}
testFunc(1, 2)
// [3, Window]
testFunc.bind({}, 1)(2)
// [3, {…}]
下面就在 JS 中手寫一下:
Function.prototype.deerBind = function(ctx, ...args) {
ctx = ctx || window
const self = this
return function (...argsNext) {
return self.apply(ctx, [...args, ...argsNext])
}
}
testFunc(1, 2)
// [3, Window]
testFunc.deerBind({}, 1)(2)
// [3, {…}]
☕作為建構函式
繫結函式自動適用於與 new 運算子一起使用,以用於構造目標函式建立的新例項。當使用繫結函式是用來構造一個值時,提供的
this
會被忽略。
在 JS 中,當你new
一個物件時:
- 建立一個新物件;
- 建構函式
this
指向這個新物件; - 執行建構函式中的程式碼;
- 返回新物件。
當一個函式被作為建構函式new
的時候,它的this
指向該函式的例項。這裡我們修改一下deerBind
函式的返回:
Function.prototype.deerBind = function(ctx, ...args) {
ctx = ctx || window
const self = this
const funcBound = function (...argsNext) {
return self.apply((this instanceof funcBound ? this : ctx), [...args, ...argsNext])
}
funcBound.prototype = self.prototype
return funcBound
}
function testNew (str) { this.test = 'test ' + str }
new (testNew.bind({}, 'shikinoko nokonoko koshitanntann'))
// testNew {test: 'test shikinoko nokonoko koshitanntann'}
new (testNew.deerBind({}, 'shikinoko nokonoko koshitanntann'))
// testNew {test: 'test shikinoko nokonoko koshitanntann'}
另外,bind
返回的函式的例項和原函式是指向同一個原型的,這裡也滿足了:
const ins1 = new (testNew.deerBind({}, 'test')), ins2 = new testNew('test')
ins1.__proto__
// {constructor: ƒ}
ins1.__proto__ === ins2.__proto__
// true
🧉處理箭頭函式的情況
注意到,箭頭函式沒有例項,也不能new
,this
來自親代作用域,用作建構函式會引起錯誤:
new (() => {})
// Uncaught TypeError: (intermediate value) is not a constructor
new ((() => {}).bind())
// Uncaught TypeError: (intermediate value).bind(...) is not a constructor
const testArrowFunc = (a, b) => {
return [a + b, this]
}
testArrowFunc(1, 2)
// [3, Window]
testArrowFunc.bind({}, 1)(2)
// [3, Window]
再修改一下這裡的實現:
Function.prototype.deerBind = function(ctx, ...args) {
ctx = ctx || window
const self = this
let funcBound
if (self.prototype) {
funcBound = function (...argsNext) {
return self.apply((this instanceof funcBound ? this : ctx), [...args, ...argsNext])
}
funcBound.prototype = self.prototype
} else {
funcBound = (...argsNext) => {
return self.apply(ctx, [...args, ...argsNext])
}
}
return funcBound
}
testArrowFunc.deerBind({}, 1)(2)
// [3, Window]
new ((() => {}).deerBind())
// Uncaught TypeError: (intermediate value).deerBind(...) is not a constructor
🍯處理類構造器的情況
你可能會發現,bind
可以在類的構造器上面使用,但是我們上面自己寫的似乎存在一點小錯誤:
class base { constructor (a, b) { this.test = a + b } }
new base(1, 2)
// base {test: 3}
new (base.bind({}, 1))(2)
// base {test: 3}
const bind = base.deerBind({}, 1)
new bind(2)
// Uncaught TypeError: Class constructor base cannot be invoked without 'new'
這裡透過prototype
上面的constructor
來檢查一個函式是否是構造器:
Function.prototype.deerBind = function(ctx, ...args) {
ctx = ctx || window
const self = this
let funcBound
if (self.prototype) {
funcBound = function (...argsNext) {
return !self.prototype.constructor
? self.apply((this instanceof funcBound ? this : ctx), [...args, ...argsNext])
: new self(...args, ...argsNext)
}
funcBound.prototype = self.prototype
} else {
funcBound = (...argsNext) => {
return self.apply(ctx, [...args, ...argsNext])
}
}
return funcBound
}
const bind = base.deerBind({}, 1)
new bind(2)
// base {test: 3}
我們實現的deerBind
也可以用於建構函式了。
🍹結語
到這裡,bind
大體就是實現完成了,這裡具體涉及了bind
函式改變this
指標,記錄引數以及作為建構函式的功能的實現。去看了一下著名 JS polyfill 庫 core-js 的實現,感覺思路大概差不多的樣子,它作為 polyfill 也考慮了相容性的問題,感覺好厲害的樣子。
參考:
- https://juejin.cn/post/6844903733013250056
- https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Function/bind