Git Product home page Git Product logo

underscore-analysis's Introduction

愿景

一直希望有一个机会可以阅读某库的源码,并写一些自己的想法和思考,最终将目标定在underscore.js,一个纯粹的js工具库,没有dom、bom的参与。 下划线的许多api都是日常工作中会用到的,诸如each,map,bind,isUndefined...等,试想如果深入其中把这些api都整明白了,是不是会有许多收获呢!想想还有些小激动。

阅读源码这几个月

老实说静下心来看完一个库是比较难得一件事,从开始阅读下划线到现在过去几个月,终于把它给整的差不多了,期间也看了网上别人的下划线源码分析(比如@老姚的underscore库源码分析系列就非常的不错),这个过程有心累,有慵懒,有喜悦,有收获,原来坚持去做一件事真的能令人开森,如果你有兴趣,也可以花点时间去阅读下划线,相信你也会有所收获。

文章书写计划

下划线的api组成总体来说分成

  1. object方法类
  2. 集合方法类
  3. array方法类
  4. function方法类
  5. 工具方法类

所以基本上会按照这样的大的类别划分来书写源码分析的文章,中间可能会结合其他的js知识写一些零碎的文章。(路漫漫其修远兮,要加油啊,吼吼吼)

源码预览

全文注释版本

下划线源码

文章列表

underscore-analysis's People

Contributors

qianlongo avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

underscore-analysis's Issues

underscore源码分析系列-1-整体结构

前言

一直希望有一个机会可以阅读某库的源码,并写一些自己的想法和思考,最终将目标定在underscore,一个纯粹的js工具库,没有dom、bom的参与。

underscore.js

整体框架

首先我们来看一下,underscore工具库的整体框架

verison 1.8.3

(function(){
	
  //拿到一个全局对象,browser中是window对象,server中是exports
  var root = this;
	
  // 缓存一些原生js的原型以及es5的方法
	
  // ...
	
  //underscore工具库的构造函数
  var _ = function(obj){
    if(obj instanceof _) return obj;
    if(!(this instanceof _)) return new _(obj);
    this._wrapped = obj;
  };
	
  //将构造函数暴露给全局
  if(typeof exports !== 'undefined'){
    if(typeof module !== 'undefined' && module.exports){
      exports = module.exports = _;
    }
    exports._=_;
  } else {
    root._ = _;
  }
	
  // 版本
	
  _.VERSION = '1.8.3';
	
  /*underscore中一些内部变量和函数*/
	
  //...
	
  /*提供给外部调用的静态api*/
	
  _.each = function(){
    //TODO
  };
  _.first = function(){
    //TODO
  };
  _.bind = function(){
   //TODO
  };
  _.keys = function(){
    //TODO
  };
  _.noConfict = function(){
    //TODO
  };
  _.chain = function(){
    //TODO
  };
  //...
	
  //一些原型上面的方法
	
  _.prototype.value = function(){
    //TODO
  };
  //...
	
  //支持amd
  if (typeof define === 'function' && define.amd) {	define('underscore', [], function() {
     return _;
   });
  }
}).call(this);

大体框架如上所述

接下来我们看一下underscore库刚开始设置哪些后面要用到的常用的变量

  var root = this;

  // 保存引进underscore库之前可能存在于全局对象下面的_属性值,主要给noConflict静态函数做去冲突用
  var previousUnderscore = root._;

  // 保存一些长江的构造函数的原型,数组,对象,函数等
  var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype;

  // 保存一些数组的常见方法
  var
    push             = ArrayProto.push,
    slice            = ArrayProto.slice,
    toString         = ObjProto.toString,
    hasOwnProperty   = ObjProto.hasOwnProperty;

  // 保存es5的一些方法
  
  var
    nativeIsArray      = Array.isArray,
    nativeKeys         = Object.keys,
    nativeBind         = FuncProto.bind,
    nativeCreate       = Object.create;

underscore构造函数

最后我们来看一下,构造函数本身。

var _ = function(obj) {
  if (obj instanceof _) return obj; // 如果传进来的obj是_的实例,那么直接将obj返回
  if (!(this instanceof _)) return new _(obj); // 如果不是_的实例,那么内部直接new 一个 _的实例,然后返回
    this._wrapped = obj; // 给_的实例挂一个属性_wrapped,并赋值为obj
  };

underscore构造函数内部这样处理,可以保证不管外部是否适用new的形式调用,都会拿到一个underscore实例

结尾

underscore.js第一篇就先到这里,了解了其整体的结构,接下来我们开始分析一些比较常用的函数。

underscore源码分析系列-3-对象方法简分析

前言

在前一篇文章中,我们主要说了underscore中几个很常见也很简单的api,在这篇博客中,我们希望说一下对象方面的api

underscore源码分析系列-3

_.isNull

判断一个对象是不是null,如果事就返回true。

_.isNull = function (obj) {
  return obj === null;	
}

注意判断用的事三等,也就是当你传入undefined 的时候,返回的事false。

_.isUndefined

判断传入的值是否事undefined

_.isUndefined = function (obj) {
  return obj === void 0;
}

判断条件中用的是 void,其作用的起到计算表达式的作用,计算结束之后永远返回undefined,为什么这里要用到void这个特性呢? 本质上还是因为低版本的ie中undefined是可以被修改的。

_.isNaN

如果object 是 "不是一个数字",则返回true。

注意: 这和原生的isNaN 函数不一样,如果变量是undefined,原生的isNaN 函数也会返回 true 。

_.isNaN = function (obj) {
  return _.isNumber(obj) && obj !== +obj;
}

首先判断传入的值是否是Number类型,接着判断该值是否等于本身。重点看后面的判断条件 +obj,这句话可以将字符串的数字转化成真正的数字,便排除了isNaN这种情况。

_.isObject

如果object是一个对象,返回true。需要注意的是JavaScript数组和函数是对象,字符串和数字不是。

_.isObject = function (obj) {
  var type = typeof obj;
  return type === 'function' || type === 'object' && !!obj;	
}

js的集中数据类型中包括常见的{},其实函数也看作是一种对象类型。当然null也称作是空对象。所以该api,将类型是function或者object(出了null)都返回true。

_.isElement

如果object是一个DOM元素,返回true。

_.isElement = function (obj) {
  return !!(obj && obj.nodeType === 1);	
}

通过传入对象的nodeType属性来判断一个对象是否是DOM元素,其实这样并不严谨。

var obj = {
  name: 'qianlong',
  nodeType: 1
};

console.log(_.isElement(obj)) => true;

可以看到只要对象有一个属性是nodeType并且是1,那么_.isElement就会认为这是一个DOM元素。

_.has(object, key)

判断object对象是否含有key属性。 有则返回true,否则返回false

var ObjProto = Object.prototype;
var hasOwnProperty = ObjProto.hasOwnProperty;

_.has = function (obj, has) {
  return obj != null && hasOwnProperty.call(obj, key);
}

注意这里通过hasOwnProperty判断,也就是原形是上面的属性,不会去判断。

var Person = function(name){
  this.name = name;
};

Person.prototype.sex = 'boy';

var p1 = new Person('qianlong');

_.has(p1, 'name'); // true
_.has(p1, 'sex'); // false

_.isArray

判断传入的值是否是数组,如果是就返回true

判断一个变量是否是数组,想必大家比较熟悉了,我们看下_中是怎么实现的

var ObjProto = Object.prototype;
var toString = ObjProto.toString;
var nativeIsArray = Array.isArray;

_.isArray = nativeIsArray || function (obj) {
  return toString.call(obj) === '[object Array]';
}

首先在源码的开头保存了nativeIsArray,如果在支持该函数的浏览器中就用这个,如果在一些版本较低的浏览器中就用自己实现的。很显然其他的数据类型也可以用类似的方法来实现。

_.isArguments, .isFunction ,.isString, _.isNumber, _.isDate, _.isRegExp, _.isError

_.each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp', 'Error'], function(name) {
    _['is' + name] = function(obj) {
      return toString.call(obj) === '[object ' + name + ']';
    };
  });

这里用到了其自身的工具方法_.each来对一个数组进行遍历,最后导出多个相应的判断数据类型的方法。

underscore源码分析系列-2-常见简单函数分析

前言

这一篇源码分析中,我们会试着去看underscore几个比较常见的函数

_.random

返回一个min 和 max之间的随机整数。如果你只传递一个参数,那么将返回0和这个参数之间的整数。取值范围是[min, max],注意是闭区间

