博客
关于我
简单模拟实现javascript中的call、apply、bind方法
阅读量:432 次
发布时间:2019-03-06

本文共 4919 字,大约阅读时间需要 16 分钟。

目录

引子

读完《你不知道的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 => objvar lose = obj.foolose()  // '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

// 模拟实现callFunction.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.sayNamevar bar = {    myName: 'bar',    foo}bar.foo()

模拟实现bind

// 使用硬绑定原理模拟实现bindFunction.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的硬绑定

写在最后

我只是一个正在学习前端的小白,有不对的地方请各位多多指正

如果感觉对您有启发或者帮助,也烦请您留言或给我个关注, 谢谢啦!

转载地址:http://mcnuz.baihongyu.com/

你可能感兴趣的文章
NAS、SAN和DAS的区别
查看>>
NAS个人云存储服务器搭建
查看>>
NAS服务器有哪些优势
查看>>
NAT PAT故障排除实战指南:从原理到技巧的深度探索
查看>>
nat 网卡间数据包转发_你是不是从来没有了解过光纤网卡,它跟普通网卡有什么区别?...
查看>>
NAT-DDNS内网穿透技术,快解析DDNS的优势
查看>>
NAT-DDNS内网穿透技术,快解析DDNS的优势
查看>>
NAT-DDNS内网穿透技术,解决动态域名解析难题
查看>>
natapp搭建外网服务器
查看>>
NativePHP:使用PHP构建跨平台桌面应用的新框架
查看>>
nativescript(angular2)——ListView组件
查看>>
NativeWindow_01
查看>>
Native方式运行Fabric(非Docker方式)
查看>>
Nature | 电子学“超构器件”, 从零基础到精通,收藏这篇就够了!
查看>>
Nature和Science同时报道,新疆出土四千年前遗骸完成DNA测序,证实并非移民而是土著...
查看>>
Nature封面:只低一毫米,时间也会变慢!叶军团队首次在毫米尺度验证广义相对论...
查看>>
Nat、端口映射、内网穿透有什么区别?
查看>>
Nat、端口映射、内网穿透有什么区别?
查看>>
nat打洞原理和实现
查看>>
NAT技术
查看>>