最近一直忙于实习以及毕业设计的事情,所以上周阅读源码之后本周就一直没有进展。今天在写完开题报告之后又抽空看了一眼Underscore源码,发现上次没有看明白的一个函数忽然就豁然开朗了,于是赶紧写下了这篇笔记。

关于如何绑定函数this指向,一直是JavaScript中的高频话题,面试时考官也喜欢问如何绑定函数this的指向,以及如何试现一个bind函数,今天我们就从Underscore源码来学习如何实现一个bind函数。

预备知识

在学习源码之前,我们最好先了解一下函数中this的指向,我在这个系列之前有写过一篇文章,比较完善的总结了一下JavaScript函数中this的指向问题,详情参见:博客园

另外,在学习_.bind函数之前,我们需要先了解一下Underscore中的重要工具函数——restArgs。就在我的上一篇文章中就有介绍到:理解Underscore中的restArgs函数

工具函数——executeBound

在学习_.bind函数之前,我们先来看一下Underscore中的另一个工具函数——executeBound。因为这是一个重要的工具函数,涉及到bind的实现。

executeBound源码(附注释):

// Determines whether to execute a function as a constructor
// or a normal function with the provided arguments.
//执行绑定函数,决定是否把一个函数作为构造函数或者普通函数调用。
var executeBound = function (sourceFunc, boundFunc, context, callingContext, args) {
//如果callingContext不是boundFunc的一个实例,则把sourceFunc作为普通函数调用。
if (!(callingContext instanceof boundFunc)) return sourceFunc.apply(context, args);
//否则把sourceFunc作为构造函数调用。
//baseCreate函数用于构造一个对象,继承指定的原型。
//此处self就是继承了sourceFunc.prototype原型的一个空白对象。
var self = baseCreate(sourceFunc.prototype);
var result = sourceFunc.apply(self, args);
//这里之所以要判断一下是因为如果构造函数有返回值并且返回值是一个对象,那么新构造的对象就会是返回值,而非this所指向的值。
if (_.isObject(result)) return result;
//只有在构造函数没有返回值或者返回值时非对象时,才返回this所指向的值。
return self;
};

  

首先我们先看为什么在executeBound函数结尾需要判断一下result,原因已经写明在注释里,请大家一定仔细注意! 举一个帮助理解的例子:

var A = function() {
this.name = 'A';
return {};
}
var B = function() {
this.name = 'B';
}
var C = function() {
this.name = 'C';
return 'C';
}
var a = new A();
var b = new B();
var c = new C();

  

在浏览器中输出a、b、c,看看你会发现什么?然后再来仔细思考代码中注释的部分吧。

其次回到我们这篇文章的重点,这个函数的功能非常好理解,就是根据实际情况来决定是否把一个函数(sourceFunc)当做构造函数或者普通函数来调用。这个根据的条件就是看callingContext参数是否是boundFunc函数的一个实例。如果callingContext是boundFunc的一个实例,那么就把sourceFunc当做一个构造函数来调用,否则就当做一个普通函数来调用,使用Function.prototype.apply来改变sourceFunc中this的指向。

单独开这个函数可能会使我们变得疑惑,为什么要这么做呢?这个callingContext跟boundFunc是什么关系?为什么要根据这两个参数的关系来决定是否以构造函数的形式调用sourceFunc。

接下来我们根据实际情景来解析这段源码。

在Underscore源码中,使用ctrl + F键查找executeBound字段,共有三处结果。其中一处是上方源码所示的executeBound函数声明。另外两处是调用,其形式都如下所示:

var bound = restArgs(function (callArgs) {
return executeBound(func, bound, context, this, args.concat(callArgs));
});

  

可以注意到实际调用时,第四个参数(callingContext)都是this,代表当前bound函数执行作用域,而第二个参数是bound自身,这样的写法着实奇怪。

其实考虑到我们的目的也就不难理解为什么这么写了,因为当我们把bound函数当做构造函数调用时,构造函数(此时也就是bound函数)内部的this会指向新构造的对象,而这个由bound函数新构造的对象自然就是bound函数的一个实例了,此时就会把sourceFunc当做构造函数调用。