_.random = function (min, max) {
  if (max == null) {
    max = min;
    min = 0;
  }	
  return min + Math.floor(Math.random() * (max - min + 1))
}

这个函数参数可以传进去两个或者一个,当为一个的时候,返回的是[0, max] 之间的一个整数,平实需要得到一个指定范围的数字的时候,比较常用。

_.noConflict

放弃Underscore 的控制变量"_"。返回Underscore 对象的引用。也就是为了防止冲突而做的处理。

// 注意在源码一开头有这句话
var previousUnderscore = root._;

_.noConflict = function() {
  root._ = previousUnderscore;
  return this;	
}

代码非常简单,就是把原来的全局变量_给赋值回去,而将underscore的引用作为函数调用后的返回值,提供给调用方使用。这是一种非常经典的防止全局冲突的方法。

举个例子我们要写一个平实自己经常要用到的工具类,取的名字是$,当然假设之前,我们并不知道jQuery中也用了这个名字,那么在实际项目中为了防止自己写的这个工具库把全局的美元符号给污染掉,怎么办呢?

<script src="//cdn.bootcss.com/jquery/3.0.0/jquery.js"></script>

<script>
 (function(win){
  var prev$ = win.$,
      $ = function () {
      
      };
      $.isUndefined = function (obj) {
        return obj === void(0);		
      }
      $.random = function (min, max) {
        if (this.isUndefined(max)) {
          max = min;
          min = 0;
        }  
        return min + Math.floor(Math.random() * (max - min + 1))
      };
      $.showMessage = function (msg) {
        alert(msg)
      };
      $.noConflict = function () {
        win.$ = prev$;
        return this;
      }
      win.$ = $;
})(window);


	$.showMessage('hello world'); // 弹出hello world
		
//自己写的$把jQuery中的$给覆盖掉了
  
    var my$ = $.noConflict(); // 不执行这句 下面的$() 调用会没有反应
    
    $(function(){
      alert('hello world two'); // 弹出hello world two
    })
</script>	

_.noop

返回undefined,不论传递给它的是什么参数。 可以用作默认可选的回调参数。

_.noop = function(){};

underscore帮我们提供一个空函数,这个有啥用呢? 其实还真有用处,平实如果我们要初始化某个变量为空函数,方便后面再把这个变量对应的值替换掉。又或者甚至用作判断一个变量是否是undefined。

// 1

if (a == _.noop()) {
  alert('a 是 undefined')
}

// 2

var obj = {
  showMsg: function () {},
  showAge: function () {}
}

var obj = {
  showMsg: _.noop,
  showAge: _.noop
}

很显然这样可以有两个好处,

  1. 因为在ie低版本中undefined的值可能被改写,所以导致判断不准确。但是如果直接于_.noop()相比较就不会有这个问题
  2. 直接给浏览器省了创建两个空函数的麻烦

_.now

一个优化的方式来获得一个当前时间的整数时间戳。可用于实现定时/动画功能。

_.now = Date.now || function () {
  return new Date().getTime();
}

如果当前的浏览器提供的Date对象中含有now函数就用直接提供的,否则就用自己实现的。平常如果需要生成唯一的id,可以用到这个函数。

_.identity

返回与传入参数相等的值,这个函数看似无用, 但是在Underscore里被用作默认的迭代器iterator.

_.identity = function (value) {
  return value;	
}

函数非常简单就是你传进来什么东西就返还给你什么东西。

_.constant

创建一个函数,这个函数返回传入的值 。

_.constant = function (value) {
  return function () {
    return value;
  }	
}	

_.uniqueId

为需要的客户端模型或DOM元素生成一个全局唯一的id。如果prefix参数存在, id 将附加在其后面。

var idCounter = 0;
_.uniqueId = function (prefix) {
  var id = ++idCounter + ''; // 将数字转化为字符串
  return prefix ? prefix + id : id;
}

结语

万事开头难,啥复杂难的事都是从简单容易的事情开始做的,这篇博文主要讲了几个underscore中非常简单的api。接下来会慢慢接触一些比较难的,又非常重要的api。

你要看看这些有趣的函数方法吗?

前言

这是underscore.js源码分析的第六篇,如果你对这个系列感兴趣,欢迎点击

underscore-analysis/ watch一下,随时可以看到动态更新。

下划线中有非常多很有趣的方法,可以用比较巧妙的方式解决我们日常生活中遇到的问题,比如_.after,_.before_.defer...等,也许你已经用过他们了,今天我们来深入源码,一探究竟,他们到底是怎么实现的。

function

指定调用次数(after, before)

把这两个方法放在前面也是因为他们俩能够解决我们工作中至少以下两个问题

  1. 如果你要等多个异步请求完成之后才去执行某个操作fn,那么你可以用_.after,而不必写多层异步回调地狱去实现需求

  2. 有一些应用可能需要进行初始化操作而且仅需要一次初始化就可以,一般的做法是在入口处对某个变量进行判断,如果为真那么认为已经初始化过了直接return掉,如果为假那么进行参数的初始化工作,并在完成初始化之后设置该变量为真,那么下次进入的时候便不必重复初始化了。

对于问题1

let async1 = (cb) => {
  setTimeout(() => {
    console.log('异步任务1结束了')
    cb()
  }, 1000)
}

let async2 = (cb) => {
  setTimeout(() => {
    console.log('异步任务2结束了')
    cb()
  }, 2000)
}

let fn = () => {
  console.log('我是两个任务都结束了才进行的任务')
}

如果要在任务1,和任务2都结束了才进行fn任务,我们一般的写法是啥?
可能会下面这样写

async1(() => {
  async2(fn)
})

这样确实可以保证任务fn是在前面两个异步任务都结束之后才进行,但是相信你是不太喜欢回调的写法的,这里举的异步任务只有两个,如果多了起来,恐怕就要蛋疼了。别疼,用下划线的after函数可以解救你。

fn = _.after(2, fn)

async1(fn)
async2(fn)

运行截图

after举例

有木有很爽,不用写成回调地狱的形式了。那么接下来我们看看源码是怎么实现的。

after源码实现

_.after = function(times, func) {
  return function() {
    // 只有返回的函数被调用times次之后才执行func操作
    if (--times < 1) {
      return func.apply(this, arguments);
    }
  };
};

源码简单到要死啊,但是就是这么神奇,妥妥地解决了我们的问题1。

对于问题2

let app = {
  init (name, sex) {
    if (this.initialized) {
      return
    }
    // 进行参数的初始化工作
    this.name = name
    this.sex = sex
    // 初始化完成,设置标志
    this.initialized = true
  },
  showInfo () {
    console.log(this.name, this.sex)
  }
}

// 传参数进行应用的初始化

app.init('qianlonog', 'boy')
app.init('xiaohuihui', 'girl')
app.showInfo() // qianlonog boy 注意这里打印出来的是第一次传入的参数

一般需要且只进行一次参数初始化工作的时候,我们可能会像上面那样做。但是其实如果用下划线中的before方法我们还可以这样做。

let app = {
  init: _.before(2, function (name, sex) {
    // 进行参数的初始化工作
    this.name = name
    this.sex = sex
  }) ,
  showInfo () {
    console.log(this.name, this.sex)
  }
}

// 传参数进行应用的初始化

app.init('qianlonog', 'boy')
app.init('xiaohuihui', 'girl')
app.showInfo() // qianlonog boy 注意这里打印出来的是第一次传入的参数

好玩吧,让我们看看_.before是怎么实现的。

// 创建一个函数,这个函数调用次数不超过times次
// 如果次数 >= times 则最后一次调用函数的返回值将被记住并一直返回该值

_.before = function(times, func) {
  var memo;
  return function() {
    // 返回的函数每次调用都times减1
    if (--times > 0) { 
      // 调用func,并传入外面传进来的参数
      // 需要注意的是,后一次调用的返回值会覆盖前一次
      memo = func.apply(this, arguments);
    }
    // 当调用次数够了,就将func销毁设置为null
    if (times <= 1) func = null;
    return memo;
  };
};

让函数具有记忆的功能

在程序中我们经常会要进行一些计算的操作,当遇到比较耗时的操作时候,如果有一种机制,对于同样的输入,一定得到相同的输出,并且对于同样的输入,后续的计算直接从缓存中读取,不再需要将计算程序运行那就非常赞了。

举例

