最近由于工作比较忙, 好久都没时间静下心来研究一些东西了. 今天在研究 call 和 apply 的区别的时候, 看到 GitHub 上面的一篇文章, 看完以后, 感觉启发很大.
文章链接为 https://github.com/lin-xin/blog/issues/7 , 有兴趣的童鞋可以前往学习一下.
但是我主要想写的并不是我今天学习了这篇博文, 那样也就太没有技术含量了对吧.
bind 的实现
其实文章并不难理解, 只要是对 JS 有一定程度的了解的同学就能很容易看懂. 我主要关心的是文章最后给出的自己动手实现 bind 函数的代码, 代码如下:
- if (!Function.prototype.bind) {
- Function.prototype.bind = function () {
- var self = this, // 保存原函数
- context = [].shift.call(arguments), // 保存需要绑定的 this 上下文
- args = [].slice.call(arguments); // 剩余的参数转为数组
- return function () {
- // 返回一个新函数
- self.apply(context,[].concat.call(args, [].slice.call(arguments)));
- }
- }
- }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
说实话, 咋看这一段代码, 感觉好像很平淡无奇, 但是你要是细细去体味的话, 简直能够让你回味无穷.
[].shift.call(arguments) 的含义
我相信, 对于很多对 JS 这门语言掌握并不算深的童鞋来说, 这句代码的含义貌似并不怎么容易理解, 我们其实细分一下, 可以拆分出好几个知识点出来.
arguments 的含义
我相信, 很多人应该知道 arguments, 甚至于在实际的学习工作中也经常用到, 但是我还是想在这个地方复习一下.
MDN 网站上对于 Arguments 对象是这么定义的:
arguments 是一个对应于传递给函数的参数的类数组对象.
我们可以看到 arguments 是一个类数组对象, 他怎么用呢? 别急, 我们马上给出示例:
上面是我在 Chrome 浏览器控制台进行的一个小测试, 可以看到, arguments 是一个类数组对象, 它的值中包含了我们在调用函数时候, 朝里面传入的参数.
而我们知道, 对于对象的调用, 我们一般采用点号 -- obj.key 的调用方式或者括号加对象的 key 的方式 -- obj[key] , 这两种方式是完全等同的.
因此我们在创建函数的时候, 其实并不需要朝里面传入变量, 直接用 arguments 的方式来获取变量岂不是更好? 依我个人的拙见, 虽然这种方式接收参数很灵活, 但是也存在弊端, 函数的参数本来就是写起来让自己或者他人阅读起来更舒服的, 君不见 C,Java 中参数还要指定类型呢, JS 已经够宽松了, 再按照这种标准宽松下去, 写起来是很随意, 但是用起来就很蛋疼了.
[].shift
咋一看, 你还不一定看得懂这一句代码, 其实它就等同于 Array.prototype.shift , 不信你打开你的 Chrome 浏览器控制台, 试着输入 Array.prototype.shift === [].shift , 然后你看看是不是能够得到 true. 也就是说, 两者其实是同一个函数, 只是调用的方式存在差异, 一个是通过原型的方式直接在类上调用; 一个是通过实例化, 继承过来, 然后再调用.
如果你对这个话题很感兴趣, 刚好我在知乎上看到有相关话题的讨论: JS 中 [].slice 与 Array.prototype.slice 有什么区别?, 你可以自行前往查看或者讨论.
call
如果还有不知道 call 为何物的同学, 请前往 mdn 网站 call 页面去详细了解.
mdn 对 call 的定义为:
call() 方法调用一个函数, 其具有一个指定的 this 值和分别地提供的参数 (参数的列表).
call 的语法为:
fun.call(thisArg, arg1, arg2, ...)
在了解了上面三个小知识点以后, 我们就能理解了 [].shift.call(arguments) 的含义了. arguments 是类数组对象, 它没有 shift 等数组独有的方法, 怎么办? 现在, 我想要拿出传入的参数中的第一个参数, 怎么操作, 就只有用这种方式了.
我们来试验一下:
- function a(){
- return arguments;
- }
- var temp = a("a","b");
- temp.slice(); //Uncaught TypeError: temp.slice is not a function
- 1
- 2
- 3
- 4
我们可以看到, 如果你想直接对 temp 用 slice() 函数, 不可能的, 做不到的! 因为 arguments 是类数组对象, 并非真正的对象, 并没有 slice 方法.
但是如果你用下面方法:
- [].slice.call(temp); //["a", "b"]
- 1
我们可以看到的是, 用上面的方式, 就神奇般的得到了想要的结果.
我们在类数组对象上面, 成功的运用了数组的 slice 方法. 也就是说, 相当于, 先将 temp 转为数组, 然后再对其运用了 slice 方法, 然后返回了我们想要的结果.
当然, 这种写法, 在 es6 面前, 已经成为了过去式, es6 为数组新增了一个 from 方法, 这个函数的使用方式如下:
- Array.from(temp).slice(); //["a", "b"]
- 1
感兴趣的同学, 可以去 mdn 网站去了解 Array.from() 的详细解释.
因此, 我们可以总结下, 原来 context = [].shift.call(arguments) 这一句就是把参数中的第一个剪切出来, 赋给 context, 那么也就相当于起到了将 参数中的 this 保存的目的.
args = [].slice.call(arguments);
这一句, 将除了 this 上下文的所有参数, 传给了 args , 以备后来使用.
bind 的理解
要读懂返回值, 必须得理解, bind 的作用是什么.
The bind() method creates a new function that, when called, has its this keyword set to the provided value, with a given sequence of arguments preceding any provided when the new function is called.
有兴趣的同学, 可以前往 Function.prototype.bind() 页面查看详细解读. 简单点理解, bind 就是用来绑定上下文的, 强制将函数的执行环境绑定到目标作用域中去. 与 call 和 apply 其实有点类似, 但是不同点在于, 它不会立即执行, 而是返回一个函数. 因此我们要想自己实现一个 bind 函数, 就必须要返回一个函数, 而且这个函数会接收绑定的参数的上下文.
返回函数
- return function () {
- // 返回一个新函数
- self.apply(context,[].concat.call(args, [].slice.call(arguments)));
- }
- 1
- 2
- 3
这一段其实很好理解, 因为 bind 绑定了上下文, 因此 self.apply 的第一个参数, 是之前我们保存的 context. 接下来, 我们将 bind 的其余参数和调用 bind 后返回的函数在执行的过程中接收的参数进行拼接
来源: http://www.bubuko.com/infodetail-3042515.html