接下来我们再看_.bind函数,一起深入理解该函数的同时,顺便理解一下executeBound函数中为什么要根据callingContext和boundFunc的关系来确定sourceFunc的调用方式。

理解_.bind函数

我们先看_.bind函数的源码(附注释):

// Create a function bound to a given object (assigning `this`, and arguments,
// optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if
// available.
//将指定函数中的this绑定到指定上下文中,并传递一些参数作为默认参数。
//其中args是默认参数,以后调用新的func时无需再次传递这些参数。
_.bind = restArgs(function (func, context, args) {
if (!_.isFunction(func)) throw new TypeError('Bind must be called on a function');
var bound = restArgs(function (callArgs) {
return executeBound(func, bound, context, this, args.concat(callArgs));
});
return bound;
});

  

我们看到在_.bind函数的内部定义了一个bound函数,然后返回了这个函数,即为闭包。闭包的好处即在于内部的函数是私有函数,可以访问外部函数作用域,在内部函数调用之前,整个外部函数的作用域都是存在且对于内部函数而言是可访问的。在restArgs函数的参数(即匿名函数)中并没有处理如何调用func,因为我们要根据情况来决定。当我们使用_.bind函数绑定一个函数的this时,会返回bound函数作为新的func函数,而bound函数会根据其调用的方式,来决定如何调用func,而此处的闭包能够保证在bound执行之前,func是一直存在的。当我们使用new来操作bound函数构造新的对象时,bound内的this指向新构造的对象(即为bound的新实例),executeBound函数内部就会把func当做构造函数来调用;如果以普通函数形式调用bound,那么内部的this会指向外部调用bound函数时的作用域,自然就不是bound的一个实例了,这就是为什么会给executeBound第四个参数传递this的原因。

口说无凭,我们自己写个代码探究一下闭包内部函数中this的指向问题:

var test = function() {
var bound = function() {
this.name = 'bound';
console.log(this);
}
return bound;
}
var Bound = test();
var b = new Bound();
var b = Bound(); //bound { name: 'bound' }
//window

  

大家可以将上面这段代码拷贝到浏览器控制台试一试,看看结果是不是跟上面的注释一样。

实现一个自己的bind函数

通过上面的学习,我们知道了原来bind函数还要考虑到特殊情况——被绑定过this的函数作为构造函数调用时的情况。 接下来我们手动实现一个简单的bind函数:

var _bind = function(func, context) {
var bound = function() {
if(this instanceof bound) {
var obj = new Object();
obj.prototype = func.prototype;
obj.prototype.constructor = func;
var res = func.call(obj);
if(typeof res == 'function' || typeof res == 'object' && !!res)
return res;
else
return obj
}
else {
return func.call(context);
}
};
return bound;
}

  

在阅读这篇文章之前,你会如何实现一个bind函数呢?

更多Underscore源码解读:GitHub