let calculate = (num, num2) => {
  let result = 0
  let start = Date.now()
  for (let i = 0; i< 10000000; i++) { // 这里只是模拟耗时的操作
    result += num
  }

  for (let i = 0; i< 10000000; i++) { // 这里只是模拟耗时的操作
    result += num2
  }
  let end = Date.now()
  console.log(end - start)
  return result
}

calculate(1, 2) // 30000000
// log 得到235
calculate(1, 2) // 30000000
// log 得到249

对于上面这个calculate函数,同样的输入1, 2,两次调用的输出都是一样的,并且两次都走了两个耗时的循环,看看下划线中的memoize函数,如何为我们省去第二次的耗时操作,直接给出300000的返回值

let calculate = _.memoize((num, num2) => {
  let start = Date.now()
  let result = 0
  for (let i = 0; i< 10000000; i++) { // 这里只是模拟耗时的操作
    result += num
  }

  for (let i = 0; i< 10000000; i++) { // 这里只是模拟耗时的操作
    result += num2
  }
  let end = Date.now()
  console.log(end - start)
  return result
}, function () {
  return [].join.call(arguments, '@') // 这里是为了给同样的输入指定唯一的缓存key
})

calculate(1, 2) // 30000000
// log 得到 238
calculate(1, 2) // 30000000
// log 啥也没有打印出,因为直接从缓存中读取了

源码实现

 _.memoize = function(func, hasher) {
  var memoize = function(key) {
    var cache = memoize.cache;
    // 注意hasher,如果传了hasher,就用hasher()执行的结果作为缓存func()执行的结果的key
    var address = '' + (hasher ? hasher.apply(this, arguments) : key); 
    // 如果没有在cache中查找到对应的key就去计算一次,并缓存下来
    if (!_.has(cache, address)) cache[address] = func.apply(this, arguments); 
    // 返回结果
    return cache[address];
  };
  memoize.cache = {};
  return memoize; // 返回一个具有cache静态属性的函数
};

相信你已经看懂了源码实现,是不是很简单,但是又很实用有趣。

来一下延时(.delay和.defer)

下划线中在原生延迟函数setTimeout的基础上做了一些改造,产生以上两个函数

*_.delay(function, wait, arguments)

就是延迟wait时间去执行functionfunction需要的参数由*arguments提供

使用举例

var log = _.bind(console.log, console)
_.delay(log, 1000, 'hello qianlongo')
// 1秒后打印出 hello qianlongo

源码实现

_.delay = function(func, wait) {
  // 读取第三个参数开始的其他参数
  var args = slice.call(arguments, 2);
  return setTimeout(function(){
    // 执行func并将参数传入,注意apply的第一个参数是null护着undefined的时候,func内部的this指的是全局的window或者global
    return func.apply(null, args); 
  }, wait);
};

不过有点需要注意的是_.delay(function, wait, *arguments)``function中的this指的是window或者global

*_.defer(function, arguments)

延迟调用function直到当前调用栈清空为止,类似使用延时为0的setTimeout方法。对于执行开销大的计算和无阻塞UI线程的HTML渲染时候非常有用。 如果传递arguments参数,当函数function执行时, arguments 会作为参数传入

源码实现

_.defer = _.partial(_.delay, _, 1);

所以主要还是看_.partial是个啥

可以预指定参数的函数_.partial

局部应用一个函数填充在任意个数的 参数,不改变其动态this值。和bind方法很相近。你可以在你的参数列表中传递_来指定一个参数 ,不应该被预先填充(underscore中文网翻译)

使用举例

let fn = (num1, num2, num3, num4) => {
  let str = `num1=${num1}`
  str += `num2=${num2}`
  str += `num3=${num3}`
  str += `num4=${num4}`
  return str
}

fn = _.partial(fn, 1, _, 3, _)
fn(2,4)// num1=1num2=2num3=3num4=4

可以看到,我们传入了_(这里指的是下划线本身)进行占位,后续再讲2和4填充到对应的位置去了。

源码具体怎么实现的呢?

_.partial = function(func) {
  // 获取除了传进回调函数之外的其他预参数
  var boundArgs = slice.call(arguments, 1); 
  var bound = function() {
    var position = 0, length = boundArgs.length;
    // 先创建一个和boundArgs长度等长的空数组
    var args = Array(length); 
    // 处理占位元素_
    for (var i = 0; i < length; i++) { 
      // 如果发现boundArgs中有_的占位元素,就依次用arguments中的元素进行替换boundArgs
      args[i] = boundArgs[i] === _ ? arguments[position++] : boundArgs[i]; 
    }
    // 把auguments中的其他元素添加到boundArgs中
    while (position < arguments.length) args.push(arguments[position++]); 
    // 最后执行executeBound,接下来看看executeBound是什么
    return executeBound(func, bound, this, this, args);
  };
  return bound;
};

在上一篇文章如何写一个实用的bind?
有详细讲解,这里我们再回顾一下
executeBound

var executeBound = function(sourceFunc, boundFunc, context, callingContext, args) {
  // 如果调用方式不是new func的形式就直接调用sourceFunc,并且给到对应的参数即可
  if (!(callingContext instanceof boundFunc)) return sourceFunc.apply(context, args); 
   // 处理new调用的形式
  var self = baseCreate(sourceFunc.prototype);
  var result = sourceFunc.apply(self, args);
  if (_.isObject(result)) return result;
  return self;
};

先看一下这些参数都�代表什么含义

  1. sourceFunc:原函数,待绑定函数
  2. boundFunc: 绑定后函数
  3. context:绑定后函数this指向的上下文
  4. callingContext:绑定后函数的执行上下文,通常就是 this
  5. args:绑定后的函数执行所需参数

这里其实就是执行了这句,所以关键还是如果处理预参数,和后续参数的逻辑

sourceFunc.apply(context, args);

管道式函数组合

你也许遇到过这种场景,任务A,任务B,任务C必须按照顺序执行,并且A的输出作为B的输入,B的输出作为C的输入,左后再得到结果。用一张图表示如下

管道

那么一般的做法是什么呢

let funcA = (str) => {
  return str += '-A'
}

let funcB = (str) => {
  return str += '-B'
}

let funcC = (str) => {
  return str += '-C'
}

funcC(funcB(funcA('hello')))
// "hello-A-B-C"

``` javascript
用下划线中的`compose`方法怎么做呢

``` javascript
let fn = _.compose(funcC, funcB, funcA)
fn('hello')
// "hello-A-B-C"

看起来没有一般的做法那样,层层绕进去了,而是以一种非常扁平的方式使用。

同样我们看看源码是怎么实现的。

_.compose源码

_.compose = function() {
  var args = arguments;
  // 从最后一个参数开始处理
  var start = args.length - 1;
  return function() {
    var i = start;
    // 执行最后一个函数,并得到结果result
    var result = args[start].apply(this, arguments); 
    // 从后往前一个个调用传进来的函数,并将上一次执行的结果作为参数传进下一个函数
    while (i--) result = args[i].call(this, result); 
    // 最后将结果导出
    return result;
  };
};

给多个函数绑定同样的上下文(_.bindAll(object, *methodNames))

将多个函数methodNames绑定上下文环境为object

😪 😪 😪,好困,写文章当真好要时间和精力,到这里已经快写了3个小时了,夜深,好像躺下睡觉啊!!!啊啊啊,再等等快说完了(希望不会误人子弟)。

var buttonView = {
  label  : 'underscore',
  onClick: function(){ alert('clicked: ' + this.label); },
  onHover: function(){ console.log('hovering: ' + this.label); }
};
_.bindAll(buttonView, 'onClick', 'onHover');

$('#underscore_button').bind('click', buttonView.onClick);

我们用官网给的例子说一下,默认的jQuery中$(selector).on(eventName, callback)callback中的this指的是当前的元素本身,当时经过上面的处理,会弹出underscore

_.bindAll源码实现

 _.bindAll = function(obj) {
  var i, length = arguments.length, key;
  // 必须要指定需要绑定到obj的函数参数
  if (length <= 1) throw new Error('bindAll must be passed function names');
  // 从第一个实参开始处理,这些便是需要绑定this作用域到obj的函数
  for (i = 1; i < length; i++) { 
    key = arguments[i];
    // 调用内部的bind方法进行this绑定
    obj[key] = _.bind(obj[key], obj); 
  }
  return obj;
};

内部使用了_.bind进行绑定,如果你对_.bind原生是如何实现的可以看这里如何写一个实用的bind?

