- A+
引子
读完《你不知道的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的硬绑定
写在最后
我只是一个正在学习前端的小白,有不对的地方请各位多多指正
如果感觉对您有启发或者帮助,也烦请您留言或给我个关注, 谢谢啦!