Where is JavaScript?

JavaScript出坑之模拟实现ES5的call、apply和bind方法

`call`, `apply` 和 `bind` 是 ES5 标准里提出的函数原型方法,他们的主要功能是用于切换 this 的指向。本文探讨如何利用 ES5 之前的相关 feature ,模拟原生的函数方法。

初识

call, applybind 的规范定义参考 ES5 规范文档中sec-15.3.4.3sec-15.3.4.4sec-15.3.4.5

个人在之前的文章中JavaScript入坑之apply,call和bind也做了介绍

模拟call

this 指向方法的第一个参数(对象),可以转化为给参数(对象)设置等价方法,执行该方法,然后再删除

              
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
Function.prototype.callLike = Function.prototype.callLike || function(context) { // 1 - 在这里this指向方法本身, 方法赋给对象 context.fn = this // 2 - 执行方法 context.fn() // 3 - 删除对象属性 delete context.fn }

以上基本思路呢,有几个问题

  1. 参数传递问题

通过 arguments 拿到的类数组可以转为数组,但在 ES5 以前的框架范围内并无方法做这种逆向转换。

              
  • 1
  • 2
  • 3
  • 4
  • 5
... // 2 - 执行方法 context.fn(arguments)? context.fn(arguments.join(','))? ...

这样写都不行的,在 ES5 之前,这种情况的解决方案是利用 eval 方法,构建一个可供执行的字符串

eval() 函数可计算某个字符串,并执行其中的的 JavaScript 代码。该方法只接受原始字符串作为参数,如果 string 参数不是原始字符串,那么该方法将不作任何改变地返回。因此请不要为 eval() 函数传递 String 对象来作为参数。

              
  • 1
  • 2
  • 3
  • 4
... // 2 - 执行方法,假设 args 为['arguments[0]', 'arguments[1]', 'arguments[2]'...] eval('context.fn(' + args +')') ...
  1. 传入 context 为 null

在非严格模式下,call 可以接收 null 作为首参数,此时 this 指向全局对象

              
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
var a = 1 function bar() { console.log(this.a) } bar.call(null)

因此,在模拟函数里要对传入首参数做处理

              
  • 1
var context = context || window
  1. 函数返回值

call, apply 只做 this 绑定,返回值也需要 return 出去

最终,

              
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
Function.prototype.callLike = Function.prototype.callLike || function(context) { var context = context || window // 1 - 在这里this指向方法本身, 方法赋给对象 context.fn = this var args = [] for(var i = 1, len = arguments.length, i < len; i++) { args.push('arguments[' + i + ']') } // 2 - 执行方法 var ret = eval('context.fn(' + args +')') // 3 - 删除对象属性 delete context.fn return ret }

模拟apply

apply 的实现机制与 call 类似,区别处理参数即可

              
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
Function.prototype.applyLike = Function.prototype.applyLike || function(context, arr) { var context = context || window // 1 - 在这里this指向方法本身, 方法赋给对象 context.fn = this // 需要判断 apply 传入的数组参数 if (!arr) { ret = context.fn() } else { var args = [] for(var i = 0, len = arr.length, i < len; i++) { args.push('arguments[' + i + ']') } ret = eval('context.fn(' + args +')') } // 3 - 删除对象属性 delete context.fn return ret }

一些优化和 ES6 trick

  1. 属性重名

细心的同学们可能已经发现了,如果对象已经存在 fn 属性,那对原对象存在破坏,这并非模拟方法的初衷。可以采用构建 random key 的方法,假如为randomKey(),

              
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
... var fn = randomKey(); // 1 - 在这里this指向方法本身, 方法赋给对象 context[fn] = this ... ... // 3 - 删除对象属性 delete context[fn] ...

或者在更洋气的 ES6 中,可以引用数据类型 Symbol,表示独一无二的值

              
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
... // var fn = Symbol() var fn = Symbol('foo') // 1 - 在这里this指向方法本身, 方法赋给对象 context[fn] = this ...
  1. 引用ES6
    ES6 版本的 callapply 就有多种实现形式了, 这里给出其中之一
              
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
Function.prototype.callLike = Function.prototype.callLike || function(context, ...args) { var context = context || window var fn = Symbol() context[fn] = this // 利用...构造参数类数组 let result = context[fn](...args) delete context[fn] return result }

模拟bind

bind() 方法会创建一个新函数。当这个新函数被调用时,bind() 的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数。

bind 函数并非像 callapply 一样直接绑定 this 后执行函数,而是返回一个函数,同时也支持参数传递。

利用上面完成的 callLikeapplyLike 方法,结合对闭包的理解,不难写出以下基本实现

              
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
Function.prototype.bindLike = Function.prototype.bindLike || function(context, ...args) { var self = this // 获取 2-tail 的参数组成参数数组 var args = Array.prototype.slice.call(arguments, 1) return function () { // 获取 bind 返回函数的执行参数 var restArgs = Array.prototype.slice.call(arguments) return self.applyLike(context, args.concat(restArgs)) } }

以上这段代码会不会有问题,的确是会有问题

  1. bind 返回的函数是可以作为构造函数调用,此时 this 绑定的机制失效,但参数依然存在
              
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
var foo = { name: 'foo' } function bar (age, sex) { console.log(age) console.log(sex) } bar.prototype.sayHello = function () { console.log('hello') } var bindFoo = bar.bind(foo, 'littlewin') var newObj = new bindFoo('male') console.log(newObj.name) // undifined, 已经丢失了到 foo 对象的 this 绑定

这种行为就像把原函数当成构造器。提供的 this 值被忽略,同时调用时的参数被提供给模拟函数。
改写模拟函数如下

              
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
Function.prototype.bindLike = Function.prototype.bindLike || function(context) { var self = this // 获取 2-tail 的参数组成参数数组 var args = Array.prototype.slice.call(arguments, 1) var func = function () { // 获取 bind 返回函数的执行参数 var restArgs = Array.prototype.slice.call(arguments) // 当作为构造函数时,this 指向实例,此时结果为 true,将绑定函数的 this 指向该实例,可以让实例获得来自绑定函数的值 // 当作为普通函数时,this 指向 window,此时结果为 false,将绑定函数的 this 指向 context return self.applyLike(this instanceof func ? this : context, args.concat(restArgs)) } // 将返回函数绑定在构造原型的原型链上 // func.prototype = this.prototype // 上面这句的缺点是当修改 func.prototype 时,同样会改动到 this.prototype function protoFunc () {} protoFunc.prototype = this.prototype func.prototype = new protoFunc() return func }
  1. 调用 bind 的必须是函数
              
  • 1
  • 2
  • 3
  • 4
  • 5
... if (typeof this !== "function") { throw new Error("调用bind的必须是函数") } ...

以上

本文于 2017-1-4  发布在  编程  分类下, 当前已被围观 587 次

并被添加「 JavaScript 前端 」标签

本站使用「 署名 4.0 国际」创作共享协议