拾遗

最后关于underscore.js中function篇章还有两个函数说一下,另外节流函数throttle以及debounce_会另外单独写一篇文章介绍,欢迎前往underscore-analysis/ watch一下,随时可以看到动态更新。

_.wrap(function, wrapper)

将第一个函数 function 封装到函数 wrapper 里面, 并把函数 function 作为第一个参数传给 wrapper. 这样可以让 wrapper 在 function 运行之前和之后 执行代码, 调整参数然后附有条件地执行.

直接看源码实现吧

_.wrap = function(func, wrapper) {
    return _.partial(wrapper, func);
  };

还记得前面说的partial吧,他会返回一个函数,内部会执行wrapper,并且func会作为wrapper的一个参数被传入。

_.negate(predicate)

将predicate函数执行的结果取反。

使用举例

let fn = () => {
  return true
}

_.negate(fn)() // false

看起来好像没什么软用,但是。。。。

let arr = [1, 2, 3, 4, 5, 6]

let findEven = (num) => {
  return num % 2 === 0
}

arr.filter(findEven) // [2, 4, 6]

如果要找到奇数呢?

let arr = [1, 2, 3, 4, 5, 6]

let findEven = (num) => {
  return num % 2 === 0
}

arr.filter(_.negate(findEven)) // [1, 3, 5]

源码实现

_.negate = function(predicate) {
  return function() {
    return !predicate.apply(this, arguments);
  };
};

源码很简单,就是把你传进来的predicate函数执行的结果取反一下,但是应用还是蛮多的。

结尾

这几个是underscore库中function相关的api,大部分已经说完了,如果对你有一点点帮助。

点一个小星星吧😀😀😀

点一个小星星吧😀😀😀

点一个小星星吧😀😀😀

good night 🌙

那些不起眼的小工具?

前言

原文链接

源码地址

今天想写一篇关于下划线这个库中一些小工具函数的故事,我们都听过一句话,一个成功的男人背后一定有一个了不起的女人(😀,其实也不一定,也许还有男人呢),那么一个经久不衰,为程序猿们所称道的库,框架的背后自然也有不少看起来不起眼,甚至你都懒得正眼瞧他的"小工具"存在。正是这些背后的无名英雄为类库和框架的形成,贡献了不可磨灭的力量。

工具图片

第一篇文章说了undefined,那我们也从undefined开始。

_.isUndefined(obj)

判断obj等于undefined与否,是就返回true,反之false。

示例

let a = null
let b = window.b
let c = () => {}
let d = undefined
let e = void 0
let f = 'qianlongo'
let g

console.log(_.isUndefined(a)) // false
console.log(_.isUndefined(b)) // true
console.log(_.isUndefined(c())) // true
console.log(_.isUndefined(d)) // true
console.log(_.isUndefined(e)) // true
console.log(_.isUndefined(f)) // false
console.log(_.isUndefined(g)) // true

对于一个对象上不存在的属性

对于一个没有返回值的函数

对于声明和却没有赋值的标量

对于直接赋值为undefined(非ie8以下)或者void 0

_.isUndefined都会返回true,其他情况全都是返回false

需要特别注意的是,有时候我们会这样判断一个变量是都存在,a == null
a == undefined都可以通难过判断。

if (a == null) { 

}

但是_.isUndefined用的是三等强制判断,所以null是通过不了的

_.isUndefined = function(obj) {
  return obj === void 0;
}

_.isNull(obj)

判断obj等于null与否,是就返回true,反之false。

这个没啥说的,只有obj输入null,结果输出才为true,因为内部判断也是用的三等判断,不仅值要相等,类型也要相同。

_.noConflict()

防止全局变量冲突的一种常见解决方案,将_的使用权交换给上一个占用_坑位的人。

示例

<script src="lodash.js"></script>
<script src="underscore.js"></script>

console.log(_)

遇见重名的事不新鲜对吧,全国有多少个小明啊,我们从小到大课本里到处都是小明和小红。

这里后面引入的underscore.js把lodash.js给覆盖了,因为两个库都想占用全局的_,结果后来者居上。

如果不想lodash被覆盖怎么办,总的有个先来后到啊。只需要调用noConflict方法便将占着的_坑位重新归还给了lodash,而之后我们用my_即可访问所有underscore.js的方法。

let my_ = _.noConflict()

接下来我们看下源码怎么实现的

var previousUnderscore = _ // 在源码的顶部,保存了前一个占着_坑位的人
 _.noConflict = function() {
    root._ = previousUnderscore; // 将_重新赋值给前一个占着_坑位的人
    return this; // 并将_返回以供后续使用
  };

_.identity(value)

返回与传入的参数value一样的值

这个函数看起来没有什么软用,但是在后面能够起非常大的作用,也正体现了,工具虽小,能量却大

我们先来简单地看下它的应用,在后续的源码分析中遇到再仔细讲解。

  1. 过滤一个数组中为"真"的值
let arr = ['a', 'b', null, 'false', 0, 'c', '', false, {}]
let arr2 = arr.filter(_.identity) // ["a", "b", "false", "c", {}]
  1. 复制数组
let arr = ['a', 'b', null, 'false', 0, 'c', '', false, {}]
let arr2 = arr.map(_.identity) // ["a", "b", null, "false", 0, "c", "", false, {}]

_.constant(value)

返回一个函数fn,fn执行之后再返回当初传进来的value

我们来看一段github上关于下划线的一个issue,挺有意思的。也许我们比较难列举出这个函数的应用,但是至少下面这个例子是比较好的。

let age = 18
let cacheAge = _.constant(age)

age += 10
console.log(cacheAge()) // 18

为什么可以缓存住18,我们看下源码大概就知道了,源码创建了常见的闭包,闭包常见的作用之一就是让外面通过函数调用的形式去访问内部的变量,以及在一定的生命周期内,缓存住变量。

 _.constant = function(value) {
    return function() {
      return value;
    };
  };

_.noop()

一个空函数,啥也不干,调用了就返回undefined给你,可以作为默认的回调参数

又是一个看起来啥用都没有的函数,然而事实真的是这样吗?请移步以下几个链接

  1. What is the use of jQuery.noop() function?
  2. What is the JavaScript convention for no operation?

例子不用多,总结一下

1. 给一个变量赋值为一个空函数,在后续的调用中你不需要去检测他是不是undefined

2. 为什么不给需要的变量重新设置一个空函数? _.noop已经创建了一个函数空间,让其他变量也指向这个函数,可以减少js中不必要的花销

_.times(n, iteratee, context)

调用给定的iteratee迭代函数n次,iteratee每次都接收一个索引值index,最后返回一个数组,数组中存着这几次iteratee的回调结果

示例

let count = 0
let result = _.times(6, (i) => {
  console.log(++count)
  return `hello:${i}`
})

console.log(result) // ["hello:0", "hello:1", "hello:2", "hello:3", "hello:4", "hello:5"]

console.log(count) // 6

可以看到传进去的函数执行执行了6次,并将对应的每次执行的结果存在了数组中返回。

_.random(min, max)

返回一个[min, max]之间的随机整数,如果没有传max,则区间是[0, min]

示例

let num1 = _.random(10, 20) // maybe 13 or other
let num2 = _.random(10) // maybe 6 or other

源码

_.random = function(min, max) {
  if (max == null) { // 如果只有一个参数
    max = min; // 就把第一个参数当最大值
    min = 0; // 0作为最小值
  }
  return min + Math.floor(Math.random() * (max - min + 1));
  // 试想我们要求取[4, 10)之间的某个整数
  // (min) 就是保证最小值可以取到4
  // (max - min + 1) => (10 - 4 + 1) => 7
  // Math.random() * 7 => [0, 1) * 7 => [0, 7)
  // Math.floor([0, 7)) => 最小取0, 最大取6
  // 最后变成 4 + [0, 6] => [4, 10]
};


当然啦,如果你传入非整数,或者max < min的数,那结果就有可能不能按照预期出现了

_.uniqueId(prefix)

生成唯一的id,如果prefix不存在则直接将数字id返回,这个函数在给dom添加唯一的id的时候比较有用。

let pre = 'qianlongo'
let id1 = _.uniqueId() // 1
let id2 = _.uniqueId(pre) // qianlongo2

源码

var idCounter = 0;
_.uniqueId = function(prefix) {
  var id = ++idCounter + ''; // 转成字符串
  return prefix ? prefix + id : id;
};

