引子
讀完《你不知道的JavaScript--上卷》中關於this的介紹和深入的章節后,對於this的指向我用這篇文章簡單總結了一下。接着我就想着能不能利用this的相關知識,模擬實現一下javascript中比較常用到的call、apply、bind方法呢?
於是就有了本文,廢話不多說全文開始!
隱式丟失
由於模擬實現中有運用到隱式丟失, 所以在這還是先介紹一下。
隱式丟失是一種常見的this綁定問題, 是指: 被隱式綁定的函數會丟失掉綁定的對象, 而最終應用到默認綁定。說人話就是: 本來屬於隱式綁定(obj.xxx
this指向obj)的情況最終卻應用默認綁定(this指向全局對象)。
常見的隱式丟失情況1: 引用傳遞
var a = 'window'
function foo() {
console.log(this.a)
}
var obj = {
a: 'obj',
foo: foo
}
obj.foo() // 'obj' 此時 this => obj
var lose = obj.foo
lose() // 'window' 此時 this => window
常見的隱式丟失情況2: 作為回調函數被傳入
var a = 'window'
function foo() {
console.log(this.a)
}
var obj = {
a: 'obj',
foo: foo
}
function lose(callback) {
callback()
}
lose(obj.foo) // 'window' 此時 this => window
// ================ 分割線 ===============
var t = 'window'
function bar() {
console.log(this.t)
}
setTimeout(bar, 1000) // 'window'
對於這個我總結的認為(不知對錯): 在排除顯式綁定后, 無論怎樣做值傳遞,只要最后是被不帶任何修飾的調用, 那么就會應用到默認綁定
進一步的得到整個實現的關鍵原理: 無論怎么做值傳遞, 最終調用的方式決定了this的指向
硬綁定
直觀的描述硬綁定就是: 一旦給一個函數顯式的指定完this之后無論以后怎么調用它, 它的this的指向將不會再被改變
硬綁定的實現解決了隱式丟失帶來的問題, bind函數的實現利用就是硬綁定的原理
// 解決隱式丟失
var a = 'window'
function foo() {
console.log(this.a)
}
var obj = {
a: 'obj',
foo: foo
}
function lose(callback) {
callback()
}
lose(obj.foo) // 'window'
var fixTheProblem = obj.foo.bind(obj)
lose(fixTheProblem) // 'obj'
實現及原理分析
模擬實現call
// 模擬實現call
Function.prototype._call = function ($this, ...parms) { // ...parms此時是rest運算符, 用於接收所有傳入的實參並返回一個含有這些實參的數組
/*
this將會指向調用_call方法的那個函數對象 this一定會是個函數
** 這一步十分關鍵 ** => 然后臨時的將這個對象儲存到我們指定的$this(context)對象中
*/
$this['caller'] = this
//$this['caller'](...parms)
// 這種寫法會比上面那種寫法清晰
$this.caller(...parms) // ...parms此時是spread運算符, 用於將數組中的元素解構出來給caller函數傳入實參
/*
為了更清楚, 采用下面更明確的寫法而不是注釋掉的
1. $this.caller是我們要改變this指向的原函數
2. 但是由於它現在是$this.caller調用, 應用的是隱式綁定的規則
3. 所以this成功指向$this
*/
delete $this['caller'] // 這是一個臨時屬性不能破壞人為綁定對象的原有結構, 所以用完之后需要刪掉
}
模擬實現apply
// 模擬實現apply ** 與_call的實現幾乎一致, 主要差別只在傳參的方法/類型上 **
Function.prototype._apply = function ($this, parmsArr) { // 根據原版apply 第二個參數傳入的是一個數組
$this['caller'] = this
$this['caller'](...parmsArr) // ...parmsArr此時是spread運算符, 用於將數組中的元素解構出來給caller函數傳入實參
delete $this['caller']
}
既然_call與_apply之前的相似度(耦合度)這么高, 那我們可以進一步對它們(的相同代碼)進行抽離
function interface4CallAndApply(caller, $this, parmsOrParmArr) {
$this['caller'] = caller
$this['caller'](...parmsOrParmArr)
delete $this['caller']
}
Function.prototype._call = function ($this, ...parms) {
var funcCaller = this
interface4CallAndApply(funcCaller, $this, parms)
}
Function.prototype._apply = function ($this, parmsArr) {
var funcCaller = this
interface4CallAndApply(funcCaller, $this, parmsArr)
}
一個我認為能夠較好展示_call 和 _apply實現原理的例子
var myName = 'window'
var obj = {
myName: 'Fitz',
sayName() {
console.log(this.myName)
}
}
var foo = obj.sayName
var bar = {
myName: 'bar',
foo
}
bar.foo()
模擬實現bind
// 使用硬綁定原理模擬實現bind
Function.prototype._bind = function ($this, ...parms) {
$bindCaller = this // 保存調用_bind函數的對象 注意: 該對象是個函數
// 根據原生bind函數的返回值: 是一個函數
return function () { // 用rest運算符替代arguments去收集傳入的實參
return $bindCaller._apply($this, parms)
}
}
一個能夠展現硬綁定原理的例子
function hardBind(fn) {
var caller = this
var parms = [].slice.call(arguments, 1)
return function bound() {
parms = [...parms, ...arguments]
fn.apply(caller, parms) // apply可以接受一個偽數組而不必一定是數組
}
}
var myName = 'window'
function foo() {
console.log(this.myName)
}
var obj = {
myName: 'obj',
foo: foo,
hardBind: hardBind
}
// 正常情況下
foo() // 'window'
obj.foo() // 'obj'
var hb = hardBind(foo)
// 可以看到一旦硬綁定后無論最終怎么調用都不能改變this指向
hb() // 'window'
obj.hb = hb // 給obj添加該方法用於測試
obj.hb() // 'window'
// 在加深一下印象
var hb2 = obj.hardBind(foo)
hb2() // 'obj' // 這里調用this本該指向window
總體實現(純凈版/沒有注釋)
function interface4CallAndApply(caller, $this, parmsOrParmArr) {
$this['caller'] = caller
$this['caller'](...parmsOrParmArr)
delete $this['caller']
}
Function.prototype._call = function ($this, ...parms) {
var funcCaller = this
interface4CallAndApply(funcCaller, $this, parms)
}
Function.prototype._apply = function ($this, parmsArr) {
var funcCaller = this
interface4CallAndApply(funcCaller, $this, parmsArr)
}
Function.prototype._bind = function ($this, ...parms) {
$bindCaller = this
return function () {
return $bindCaller._apply($this, parms)
}
}
// ============ 測試 ===============
var foo = {
name: 'foo',
sayHello: function (a, b) {
console.log(`hello, get the parms => ${a} and ${b}`)
}
}
var bar = {
name: 'bar'
}
foo.sayHello._call(bar, 'Fitz', 'smart')
foo.sayHello._apply(bar, ['Fitz', 'smart'])
var baz = foo.sayHello._bind(bar, 'Fitz', 'smart')
baz()
var testHardBind = foo.sayHello._bind(bar, 'hard', 'bind')
testHardBind._call(Object.create(null)) // hello, get the parms => hard and bind 測試_bind的硬綁定
寫在最后
我只是一個正在學習前端的小白,有不對的地方請各位多多指正
如果感覺對您有啟發或者幫助,也煩請您留言或給我個關注, 謝謝啦!