最近一直忙于实习以及毕业设计的事情,所以上周阅读源码之后本周就一直没有进展。今天在写完开题报告之后又抽空看了一眼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. jmeter(6)——集合点与检查点

    集合点 1.概念 集合点:我们所说的并发不会是真正的并发, 集合点可以理解成,所有的用户在进行某一操作时在同一时间点一起执行,比如:抢票或者促销抢购,集合点可以帮助我们使并发更加有效可控 2.位置 位 ...

  2. java代码行数统计工具类

    package com.syl.demo.test; import java.io.*; /** * java代码行数统计工具类 * Created by 孙义朗 on 2017/11/17 0017 ...

  3. 【JSON.parse()和JSON.stringify()】

    var str = '{"name":"huangxiaojian","age":"23"}' 结果: JSON.par ...

  4. WES7 定制界面完整过程(去除所有windows标识)

    转载但有改动 红色字体记录 目的:实验从启动开始到出现桌面,不出现任何windows图标或标识.重大提示:在某些虚拟机上面操作和真实机器是不一样的,主机会容易很多;所以在虚拟机无法实现效果的时候使用主 ...

  5. Swift数据类型_整型和浮点型

    //swift中的整型和浮点型 /** * //类型推断整数是Int 浮点数是Double ,日常使用需要注意不能越界,存储时间毫秒数 英雄经验数等等之类内容容易越界 整型 大多数情况下,你不需要在代 ...

  6. HttpResponse Entity的处理(将字符数组转为JSON)

    1.问题背景 调用高德的IP地址查询接口,获取的返回值为字符串数组(如下); 因为这里只是纯字符串,并不是真正的数组,无法直接取值,所以想到看能不能转为数组或者JSON再进行取值. 2.解决: 通过在 ...

  7. JavaScript之如何对客户端进行检测

    本文主要是针对各种客户端进行检测,使用了用户代理字符串检测技术,具体代码如下: var client=function() { var engine= { // 呈现引擎 ie: 0, gecko: ...

  8. canal —— 阿里巴巴mysql数据库binlog的增量订阅&消费组件

    阿里巴巴mysql数据库binlog的增量订阅&消费组件canal ,转载自  https://github.com/alibaba/canal 最新更新 canal QQ讨论群已经建立,群号 ...

  9. 第八章.Java集合

    Java集合类是一种特别有用的工具类,可用于存储数量不等的对象.Java集合大致可分为Set.List.Queue和Map四种体系 Set代表无序.不可重复的集合 List代表有序.重复的集合 Map ...

  10. Effective C++ .09 不在构造和析构过程中调用virtual函数

    看过C++对象模型的话就可以知道,在构造基类时,完整的vtable没有建立起来(表项没有被相应的子类函数替换),因而无法调用到子类的函数(即构造函数中的virtual函数是本类里的方法,不是virtu ...