_.now()

一个优化的方式来获得一个当前时间的整数时间戳。

直接看源码

_.now = Date.now || function() { // 如果原生支持now就用原生的,否知自己实现一个
  return new Date().getTime();
};


结尾

暂时就介绍这些看起来并不起眼的工具函数,在以后的文章和源码分析中遇到其他的会陆续更新到这篇文章中来。写一篇文章真够耗费时间的,陆陆续续用了好几个小时才写这么点。

不介意的话,在文章开头的源码地址那里点一个小星星吧😀

不介意的话,在文章开头的源码地址那里点一个小星星吧😀

不介意的话,在文章开头的源码地址那里点一个小星星吧😀

教你认清这8大杀手锏

前言

underscore.js源码分析第三篇,前两篇地址分别是

那些不起眼的小工具?

(void 0)与undefined之间的小九九

本篇原文链接

源码地址

😔看了很多篇技术文章,却依然写不好前端。

从步入程序猿这个大坑开始到现在,已经看过数不清的技术文章和书籍,有的是零散的知识,有的是系列权威的教程,但为毛还写不好挚爱的前端,听说过一句话,这个世界又不是只有你一个人深爱而不得。但纵使如此,我也要技术这条路上一路走到黑。直到天涯迷了路,海角翻了船。

开始

今天想说几个类似我们平常的工作中经常用到的几个宝贝,姑且把他叫做杀手锏好了,因为实在是特别好用呀,他们分别是...

  1. each
  2. map
  3. reduce
  4. reduceRight
  5. find
  6. filter
  7. every
  8. some

接下来我们从下划线underscore.js的视角,一步步看他们的内部运行的原理是什么....

1 _.each(list, iteratee, [context])

遍历list中的所有元素,按顺序用遍历输出每个元素,如果传递了context,则将iteratee函数中的this绑定到context上。

先来看一下怎么使用

let arr = ['name', 'sex']
let obj = {
  name: 'qianlongo',
  sex: 'boy'
}

// 不传入context
// 遍历数组
_.each(arr, console.log) 
// name 0 (2) ["name", "sex"]
// sex 1 (2) ["name", "sex"]

// 遍历对象
_.each(obj, console.log)
// qianlongo name {name: "qianlongo", sex: "boy"}
// boy sex  {name: "qianlongo", sex: "boy"}


// 传入context
_.each(arr, function (val, key, arr) {
  console.log(this[val])
}, obj)
// qianlongo
// boy

可以看出下划线的each和原生的数组forEach有些类似也有不同的地方

原生的forEach只可以遍历数组,而下划线的each还可以遍历对象。接下来你想不想一起看下下划线是怎么实现的。come on!!!

源码

_.each = _.forEach = function(obj, iteratee, context) {
  // 优化遍历函数iteratee,将iteratee中的this动态设置为context
  iteratee = optimizeCb(iteratee, context); 
  var i, length;
  if (isArrayLike(obj)) { // 如果是类数组类型的obj
    for (i = 0, length = obj.length; i < length; i++) {
      // iteratee接收的三个参数分别是 数组的值,数组的索引,以及数组本身
      iteratee(obj[i], i, obj); 
    }
  } else { // 支持对象类型的数据迭代
    var keys = _.keys(obj); // 拿到obj自身的所有keys
    for (i = 0, length = keys.length; i < length; i++) {
      // iteratee接收的三个参数分别是 obj的属性值,obj的属性,obj本身
      iteratee(obj[keys[i]], keys[i], obj);
    }
  }
  return obj; // 最后将obj返回
};

😉,其实也没有那么难理解是吧!开始map函数之旅吧

2 _.map(list, iteratee, [context])