理解Underscore中的_.bind函数的更多相关文章

  1. 理解Underscore中的_.template函数

    Underscore中提供了_.template函数实现模板引擎功能,它可以将JSON数据源中的数据对应的填充到提供的字符串中去,类似于服务端渲染的模板引擎.接下来看一下Underscore是如何实现 ...

  2. 理解Underscore中的节流函数

    上一篇中讲解了Underscore中的去抖函数(_.debounced),这一篇就来介绍节流函数(_.throttled). 经过上一篇文章,我相信很多人都已经了解了去抖和节流的概念.去抖,在一段连续 ...

  3. 深入理解javascript中的立即执行函数(function(){…})()

    投稿:junjie 字体:[增加 减小] 类型:转载 时间:2014-06-12 我要评论 这篇文章主要介绍了深入理解javascript中的立即执行函数,立即执行函数也叫立即调用函数,通常它的写法是 ...

  4. 理解Vue中的Render渲染函数

    理解Vue中的Render渲染函数 VUE一般使用template来创建HTML,然后在有的时候,我们需要使用javascript来创建html,这时候我们需要使用render函数.比如如下我想要实现 ...

  5. 深入理解javascript中的立即执行函数

    这篇文章主要介绍了深入理解javascript中的立即执行函数,立即执行函数也叫立即调用函数,通常它的写法是用(function(){…})()包住业务代码,使用jquery时比较常见,需要的朋友可以 ...

  6. 理解Underscore中的uniq函数

    uniq函数,是Underscore中的一个数组去重函数,给它传递一个数组,它将会返回该数组的去重副本. 1 ES6版本去重 在ES6版本中,引入了一个新的数据结构——set,这是一种类似数组的数据结 ...

  7. 理解Underscore中的restArgs函数

    虽然Underscore并没有在API手册中提及到restArgs函数,我们仍然可以通过_.restArgs接口使用restArgs函数.如果不去阅读源码,我们很难发现Underscore中还有这样的 ...

  8. 理解Underscore中的flatten函数

    最近是在所在实习公司的第一个sprint,有个朋友又请假了,所以任务比较重,一直这么久都没怎么更新了,这个周末赖了个床,纠结了一会儿决定还是继续写这个系列,虽然比较乏味,但是学到的东西还是很多的. 之 ...

  9. 理解JavaScript中的去抖函数

    何为去抖函数?在学习JavaScript去抖函数之前我们需要先弄明白这个概念.很多人都会把去抖跟节流两个概念弄混,但是这两个概念其实是很好理解的. 去抖函数(Debounce Function),是一 ...

随机推荐

  1. JavaScript弹出层

    1.这个弹出层就是一个DIV 2.看需要什么效果 2.1.如果是仅仅需要弹出层,而背后的网页同样可以点击,那么只需要一个DIV即可,效果如图: 2.2.需要一层透明背景,而后面的网页只能看不能点,效果 ...

  2. 九度oj 题目1009:二叉搜索树

    题目1009:二叉搜索树 时间限制:1 秒 内存限制:32 兆 特殊判题:否 提交:5733 解决:2538 题目描述: 判断两序列是否为同一二叉搜索树序列 输入: 开始一个数n,(1<=n&l ...

  3. SpringMVC入门(二)

    使用注解的方式进行Handler的开发   注意:此处只介绍和方式一不同的地方 1.注解的处理器适配器  在spring3.1之前使用org.springframework.web.servlet.m ...

  4. Python基础(1) - 初识Python

    Python 特点: 1)面向对象 2)解释执行 3)跨平台.可移植 4)垃圾回收机制 5)动态数据类型.强类型 6)可扩展.可嵌入 Python可以方便调用C/C++等语言,同时也可以方便的被C/C ...

  5. Linux 线程实现机制分析--转

    http://www.ibm.com/developerworks/cn/linux/kernel/l-thread/ 一.基础知识:线程和进程 按照教科书上的定义,进程是资源管理的最小单位,线程是程 ...

  6. Firebird execute block 批处理

    火鸟的批处理,效率好高,使用简单. execute block as declare variable i ; begin ) do begin :i = :i + ; insert into m_u ...

  7. 如何高效的算出2x8的值

    原文出自:https://blog.csdn.net/seesun2012 位移算法,如何高效的算出2*8的值,为什么8<<1,4<<2,2<<3,1<< ...

  8. Java基础 深拷贝浅拷贝

    Java基础 深拷贝浅拷贝 非基本数据类型 需要new新空间 class Student implements Cloneable{ private int id; private String na ...

  9. Spring课程 Spring入门篇 7-1 Aspect介绍及PointCut注解应用

    本节主要是理论型: 关键看下节实操. 这个其实只要理解一点:使用AspectJ进行Spring AOP 编程,其实就是使用了JAVA注解的风格进行配置和使用. 不像之前讲的那样在配置文件中配置使用.

  10. 第2章、数据与简单计算程序(c语言入门)

    一.写程序描述数据的处理 ①描述数据 ②描述过程 ps:和做鸡蛋饼差不多:数据是材料,作法是过程. 空白符的作用是使用程序更容易了解. 二.标识符 1.基本要求: ①由字母,数字,下划线组成. ②数字 ...