通过iteratee将list中的每个值映射到一个新的数组中(注:产生一个新的数组。y = f(x),类似高中学过的知识,将x通过f()映射为一个新的数

使用案例

let arr = ['qianlongo', 'boy']
let obj = {
  name: 'qianlongo',
  sex: 'boy'
}

// list是个数组的时候
_.map(arr, (val, index) => {
  return `hello : ${val}`
})
// ["hello : qianlongo", "hello : boy"]

// list是个对象的时候
_.map(obj, (val, key, obj) => {
  return `hello : ${val}`
})
// ["hello : qianlongo", "hello : boy"]

当然还可以传入第三个参数context,其本质如each一般,也是让iteratee函数中的this动态设置为context

源码

 _.map = _.collect = function(obj, iteratee, context) {
  // 可以将这里的内部cb函数理解为绑定iteratee的this到context
  iteratee = cb(iteratee, context);
  // 非类数组对象就获取obj的keys,这里如果是类数组最后得到的keys为undefined
  var keys = !isArrayLike(obj) && _.keys(obj),
      length = (keys || obj).length,
      results = Array(length); // 创建一个和obj长度空间一样的数组
  for (var index = 0; index < length; index++) {
    // 注意这里,keys存在则代表obj是个对象,所以要拿到keys中的值,否则是类数组的话,直接用index索引就好了
    var currentKey = keys ? keys[index] : index;
    // 看到了吗,这里将iteratee执行后的返回值塞到了results数组中
    results[index] = iteratee(obj[currentKey], currentKey, obj);
  }
  return results; // 最后将映射之后的数组返回
};

通过源码可以看到map的实现思路

  1. 创建一个即将返回的数组
  2. 遍历list(可以为数组也可以为对象),将list的元素输入到传进来的iteratee函数中,并将其执行后的返回值填充进数组。这个iteratee负责映射规则

3 _.every(list, [predicate], [context])

当list中的所有的元素都可以通过predicate的检测,那么结果返回true,否则false

使用案例

let arr = [-1, -3, -6, 0, 3, 6, 9]
let obj = {
  name: 'qianlongo',
  sex: 'boy'
}

let result = _.every(arr, (val, key, arr) => {
  return val > 0
})
// false

let result2 = _.every(obj, (val, key, obj) => {
  return val.indexOf('o') > -1
})
// true

使用起来蛮简单的,传入一个谓词函数(返回值是一个布尔值的函数),最后得到true或者false。

源码

_.every = _.all = function(obj, predicate, context) {
  // 可以将这里的内部cb函数理解为绑定iteratee的this到context
  predicate = cb(predicate, context);
  // 短路写法,非类数组则获取其keys
  var keys = !isArrayLike(obj) && _.keys(obj),
      length = (keys || obj).length;
  for (var index = 0; index < length; index++) {
    // keys若能转化为"真" 则说明obj是对象类型
    var currentKey = keys ? keys[index] : index; 
    // 只要有一个不满足就返回false,中断迭代
    if (!predicate(obj[currentKey], currentKey, obj)) return false;
  }
  return true; // 否则所有元素都通过判断返回true
};

4 _.some(list, [predicate], [context])

如果list中有任何一个元素通过 predicate的检测就返回true。否则返回false,和every恰好有点相反的意思。

使用案例

let arr = [-1, -3, -6, 0, 3, 6, 9]
let obj = {
  name: 'qianlongo',
  sex: ''
}

let result = _.some(arr, (val, key, arr) => {
  return val > 0
})
// true 因为至少有一个元素 >0

let result2 = _.some(obj, (val, key, obj) => {
  return val.indexOf('o') > -1
})
// true 两个都包含'o' 当然返回true

源码中是怎么实现的呢,与every唯一不同的地方在返回true还是falase之处?

源码

_.some = _.any = function(obj, predicate, context) {
  predicate = cb(predicate, context);
  var keys = !isArrayLike(obj) && _.keys(obj),
      length = (keys || obj).length;
  for (var index = 0; index < length; index++) {
    var currentKey = keys ? keys[index] : index;
    if (predicate(obj[currentKey], currentKey, obj)) return true; // 只要有一个满足条件就返回true
  }
  return false; // 所有都不满足则返回false
};

5 _.find(list, predicate, [context])

遍历list中的元素,返回第一个通过predicate函数检测的值。

使用案例

let arr = [-1, -3, -6, 0, 3, 6, 9]
let obj = {
  sex: 'boy',
  name: 'qianlongo'
}
let result = _.find(arr, (val, key, arr) => {
  return val > 0
})
// 3
let result2 = _.find(obj, (val, key, obj) => {
  return val.indexOf('o') > -1
})
// boy

源码

_.find = _.detect = function(obj, predicate, context) {
  var key;
  if (isArrayLike(obj)) {
    // 当传入的是类数组的时候,调用findIndex方法,结果是>= -1的数组
    key = _.findIndex(obj, predicate, context);
  } else {
    // 当传入的是一个对象的时候,调用findKey,结果是一个字符串属性或者undefined
    key = _.findKey(obj, predicate, context);
  }
  // 返回符合条件的value,否则没有返回值,即默认的undefined
  if (key !== void 0 && key !== -1) return obj[key]; 
};

_.findIndex_.findKey在后面会一一分析,目前理解find函数知道他们怎么用就好。

6 _.filter(list, predicate, [context])

遍历list,返回包含所有通过predicate检测的元素(结果是个数组)

使用案例

let arr = [-1, -3, -6, 0, 3, 6, 9]
let obj = {
  sex: 'boy',
  name: 'qianlongo',
  age: 100
}
let result = _.filter(arr, (val, key, arr) => {
  return val > 0
})
// [3, 6, 9]
let result2 = _.filter(obj, (val, key, obj) => {
  return `${val}`.indexOf('o') > -1 // 使用模板字符串是防止100没有indexOf方法而报错
})
// ["boy", "qianlongo"]

聪明的你是不是已经想到了源码是怎么实现的了 😉

源码

_.filter = _.select = function(obj, predicate, context) {
  var results = [];
  // 绑定predicate的this作用域到context
  predicate = cb(predicate, context);
  // 用each方法对obj进行遍历
  _.each(obj, function(value, index, list) {
    // 符合predicate过滤条件的,就把对应的值塞到results数组中
    if (predicate(value, index, list)) results.push(value);
  });
  return results; // 最后返回
};

最后是reduce和reduceRight,两个相对来说更难一些的api,虽然已经过了12点了,手动困乏😪, 我们咬咬牙坚持一下,把最后两个说完

7 _.reduce(list, iteratee, [memo], [context]),

别名为 inject 和 foldl, reduce方法把list中元素归结为一个单独的数值。Memo是reduce函数的初始值,reduce的每一步都需要由iteratee返回。这个迭代传递4个参数:memo, value 和 迭代的index(或者 key)和最后一个引用的整个 list

8 _.reduceRight(list, iteratee, memo, [context])

reducRight是从右侧开始组合的元素的reduce函数

使用案例

var arr = [0, 1, 2, 3, 4, 5],
  sum = _.reduce(arr, (init, cur, i, arr) => {
    return init + cur;
  });	
  
  // 15

我们来看一下上面的执行过程是怎样的。

第一回合

// 因为initialValue没有传入所以回调函数的第一个参数为数组的第一项
init = 0;
cur = 1;
=> init + cur = 1;

第二回合

init = 1;
cur = 2;
=> init + cur = 3;

第三回合

init = 3;
cur = 3;
=> init + cur = 6;

第四回合

init = 6;
cur = 4;
=> init + cur = 10;

第五回合

init = 10;
cur = 5;
=> init + cur = 15;

😭妈妈啊,终于执行完了,这么多回合才结束,哪像人家格斗高手瞬间就把太极大师整挂了

知道了一步步执行流程,我们来看下源码到底是怎么实现的。

源码

// 源码还是通过调用createReduce生成的,所以主要是看createReduce这个函数
_.reduce = _.foldl = _.inject = createReduce(1);

这尼玛看起来好吓人啊,不怕,我们一点点来分析

function createReduce(dir) {
    // Optimized iterator function as using arguments.length
    // in the main function will deoptimize the, see #1991.
    function iterator(obj, iteratee, memo, keys, index, length) { // 真正执行迭代的地方
      for (; index >= 0 && index < length; index += dir) {
        var currentKey = keys ? keys[index] : index; // 如果keys存在则认为是obj形式的参数,所以读取keys中的属性值,否则类数组只需要读取索引index即可
        memo = iteratee(memo, obj[currentKey], currentKey, obj); // 接着就是执行外部传入的回调了,并将结果赋值为memo,也就是我们最后要到的值
      }
      return memo;
    }

    return function(obj, iteratee, memo, context) {
      iteratee = optimizeCb(iteratee, context, 4); // 首先绑定一下this作用域
      var keys = !isArrayLike(obj) && _.keys(obj), // 如果不是类数组就读取其keys
          length = (keys || obj).length,
          index = dir > 0 ? 0 : length - 1; // 默认开始迭代的位置,从左边第一个开始还是右边第一个
      // Determine the initial value if none is provided.
      if (arguments.length < 3) { // 如果没有传入初始化值,则将第一个值(左边第一个或者右边第一个)作为初始值
        memo = obj[keys ? keys[index] : index];
        index += dir; // 从索引为1开始或者索引为length - 2开始迭代
      }
      return iterator(obj, iteratee, memo, keys, index, length); // 接着开始进入自定义的迭代函数,请往上看
    };
  }

结语

夜深人静,有点困乏了。希望这篇文章对大家有点作用。如果对前几篇源码分析的文章感兴趣,欢迎前往顶部地址查看

不介意的话,在文章开头的源码地址那里点一个小星星吧😀

不介意的话,在文章开头的源码地址那里点一个小星星吧😀

不介意的话,在文章开头的源码地址那里点一个小星星吧😀

如何写一个实用的bind?

前言

这是underscore.js源码分析的第五篇,如果你对这个系列感兴趣,欢迎点击

underscore-analysis/ watch一下,随时可以看到动态更新。

事情要从js中的this开始说起,你是不是也经常有种无法掌控和知晓它的感觉,对于初学者来说,this简直如同回调地狱般,神乎其神,让人无法捉摸透。但是通过原生js中的bind方法,我们可以显示绑定函数的this作用域,而无需担心运行时是否会改变而不符合自己的预期。当然了下划线中的bind也是模仿它的功能同样可以达到类似的效果。

ctx

bind简单回顾

我们从mdn上的介绍来回顾一下bind的使用方法。

bind方法创建一个新的函数, 当被调用时,它的this关键字被设置为提供的值。

语法

fun.bind(thisArg[, arg1[, arg2[, ...]]])

简单地看一下这些参数的含义

  1. thisArg

当绑定函数被调用时,该参数会作为原函数运行时的this指向,当使用new 操作符调用绑定函数时,该参数无效。

  1. arg1, arg2, ...

当绑定函数被调用时,这些参数将置于实参之前传递给被绑定的方法。

绑定this作用域示例

window.name = 'windowName'

let obj = {
  name: 'qianlongo',
  showName () {
    console.log(this.name)
  }
}

obj.showName() // qianlongo

let showName = obj.showName
  showName() // windowName

let bindShowName = obj.showName.bind(obj)
  bindShowName() // qianlongo

通过以上简单示例,我们知道了第一个参数的作用就是绑定函数运行时候的this指向

第二个参数开始起使用示例

let sum = (num1, num2) => {
  console.log(num1 + num2)
}

let bindSum = sum.bind(null, 1)
bindSum(2) // 3

bind可以使一个函数拥有预设的初始参数。这些参数(如果有的话)作为bind的第二个参数跟在this(或其他对象)后面,之后它们会被插入到目标函数的参数列表的开始位置,传递给绑定函数的参数会跟在它们的后面。

参数的使用基本上明白了,我们再来看看使用new去调用bind之后的函数是怎么回事。

function Person (name, sex) {
  console.log(this) // Person {}
  this.name = name
  this.sex = sex
}
let obj = {
  age: 100
}
let bindPerson = Person.bind(obj, 'qianlongo')

let p = new bindPerson('boy')

console.log(p) // Person {name: "qianlongo", sex: "boy"}

有没有发现bindPerson内部的this不再是bind的第一个参数obj,此时obj已经不再起效了。

实际上bind的使用是有一定限制的,在一些低版本浏览器下不可用,你想不想看看下划线中是如何实现一个兼容性好的bind呢!!!come on

下划线中bind实现

源码

 _.bind = function(func, context) {
  // 如果原生支持bind函数,就用原生的,并将对应的参数传进去
  if (nativeBind && func.bind === nativeBind) return nativeBind.apply(func, slice.call(arguments, 1));
  // 如果传入的func不是一个函数类型 就抛出异常
  if (!_.isFunction(func)) throw new TypeError('Bind must be called on a function');
  // 把第三个参数以后的值存起来,接下来请看executeBound
  var args = slice.call(arguments, 2);
  var bound = function() {
    return executeBound(func, bound, context, this, args.concat(slice.call(arguments)));
  };
  return bound;
};

executeBound实现

var executeBound = function(sourceFunc, boundFunc, context, callingContext, args) {
  // 如果调用方式不是new func的形式就直接调用sourceFunc,并且给到对应的参数即可
  if (!(callingContext instanceof boundFunc)) return sourceFunc.apply(context, args); 
  // 处理new调用的形式
  var self = baseCreate(sourceFunc.prototype);
  var result = sourceFunc.apply(self, args);
  if (_.isObject(result)) return result;
  return self;
};

上面的源码都做了相应的注释,我们着重来看一下executeBound的实现

先看一下这些参数都代表什么含义

  1. sourceFunc:原函数,待绑定函数
  2. boundFunc: 绑定后函数
  3. context:绑定后函数this指向的上下文
  4. callingContext:绑定后函数的执行上下文,通常就是 this
  5. args:绑定后的函数执行所需参数

ok,我们来看一下第一句

if (!(callingContext instanceof boundFunc)) return sourceFunc.apply(context, args); 

这句话是为了判断绑定后的函数是以new关键字被调用还是普通的函数调用的方式,举个例子

function Person () {
  if (!(this instanceof Person)) {
    return console.log('非new调用方式')
  }

  console.log('new 调用方式')
}

Person() // 非new调用方式
new Person() // new 调用方式

所以如果你希望自己写的构造函数无论是new还是没用new都起效的话可以用下面的代码

function Person (name, sex) {
  if (!(this instanceof Person)) {
    return new Person(name, sex)
  }

  this.name = name
  this.sex = sex
}

new Person('qianlongo', 'boy') // Person {name: "qianlongo", sex: "boy"}

Person('qianlongo', 'boy') // Person {name: "qianlongo", sex: "boy"}

我们回到executeBound的解析

if (!(callingContext instanceof boundFunc)) return sourceFunc.apply(context, args); 

callingContext是被绑定后的函数的this作用域,boundFunc就是那个被绑定后的函数,那么通过这个if判断,当为非new调用形式的时候,直接利用apply处理即可。

但是如果是用new调用的呢?我们看下面这段代码,别看短短的四行代码里面知识点挺多的呢!

// 这里拿到的是一个空对象,且其继承于原函数的原型链prototype
var self = baseCreate(sourceFunc.prototype);
// 构造函数执行之后的返回值
// 一般情况下是没有返回值的,也就是undefined
// 但是有时候写构造函数的时候会显示地返回一个obj
var result = sourceFunc.apply(self, args);
// 所以去判断结果是不是object,如果是那么返回构造函数返回的object
if (_.isObject(result)) return result;
// 如果没有显示返回object,就返回原函数执行结束后的实例
return self;

好,到这里,我有一个疑问,baseCreate是个什么鬼?

var Ctor = function(){};

var baseCreate = function(prototype) {
  // 如果prototype不是object类型直接返回空对象
  if (!_.isObject(prototype)) return {};
  // 如果原生支持create则用原生的
  if (nativeCreate) return nativeCreate(prototype); 
  // 将prototype赋值为Ctor构造函数的原型
  Ctor.prototype = prototype; 
  // 创建一个Ctor实例对象
  var result = new Ctor; 
  // 为了下一次使用,将原型清空
  Ctor.prototype = null; 
  // 最后将实例返回
  return result; 
};

是不是好简单,就是实现了原生的Object.create用来做一些继承的事情。

结尾

文章很简短,知道怎么实现一个原生的bind就行。如果你对apply、call和this感兴趣,欢迎查看

js中call、apply、bind那些事

this-想说爱你不容易

悄悄地说一个bug

前言

underscore.js源码分析第四篇,前三篇地址分别是,如果你对这个系列感兴趣,欢迎点击watch,随时关注动态。

教你认清这8大杀手锏

那些不起眼的小工具?

(void 0)与undefined之间的小九九

原文地址
源码地址

逗我呢?哥!你要说什么bug,什么bug,什么bug,我最讨厌bug。去他妹的bug。

客观别急,今天真的是要说一个bug,也许你早已知晓,也许你时常躺枪于他手,悄悄地,我们慢慢开始。

for in 遍历对象属性时存在bug

for in 遍历对象属性时存在bug

for in 遍历对象属性时存在bug

使用for in去遍历一个对象俺们再熟悉不过了,经常干这种事,那他到底可以遍历一个对象哪些类型的属性呢? 长得帅的还是看起来美美的,瞎说,它能够遍历的是对象身上那些可枚举标志([[Enumerable]])为true的属性。

  1. 对于通过直接的赋值和属性初始化的属性,该标识值默认为即为 true
  2. 对于通过 Object.defineProperty 等定义的属性,该标识值默认为 false

举个例子哪些属性可以被枚举

let Person = function (name, sex) {
  this.name = name
  this.sex = sex
}

Person.prototype = {
  constructor: Person,
  showName () {
    console.log(this.name)
  },
  showSex () {
    console.log(this.sex)
  }
}

Person.wrap = {
  sayHi () {
    console.log('hi')
  }
}

var p1 = new Person('qianlongo', 'sex')

p1.sayBye = () => {
  console.log('bye')
}

p1.toString = () => {
  console.log('string')
}

Object.defineProperty(p1, 'info', {
  enumerable: false,
  configurable: false,
  writable: false,
  value: 'feDev'
});Ï

for (var key in p1) {
  console.log(key)
}


// name
// sex
// sayBye
// constructor
// showName
// showSex
// toString
  1. 可以看到我们手动地用defineProperty,给某个对象设置属性时,enumerable为false此时该属性是不可枚举的
  2. Person继承自Object构造函数,但是for in并没有枚举出Object原型上的一些方法
  3. 手动地覆盖对象原型上面的方法toString也是可枚举的

如何判断一个对象的属性是可枚举的

方式其实很简单,使用原生js提供的Object.propertyIsEnumerable来判断

let obj = {
  name: 'qianlongo'
}

let obj2 = {
  name: 'qianlongo2',
  toString () {
    return this.name
  }
}

obj.propertyIsEnumerable('name') // true
obj.propertyIsEnumerable('toString') // false

obj2.propertyIsEnumerable('name') // true
obj2.propertyIsEnumerable('toString') // true

为什么obj判断toString为不可枚举属性,而obj2就是可枚举的了呢?原因很简单,obj2将toString重写了,而一个对象自身直接赋值的属性是可被枚举的

说了这么多,接下来我们来看一下下划线中涉及到遍历的部分对象方法,come on!!!

_.has(object, key)

判断对象obejct是否包含key属性

平时你可能经常这样去判断一个对象是否包含某个属性

if (obj && obj.key) {
  // xxx
}

但是这样做有缺陷,比如某个属性其对应的值为0,null,false,''空字符串呢?这样明明obj有以下对应的属性,却因为属性值为而通过不了验证

let obj = {
  name: '',
  sex: 0,
  handsomeBoy: false,
  timer: null
}

所以我们可以采用下划线中的这种方式

源码

var hasOwnProperty = ObjProto.hasOwnProperty;
_.has = function(obj, key) {
  return obj != null && hasOwnProperty.call(obj, key);
};

_.keys(object)

获取object对象所有的属性名称。

使用示例

let obj = {
  name: 'qianlongo',
  sex: 'boy'
}

let keys = _.keys(obj)
// ["name", "sex"]

源码

_.keys = function(obj) {
  // 如果obj不是object类型直接返回空数组
  if (!_.isObject(obj)) return [];
  // 如果浏览器支持原生的keys方法,则使用原生的keys
  if (nativeKeys) return nativeKeys(obj);
  var keys = [];
  // 注意这里1、for in会遍历原型上的键,所以用_.has来确保读取的只是对象本身的属性
  for (var key in obj) if (_.has(obj, key)) keys.push(key);
  // Ahem, IE < 9.
  // 这里主要处理ie9以下的浏览器的bug,会将对象上一些本该枚举的属性认为不可枚举,详细可以看collectNonEnumProps分析
  if (hasEnumBug) collectNonEnumProps(obj, keys); 
  return keys;
};

collectNonEnumProps函数分析

该函数为下划线中的内部函数一枚,专门处理ie9以下的枚举bug问题,for in到底有啥bug,终于可以说出来了。

简单地说就是如果对象将其原型上的类似toString的方法覆盖了的话,那么我们认为toString就是可枚举的了,但是在ie9以下的浏览器中还是认为是不可以枚举的,又是万恶的ie

源码

// 判断浏览器是否存在枚举bug,如果有,在取反操作前会返回false
var hasEnumBug = !{toString: null}.propertyIsEnumerable('toString'); 
// 所有需要处理的可能存在枚举问题的属性
var nonEnumerableProps = ['valueOf', 'isPrototypeOf', 'toString',
                    'propertyIsEnumerable', 'hasOwnProperty', 'toLocaleString']; 

// 处理ie9以下的一个枚举bug                      
function collectNonEnumProps(obj, keys) {
  var nonEnumIdx = nonEnumerableProps.length;
  var constructor = obj.constructor;
  // 读取obj的原型
  var proto = (_.isFunction(constructor) && constructor.prototype) || ObjProto;  

  // 这里我有个疑问,对于constructor属性为什么要单独处理?
  // Constructor is a special case.
  var prop = 'constructor'; 
  if (_.has(obj, prop) && !_.contains(keys, prop)) keys.push(prop);

  while (nonEnumIdx--) {
    prop = nonEnumerableProps[nonEnumIdx];
    // nonEnumerableProps中的属性出现在obj中,并且和原型中的同名方法不等,再者keys中不存在该属性,就添加进去
    if (prop in obj && obj[prop] !== proto[prop] && !_.contains(keys, prop)) {
      keys.push(prop);
    }
  }
}

代码看起来并不复杂,但是有一个小疑问,对于constructor属性为什么要单独处理呢?各个看官,如果知晓,请教我啊

_.allKeys(object)

获取object中所有的属性,包括原型上的。

举个简单的例子说明

let Person = function (name, sex) {
  this.name = name
  this.sex = sex
}

Person.prototype = {
  constructor: Person,
  showName () {
    console.log(this.name)
  }
}

let p = new Person('qianlongo', 'boy')

_.keys(p)
// ["name", "sex"] 只包括自身的属性


_.allKeys(p)
// ["name", "sex", "constructor", "showName"] 还包括原型上的属性

接下来看下源码是怎么干的

源码

// 获取对象obj的所有的键
// 与keys不同,这里包括继承来的key

// Retrieve all the property names of an object.
_.allKeys = function(obj) {
  if (!_.isObject(obj)) return [];
  var keys = [];
  // 直接读遍历取到的key,包括原型上的
  for (var key in obj) keys.push(key); 
  // Ahem, IE < 9.
  if (hasEnumBug) collectNonEnumProps(obj, keys); // 同样处理一下有枚举问题的浏览器
  return keys;
};

可以看到和_.keys的唯一的不同就在于遍历obj的时候有没有用hasOwnProperty去判断

_.values()

返回object对象所有的属性值。

使用案例

let obj = {
  name: 'qianlongo',
  sex: 'boy'
}

_.values(obj)
// ["qianlongo", "boy"]

源码

// Retrieve the values of an object's properties.
_.values = function(obj) {
  // 用到了前面已经写好的keys函数,所以values认为获取的属性值,不包括原型
  var keys = _.keys(obj);
  var length = keys.length;
  var values = Array(length);
  for (var i = 0; i < length; i++) {
    values[i] = obj[keys[i]];
  }
  return values;
};

_.invert(object)

返回一个object副本,使其键(keys)和值(values)对换。

使用案例

let obj = {
  name: 'qianlongo',
  secName: 'qianlongo',
  age: 100
}

_.invert(obj)

// {100: "age", qianlongo: "secName"}

注意哟,如果对象中有些属性值是相等的,那么翻转过来的对象其key取最后一个

源码

_.invert = function(obj) {
  var result = {};
  // 所以也只是取对象本身的属性
  var keys = _.keys(obj); 
  for (var i = 0, length = keys.length; i < length; i++) {
    // 值为key,key为值,如果有值相等,后面的覆盖前面的
    result[obj[keys[i]]] = keys[i]; 
  }
  return result;
};

_.functions(object)

返回一个对象里所有的方法名, 而且是已经排序的(注意这里包括原型上的属性)

源码

_.functions = _.methods = function(obj) {
  var names = [];
  for (var key in obj) {
    // 是函数,就装载进去
    if (_.isFunction(obj[key])) names.push(key);
  }
  return names.sort(); // 最后返回经过排序的数组
};

结尾

夜深人静,悄悄地说一个bug这个鬼故事讲完了,各位good night。

(void 0)与undefined之间的小九九

前言

原文链接

源码地址

这是underscore.js源码分析的第一篇文章,为什么选择写这篇文章呢?其实主要有两点

  1. 下划线源码中通篇可见这样的判断obj === void 0,初次看这样的写法完全不知道什么意思,所以想整明白它。
  2. 决定写一个系列把下划线分析完整,希望由浅入深,柿子捡软的捏,先从简单的开始入手(😀)

写完这篇文章希望达到什么样的效果呢?

  1. 说明白为什么用(void 0)代替undefined

  2. (void 0)的一些简单应用

http://odssgnnpf.bkt.clouddn.com/aa36597c9b81cb72.jpg

void 0是个啥

void 0是个啥,为毛它可以直接代替undefined关键字来做判断呢?我们可以看下mdn上的解释

The void operator evaluates the given expression and then returns undefined.

void 运算符 对给定的表达式进行求值,然后返回 undefined

啥?去执行了一段表达式,最后却得到undefined,那要是表达式执行的结果是2、3、8、毛主席**,也是返回undefined吗?答案是:对的。他就是这么个东西,不管你表达式里写的是个啥,我最后就是给你个undefined。

黑人问号

undefined又是啥

undefined是js原始类型值之一,也是全局对象window的属性,在部分低级别的浏览器中可以被修改,在局部作用域中也可以被修改。

首先我们来看这一段断码

var undefined = 'qianlongo'
alert(undefined)

最后console出来的是啥呢?undefined : qianlongo,
没图你说个js,接下来截取部分浏览器运行后的截图

ie7

ie7

ie8

ie8

ie9

测试结果为undefined

ie10

ie10

chrome

在最新的版本58.0.3029.81测试结果为undefined

firefox

在最新的版本52.0.2测试结果为undefined

欧朋浏览器

在最新的版本39.0.2256.48测试结果为undefined

你看ie老版本中就是那么任性,在全局作用域中可以直接改写undefined,也就是说当你想知道一个变量是不是等于undefined的时候直接这样判断已经不安全了。

if (obj === undefined) {
  // xxx
}

接下来我们再看一段js

var testUndefined = function () {
  var obj = {}
  var undefined = 'underscore'
  var window = {
    'undefined': 'qianlongo'
  }
  console.log(window) // {'undefined': 'qianlongo'}
  console.log(undefined) // underscore
  console.log(window.undefined) // qianlongo
  console.log(obj.name === undefined) // false
  console.log(obj.name === window.undefined) // false
  console.log(obj.name === (void 0)) // true
}

testUndefined()

可以得出,window,undefined本身在局部作用域中是可以被重写掉的,同样的道理,如果你在局部作用域中同样用以下代码来判断obj是不是undefined,是有风险的。

if (obj === undefined) {
  // xxx
}

为啥要用void 0来代替undefined

为啥要用void 0来代替undefined,基于以上介绍,原因就在这里了,void 0无论何时何地,后面跟了什么,结果都得到undefined,这正好是我们需要的。所以将上面的判断改写一下

当然了还要另一个原因void 0 比undefined短

if (obj === void 0) {
  // xxx
}

void 0的一些其他应用

  1. 填充a标签的href
<div style="height: 10000px;"></div>
<a href="#">xxxx</a>
<script>
  console.log('xxxx')
</script>

上面这段代码使用一个#号来填充a标签的href属性,这其实是有一些弊端的,比如用户点击的时候,页面会回到顶部(网上有人说会刷新页面,但是自己试了好像不会),试想我好不容易滚啊滚啊滚到xxxx这几个文字的地方,一不留神手贱点了一下,瞬间页面又回到顶部了,是不是要哭死😭。

所以常见的解决方法是

<div style="height: 10000px;"></div>
<a href="javascript:void(0)">xxxx</a>
<script>
  console.log('xxxx')
</script>
  1. What's the valid way to include an image with no src?,在这个问题中提到用void 0去替代image标签的空src属性会减少页面请求(是否属实有待考证)

结尾

第一篇暂时写完了,欢迎大家吐槽和提意见。

参考文章链接:

What does “javascript:void(0)” mean?

difference between “void 0 ” and “undefined”

void operator

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.