lessfish / underscore-analysis Goto Github PK
View Code? Open in Web Editor NEW【NO LONGER UPDATE】underscore-1.8.3.js 源码解读 & 系列文章(完)
License: MIT License
【NO LONGER UPDATE】underscore-1.8.3.js 源码解读 & 系列文章(完)
License: MIT License
前文 浅谈 Web 中前后端模板引擎的使用 我们简单了解了模板引擎在前后端的应用场景,本文重点深入 Underscore 的模板函数 _.template,来看看它的用法以及实现原理。
我们从 官方文档 中最简单的例子说起。
var compiled = _.template("hello: <%= name %>");
var html = compiled({name: 'moe'}); // hello: moe
{name: 'moe'} 模拟后台请求到的接口数据,而变量 html 则为拼接成的字符串,之后便可以用 innerHTML 方法加入到页面生成 DOM。
这一切是如何做到的?我们可以打印看下 compliled 方法是个什么样子(需要去 Underscore 源码中打印)。
大概是这个样子(其实不完全准确,真实的应该还会有个 _ 参数传入,使得函数能用 Underscore 内部方法):
function(obj){
var __t,__p='',__j=Array.prototype.join,print=function(){__p+=__j.call(arguments,'');};
with(obj||{}){
__p+='hello: '+
((__t=( name ))==null?'':__t)+
'';
}
return __p;
}
仔细想想,其实就是对模板字符串进行了正则解析,将需要填入数据的位置预留出来,拼接成一个字符串,用 new Function 构造一个方法(动态执行 JavaScript 字符串),方法中有大量的字符串拼接过程,然后将数据代入这个方法,返回我们需要的 HTML 字符串。
盗用 木神 两张图,过程非常清晰。
_.template 支持以下三种模板。
1. <% %> - to execute some code
2. <%= %> - to print some value in template
3. <%- %> - to print some values HTML escaped
<% %>
里包裹的是一些可执行的 JavaScript 语句,比如 if-else 语句,for 循环语句,等等。<%= %>
正是我们前面使用的,会打印传入数据相应的 key 的值,<%- %>
和前者相比,多了步 HTML 实体编码的过程,可以有效防止 XSS 攻击。
举个栗子:
<div></div>
<script src="underscore.js"></script>
<script type="text/template" id="tpl">
<ul class="list">
<% _.each(obj, function(e, i, a){ %>
<% if (i === 0) %>
<li><%- e.name %>
<% else if (i === a.length - 1) %>
<li class="last-item"><%= e.name %></li>
<% else %>
<li><%= e.name %></li>
<% }) %>
</ul>
</script>
<script>
// mock data
var data = [{name: "<script>"}, {name: "orange"}, {name: "peach"}];
var compiled = _.template(document.getElementById("tpl").innerHTML);
var html = compiled(data);
// console.log(html)
document.querySelector("div").innerHTML = html;
</script>
将数据用 li
标签循环展示,并且将第一个值实体编码了。
_.template 最基础的应用就是这样。
如果你不喜欢它默认的模板风格,也可以自己定义,注意 key 必须和源码中的 key 保持一致,才能覆盖。
_.templateSettings = {
// 三种渲染模板
evaluate : /<%([\s\S]+?)%>/g,
interpolate : /<%=([\s\S]+?)%>/g,
escape : /<%-([\s\S]+?)%>/g
};
有两种方式,一种是直接修改 _.templateSettings 变量(不推荐,修改了源码中的变量)
_.templateSettings = {
interpolate: /\{\{(.+?)\}\}/g
};
var template = _.template("Hello {{ name }}!");
var ans = template({name: "Mustache"});
console.log(ans); // Hello Mustache!
比较好的方法是作为 _.template 的第二个参数 settings 传入:
var settings = {
interpolate: /\{\{(.+?)\}\}/g // 覆盖 _.templateSettings.interpolate
};
var template = _.template("Hello {{ name }}!", settings);
var ans = template({name: "Mustache"});
console.log(ans); // Hello Mustache!
我们还能设定 settings.variable 指定 scope:
var template = _.template("Using 'with': <%= data.answer %>", {variable: 'data'})
var ans = template({answer: 'no'});
console.log(ans) // Using 'with': no
模板引擎一般都带有预编译功能,_.template 也不例外。
什么是预编译?有什么用?
上面的代码有两个痛点:
性能:模板引擎渲染的时候依赖 Function 构造器实现,Function 与 eval、setTimeout、setInterval 一样,提供了使用文本访问 javascript 解析引擎的方法,但这样执行 javascript 的性能非常低下。
调试:由于是动态执行字符串,若遇到错误调试器无法捕获错误源,导致模板 BUG 调试变得异常痛苦。在没有进行容错的引擎中,局部模板若因为数据异常甚至可以导致整个应用崩溃,随着模板的数目增加,维护成本将剧增。
如果我们 JavaScript 代码中直接保存 _.template 的结果,那么以上两个问题就不复存在。而 _.template(jstText).source
则保存了 _.template(jstText)
返回的方法字符串。
JST is a server-side thing, not client-side. This mean that you compile Unserscore template on server side by some server-side script and save the result in a file. Then use this file as compiled Unserscore template.
关于 _.template 方法的具体实现,可以参考楼主的 underscore-1.8.3.js 源码解读全文注释版 ,全局搜索即可。
关于前端模板引擎,其实楼主也是个初学者,学习过程中搜到的资料与大家分享下,有机会一定要用下各种模板引擎然后分析下。
(觉得这一段眼熟的童鞋可以直接跳到正文了...)
最近开始看 underscore.js 源码,并将 underscore.js 源码解读 放在了我的 2016 计划中。
阅读一些著名框架类库的源码,就好像和一个个大师对话,你会学到很多。为什么是 underscore?最主要的原因是 underscore 简短精悍(约 1.5k 行),封装了 100 多个有用的方法,耦合度低,非常适合逐个方法阅读,适合楼主这样的 JavaScript 初学者。从中,你不仅可以学到用 void 0 代替 undefined 避免 undefined 被重写等一些小技巧 ,也可以学到变量类型判断、函数节流&函数去抖等常用的方法,还可以学到很多浏览器兼容的 hack,更可以学到作者的整体设计思路以及 API 设计的原理(向后兼容)。
之后楼主会写一系列的文章跟大家分享在源码阅读中学习到的知识。
欢迎围观~ (如果有兴趣,欢迎 star & watch~)您的关注是楼主继续写作的动力
端午休息三天,睡了两天,是该有点产出了。
今天要讲的是数组展开以及和数组展开息息相关的一个重要的内部方法 flatten。
什么是数组展开?简单的说就是将嵌套的数组 "铺平",还是举几个简单的例子吧。
[[[1, 2], [1, 2, 3]], [1, 2]] => [1, 2, 1, 2, 3, 1, 2]
[[[1, 2], [1, 2, 3]], [1, 2]] => [[1, 2], [1, 2, 3], 1, 2]
以上两种都是数组展开,第一种我们认为是深度展开,即打破所有嵌套数组,将元素提取出来放入一个数组中;第二种只展开了一层,即只把数组内嵌套的一层数组展开,而没有递归展开下去。
我们首先来看看 flatten 方法的调用形式。
var flatten = function(input, shallow, strict, startIndex) {
// ...
};
第一个参数 input 即为需要展开的数组,所以 flatten 方法中传入的第一个参数肯定是数组(或者 arguments);第二个参数 shallow 是个布尔值,如果为 false,则表示数组是深度展开,如果为 true 则表示只展开一层;第四个参数表示 input 展开的起始位置,即从 input 数组中第几个元素开始展开。
var ans = flatten([[1, 2], [3, 4]], false, false, 1);
console.log(ans); // => [3, 4]
从第 1 项开始展开数组,即忽略了数组的第 0 项([1, 2])。
以上三个参数还是比较容易理解的,相对来说费劲的是第三个参数 strict。strict 也是个布尔值,当 shallow 为 true 并且 strict 也为 true 时,能过滤 input 参数元素中的非数组元素。好难理解啊!我们举个简单的例子。
var ans = flatten([5, 6, [1, 2], [3, 4]], true, true);
console.log(ans); // => [1, 2, 3, 4]
5 和 6 是 input 参数中的非数组元素,直接过滤掉了。如果 strict 为 true 并且 shallow 为 false,那么调用 flatten 方法的结果只能是 []。所以我们会看到源码里如果 strict 为 true,那么 shallow 也一定是 true。
直接来看源码,加了非常多的注释。
var flatten = function(input, shallow, strict, startIndex) {
// output 数组保存结果
// 即 flatten 方法返回数据
// idx 为 output 的累计数组下标
var output = [], idx = 0;
// 根据 startIndex 变量确定需要展开的起始位置
for (var i = startIndex || 0, length = getLength(input); i < length; i++) {
var value = input[i];
// 数组 或者 arguments
if (isArrayLike(value) && (_.isArray(value) || _.isArguments(value))) {
// flatten current level of array or arguments object
// (!shallow === true) => (shallow === false)
// 则表示需深度展开
// 继续递归展开
if (!shallow)
// flatten 方法返回数组
// 将上面定义的 value 重新赋值
value = flatten(value, shallow, strict);
// 递归展开到最后一层(没有嵌套的数组了)
// 或者 (shallow === true) => 只展开一层
// value 值肯定是一个数组
var j = 0, len = value.length;
// 这一步貌似没有必要
// 毕竟 JavaScript 的数组会自动扩充
// 但是这样写,感觉比较好,对于元素的 push 过程有个比较清晰的认识
output.length += len;
// 将 value 数组的元素添加到 output 数组中
while (j < len) {
output[idx++] = value[j++];
}
} else if (!strict) {
// (!strict === true) => (strict === false)
// 如果是深度展开,即 shallow 参数为 false
// 那么当最后 value 不是数组,是基本类型时
// 肯定会走到这个 else-if 判断中
// 而如果此时 strict 为 true,则不能跳到这个分支内部
// 所以 shallow === false 如果和 strict === true 搭配
// 调用 flatten 方法得到的结果永远是空数组 []
output[idx++] = value;
}
}
return output;
};
总的来说,就是持续递归调用 flatten,直到不能展开为止。给出 flatten 方法的实现源码位置 https://github.com/hanzichi/underscore-analysis/blob/master/underscore-1.8.3.js/src/underscore-1.8.3.js#L489-L507。
接着我们来看看源码中有用到这个内部方法的 API。
首先是 _.flatten 方法,非常简单,用了 flatten 的前三个参数。
_.flatten = function(array, shallow) {
// array => 需要展开的数组
// shallow => 是否只展开一层
// false 为 flatten 方法 strict 变量
return flatten(array, shallow, false);
};
前面说了,strict 为 true 只和 shallow 为 true 一起使用,所以没有特殊情况的话 strict 默认为 false。
_.union 方法同样用到了 flatten,这个方法的作用是传入多个数组,然后对数组元素去重。
var ans = _.union([[1]], [1, 2], 3, 4);
console.log(ans); // => [[1], 1, 2]
首先并不需要对数组深度展开,其次 _.union 传入的是数组,对于非数组元素可以直接忽略。这两点直接对应了 shallow 参数和 strict 参数均为 true(都不用做容错处理了)。对于一个数组的去重,最后调用 _.unique 即可。
_.union = function() {
// 首先用 flatten 方法将传入的数组展开成一个数组
// 然后就可以愉快地调用 _.uniq 方法了
// 假设 _.union([1, 2, 3], [101, 2, 1, 10], [2, 1]);
// arguments 为 [[1, 2, 3], [101, 2, 1, 10], [2, 1]]
// shallow 参数为 true,展开一层
// 结果为 [1, 2, 3, 101, 2, 1, 10, 2, 1]
// 然后对其去重
return _.uniq(flatten(arguments, true, true));
};
而 _.difference,_.pick,_.omit 方法,大家可以自己进源码去看,都大同小异,没什么特别要注意的点。(注意下 startIndex 参数即可)
对于内部方法 flatten,我要总结的是,可能某个内部方法会被多个 API 调用,如何设计地合理,优雅,如何兼顾到各种情况,真的需要强大的实践以及代码能力,这点还需要日后多加摸索。
这篇文章拖了有两周,今天来跟大家聊聊 JavaScript 中一类特殊的对象 -> Array-Like Objects。
JavaScript 中一切皆为对象,那么什么是 Array-Like Objects?顾名思义,就是像数组的对象,当然,数组本身就是对象嘛!稍微有点基础的同学,一定知道 arguments 就是 Array-Like Objects 的一种,能像数组一样用 []
去访问 arguments 的元素,有 length
属性,但是却不能用一些数组的方法,如 push,pop,等等。
那么,什么样的元素是 Array-Like Objects?我们来看看 underscore 中对其的定义。
var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1;
var getLength = property('length');
var isArrayLike = function(collection) {
var length = getLength(collection);
return typeof length == 'number' && length >= 0 && length <= MAX_ARRAY_INDEX;
};
很简单,不是数组,但是有 length
属性,且属性值为非负 Number 类型即可。至于 length
属性的值,underscore 给出了一个上限值 MAX_ARRAY_INDEX,其实是 MAX_SAFE_INTEGER(感谢 @HangYang 同学指出) ,因为这是 JavaScript 中能精确表示的最大数字。(数组最大长度应该为 Math.pow(2,32) - 1)
想想还有什么同时能满足以上条件的?NodeList,HTML Collections,仔细想想,甚至还有字符串,或者拥有 length
属性的对象,函数(length 属性值为形参数量),等等。
有的时候,需要将 Array-Like Objects 转为 Array 类型,使之能用数组的一些方法,一个非常简单粗暴并且兼容性良好的方法是新建个数组,然后循环存入数据。
我们以 arguments 为例。
function fn() {
// Uncaught TypeError: arguments.push is not a function
// arguments.push(4);
var arr = [];
for (var i = 0, len = arguments.length; i < len; i++)
arr[i] = arguments[i];
arr.push(4); // [1, 2, 3, 4]
}
fn(1, 2, 3);
但是这不是最优雅的,更优雅的解法大家一定都知道了,use Array.prototype.slice(IE9- 会有问题)。
function fn() {
var arr = Array.prototype.slice.call(arguments);
arr.push(4); // arr -> [1, 2, 3, 4]
}
fn(1, 2, 3);
或者可以用 []
代替 Array.prototype 节省几个字节。
function fn() {
var arr = [].slice.call(arguments);
arr.push(4); // arr -> [1, 2, 3, 4]
}
fn(1, 2, 3);
如果非得追求性能,用 []
会新建个数组,性能肯定不及前者,但是由于引擎的优化,这点差异基本可以忽略不计了(所以很多框架用的就是后者)。
为什么这样可以转换?我们简单了解下,主要的原因是 slice 方法只需要参数有 length 属性即可。首先,slice 方法得到的结果是一个 新的数组,通过 Array.prototype.slice.call 传入的参数(假设为 a),如果没有 length 属性,或者 length 属性值不是 Number 类型,或者为负,那么直接返回一个空数组,否则返回 a[0]-a[length-1] 组成的数组。(具体可以看下 v8 源码 https://github.com/v8/v8/blob/master/src/js/array.js#L621-L660)
当然,ES6 提供了更简便的方法。
var str = "helloworld";
var arr = Array.from(str);
// ["h", "e", "l", "l", "o", "w", "o", "r", "l", "d"]
小结下,如果要把 Array-Like Objects 转为 Array,首选 Array.prototype.slice,但是由于 IE 下 Array.prototype.slice.call(nodes) 会抛出错误(because a DOM NodeList is not a JavaScript object),所以兼容的写法如下。(但还有一点要注意的是,如果是 arguments 转为 Array,最好别用 Array.prototype.slice,V8 下会很慢,具体可以看下 避免修改和传递 arguments 给其他方法 — 影响优化 )
function nodeListToArray(nodes){
var arr, length;
try {
// works in every browser except IE
arr = [].slice.call(nodes);
return arr;
} catch(err){
// slower, but works in IE
arr = [];
length = nodes.length;
for(var i = 0; i < length; i++){
arr.push(nodes[i]);
}
return arr;
}
}
很多时候,某个方法你以为接收的参数是数组,其实类数组也是可以的。
Function.prototype.apply() 函数接收的第二个参数,其实也可以是类数组。
var obj = {0: 4, length: 2};
var arr = [1, 2, 3];
Array.prototype.push.apply(arr, obj);
console.log(arr); // [1, 2, 3, 4, undefined]
天哪原来的issue太暴露智商了...
一种极端情况,在谷歌浏览器中输入:
var abb = true
Object.prototype.toString().call(abb) // Object.prototype.toString(...).call is not a function
是不是可以表明
// 以及 new Boolean() 两个方向判断
// 有点多余了吧?
// 个人觉得直接用 toString.call(obj) 来判断就可以了
_.isBoolean = function(obj) {
return obj === true || obj === false || toString.call(obj) === '[object Boolean]';
};
不能直接用toString.call(obj) === '[object Boolean]'
作者:韩子迟
不知不觉间,「Underscore 源码解读系列」进入了真正的尾声,也请允许我最后一次 po 下项目的原始地址 https://github.com/hanzichi/underscore-analysis
这半年以来,花费了大量的业余时间,共计写了 25 篇随笔(包括此文),也给 underscore-1.8.3 的源码加了差不多 1500 行 注释,对于当初说的要做「史上最详细的 underscore 源码剖析」,至此我也觉得问心无愧。
本文不想说我在这个过程中学到了什么,学到的东西,能写的都在前面 24 篇随笔中了,只能意会的我也说不出来(感触最深的是闭包的强大)。本文是一篇「水文」,不谈技术,抛开代码,听楼主扯扯淡。
早期的几篇随笔都有「Why Underscore」这一节:
阅读一些著名框架类库的源码,就好像和一个个大师对话,你会学到很多。为什么是 Underscore?最主要的原因是 Underscore 简短精悍(约 1.5k 行),封装了 100 多个有用的方法,耦合度低,非常适合逐个方法阅读,适合楼主这样的 JavaScript 初学者。从中,你不仅可以学到用 void 0 代替 undefined 避免 undefined 被重写等一些小技巧 ,也可以学到变量类型判断、函数节流&函数去抖等常用的方法,还可以学到很多浏览器兼容的 hack,更可以学到作者的整体设计思路以及 API 设计的原理。
其实这并不是直接原因,「导火索」是当时(三月末)学习函数节流和函数去抖的时候,发现很多文章混淆了这两个概念,了解到 Underscore 封装了这两个函数,但是一用却发现了它的 BUG!(详见 一次发现underscore源码bug的经历以及对学术界『拿来主义』的思考 一文)
Underscore 早些年就接触过,对其的了解也仅限于「一个轻巧的工具库」,会不会还有其他的 BUG?同时源码这么短(之前想过看 jQuery,被接近 1w 行吓回来了),耦合度低(可以一个个方法分开看),也进一步激发了通篇解读的兴趣,想着这一路下来肯定能学到不少东西。好东西要与大家分享,同时确定了写系列文章的 idea。
哪里写?想都没想就确定了主战场 GitHub。之前我是不玩 GitHub 的,现在已经离不开了,每天不刷个几十次都手痒。为什么确定在 GitHub?理由很简单,GitHub 有逼格啊(毕竟是全球最大的同性交友社区)。
当然,酒香也怕巷子深,写完了没人看无疑是对自信心的极大打击,同时也需要大家来提出宝贵的意见。为此,几乎每一篇完成后,都会在以下渠道分享:
其实主要就是前三个,后面两个用了一段时间感觉不大好就不用了。极客头条个人感觉分类有点乱,同时因为是 CSDN 旗下的,个人对 CSDN 没太多好感;干货集中营的话,维护者是「daimajia」大大,感觉移动开发者关注的会比较多。
分享的话,如果脸皮厚的话,也可以在微博艾特一些圈内大大们(楼主脸皮比较薄 ...)。
当然,我不仅在 GitHub 的 issue 里写,还在以下一些渠道写:
简书的话,后面断断续续不更了,个人喜欢专门的 IT 类博客,而且觉得简书的 Markdown 渲染有问题,为什么都说它的 Markdown 体验好 ... sf 和 伯乐在线 的话,阅读量还是可以的,只是它们不是专门做博客系统的,基础的分类归档功能都没有(或许是我没找到?),以后应该还是会以博客园为主要战场。
underscore.js 的源码解读画上了句号,接下去干嘛?其实楼主对以下几个库也挺感兴趣的。
那就这样吧,749 Star / 140 Watch / 153 Fork 算是对我的肯定和鼓励吧 ^_^
也许我韩子迟还会回来的!
在导出模块中有这样的代码:
if (typeof exports !== 'undefined') {
if (typeof module !== 'undefined' && module.exports) {
exports = module.exports = _;
}
//无论上面这段if代码是否执行,最后都会执行下面这一步,那请问上面的if存在的意义是什么?
exports._ = _;
}
这里的注释是,兼容老的require API,搜索了相关的内容找不到合适的解释,还请指点一下!
最近开始看 underscore.js 源码,并将 underscore.js 源码解读 放在了我的 2016 计划中。
阅读一些著名框架类库的源码,就好像和一个个大师对话,你会学到很多。为什么是 underscore?最主要的原因是 underscore 简短精悍(约 1.5k 行),封装了 100 多个有用的方法,耦合度低,非常适合逐个方法阅读,适合楼主这样的 JavaScript 初学者。从中,你不仅可以学到用 void 0 代替 undefined 避免 undefined 被重写等一些小技巧 ,也可以学到变量类型判断、函数节流&函数去抖等常用的方法,还可以学到很多浏览器兼容的 hack,更可以学到作者的整体设计思路以及 API 设计的原理(向后兼容)。
之后楼主会写一系列的文章跟大家分享在源码阅读中学习到的知识。
欢迎围观~ (如果有兴趣,欢迎 star & watch~)您的关注是楼主继续写作的动力
趁着今天是工作日的最后一天,把源码解读部分 Object Functions 更新完毕。
如果你有心,可能就会发现楼主之前的解读系列文章说的都是 Object 上的扩展方法,也就是源码中 Object Functions 部分。underscore 为 5 种类型添加了扩展方法,分别是 Object -> Array -> Collection -> Function -> Utility,这也正是楼主的源码解读顺序(并不是源码顺序)。其中,Object 上的扩展方法多达 38 个,方法多并不代表代码多,比如类型检测,两行代码就可以搞定好几个方法,而上一篇中说的 _.isEqual 方法,却要百来行去实现。今天是 Object Functions 部分的最后一篇,我们来看看楼主认为的几个没被解读过的但是却有意思的方法的源码。(其实很多方法使用简单,实现也非常简单,有兴趣的同学可以自己扒下源码)
首先来看看 _.pick 方法,该方法传入一个对象,然后删选对象的键值对,返回一个对象副本。
直接来看例子:
_.pick({name: 'moe', age: 50, userid: 'moe1'}, 'name', 'age');
=> {name: 'moe', age: 50}
_.pick({name: 'moe', age: 50, userid: 'moe1'}, ['name', 'age']);
=> {name: 'moe', age: 50}
_.pick({name: 'moe', age: 50, userid: 'moe1'}, function(value, key, object) {
return _.isNumber(value);
});
=> {age: 50}
一目了然,第一个参数 obj 是对象,第二个参数可以是一系列的 key 值,也可以是数组(数组中含 key),也可以是迭代函数,我们根据 key 值,或者迭代函数来过滤 obj 中的键值对,返回新的对象副本。
如果让我来设计,估计会根据参数来判断类型,然后写几个 if-else,每个 if-else 分支里的内容毫无关联。但是 underscore 的写法简直美妙,将几种情况转为了一种。
// 如果第二个参数是函数
if (_.isFunction(oiteratee)) {
keys = _.allKeys(obj);
iteratee = optimizeCb(oiteratee, context);
}
首先 if-else 是不可避免的,如果传入的第二个参数是 function,那么就是传入迭代函数了,根据 context(this)返回新的迭代函数(optimizeCb 我以后会讲,就是规定了迭代函数中的 this 指向,不是很重要,这里可以选择性忽略)。
如果第二个参数不是函数,则后面的 keys 可能是数组,也可能是连续的几个并列的参数。这里我们要用到 underscore 中另一个重要的内部方法,flatten,它的作用是将嵌套的数组展开,这个方法我以后会分析,这里知道它的作用就可以了。
else {
// 如果第二个参数不是函数
// 则后面的 keys 可能是数组
// 也可能是连续的几个并列的参数
keys = flatten(arguments, false, false, 1);
// 也转为 predicate 函数判断形式
// 将指定 key 转化为 predicate 函数
iteratee = function(value, key, obj) { return key in obj; };
obj = Object(obj);
}
也转为和传入迭代函数一样的形式,就可以用一个方法判断了,而且 keys 变量在两种情况下的意义是不同的,真的非常巧妙。这点令我思考良多,很多时候的代码冗余,其实大概是自己代码能力太差了吧,打死我也想不到这样做。
_.create 方法非常简单,根据你给的原型(prototype),以及一些 own properties,构造新的对象返回。
举个简单的例子:
var Person = function() {};
Person.prototype = {
show: function() {
alert(this.name);
}
};
var me = _.create(Person.prototype, {name: 'hanzichi'});
console.log(me);
// Object {name: "hanzichi"}
// name: "hanzichi"
// __proto__: Object
// show: function()
其实 me 变量就是一个拥有 name 作为 own properties,且用 Person 函数构造的对象。
如果浏览器支持 ES5,我们可以用 Object.create():
var Person = function() {};
Person.prototype = {
show: function() {
alert(this.name);
}
};
var me = Object.create(Person.prototype);
_.extendOwn(me, {name: 'hanzichi'});
console.log(me);
如果不支持 ES5,我们可以新定义一个构造函数,将该构造函数的 prototype 赋值为已知的 prototype 变量,然后用 new 运算符来获取实例:
var Person = function() {};
Person.prototype = {
show: function() {
alert(this.name);
}
};
var _Person = function() {};
_Person.prototype = Person.prototype;
var me = new _Person();
_.extendOwn(me, {name: 'hanzichi'});
console.log(me);
undercore 的实现思路也大抵如此。
本来打算把 _.tap 讲掉,突然觉得跟链式调用一起讲比较好,挖个坑。
关于 Objects Function 部分的源码剖析就到这了,具体这部分代码可以参考 https://github.com/hanzichi/underscore-analysis/blob/master/underscore-1.8.3.js/src/underscore-1.8.3.js#L901-L1269。虽然说看完了这部分代码,但是要真正理解消化还是需要时间的,我只能说自己理解了 90% 左右,欢迎探讨交流。
接下去应该会着手看 Array 扩展方法相关代码,继续为大家整理有意思的东西,各种奇淫怪巧,敬请期待。
真是天一热什么事都不想干,这个月只产出了一篇文章,赶紧写一篇压压惊!
前文(#15)说到楼主开始解读 underscore.js 中的 Collection Functions 部分,看了一遍这部分的源码,很多方法都是一看便懂,不需要楼主过分解读。本文来聊聊内部方法 group 的设计,这会是 Collection Functions 部分的第二篇文章,也是最后一篇文章,关于这部分其他方法的实现,可以看我的 全文源码注释。
group 是 underscore.js 中的一个内部方法,顾名思义,为了分组而用。有三个 API 用到了这个方法,分别是 .groupBy,.indexBy,_.countBy。
来看看这三个 API 的作用。
_.groupBy,可以对一个数组的元素进行分组,如何分组?可以将元素传入一个迭代函数,根据迭代后的值进行分组,也可以传入一个字符串表示元素属性,根据该属性值进行分组。
_.groupBy([1.3, 2.1, 2.4], function(num){ return Math.floor(num); });
=> {1: [1.3], 2: [2.1, 2.4]}
_.groupBy(['one', 'two', 'three'], 'length');
=> {3: ["one", "two"], 5: ["three"]}
可以看到,返回的结果是一个对象,key 为经过迭代函数迭代后的值,或者属性值,value 为一个数组,保存迭代结果或者属性值一样的元素。
再来看 _.indexBy。
var stooges = [{name: 'moe', age: 40}, {name: 'larry', age: 50}, {name: 'curly', age: 60}];
_.indexBy(stooges, 'age');
=> {
"40": {name: 'moe', age: 40},
"50": {name: 'larry', age: 50},
"60": {name: 'curly', age: 60}
}
跟 .groupBy 差不多,不同的是,.indexBy 的结果,每个 key 值对应的是一个元素(传入 _.indexBy 方法的数组中的元素),而不是一个数组(Just like groupBy, but for when you know your keys are unique.)。如果原来数组中,有两个元素,经过迭代后(或者元素属性值)相同,那么在结果对象中,后者会覆盖前者。(所以最好确认数组中的元素经过迭代后的值没有相同的,或者属性值没有相同的)
var tmp = _.indexBy(['one', 'two', 'three'], 'length');
=> Object {3: "two", 5: "three"}
最后来看 _.countBy。还是返回一个结果对象,它的 key 值意思还是和 _.groupBy 以及 _.indexBy 相同,而 value 值为迭代结果(或者属性值)是该 key 值的元素的个数。
_.countBy([1, 2, 3, 4, 5], function(num) {
return num % 2 == 0 ? 'even': 'odd';
});
=> {odd: 3, even: 2}
这三个 API 功能相近,难道要写三个独立的方法?如果独立写,大概会是这样。
_.groupBy = function(obj, iteratee, context) {
// 返回结果是一个对象
var result = {};
// 根据 iteratee 值确定迭代函数
iteratee = cb(iteratee, context);
// 遍历元素
_.each(obj, function(value, index) {
// 经过迭代,获取结果值,存为 key
var key = iteratee(value, index, obj);
// TODO
// ...
});
// 返回结果对象
return result;
};
_.indexBy = function(obj, iteratee, context) {
// 返回结果是一个对象
var result = {};
// 根据 iteratee 值确定迭代函数
iteratee = cb(iteratee, context);
// 遍历元素
_.each(obj, function(value, index) {
// 经过迭代,获取结果值,存为 key
var key = iteratee(value, index, obj);
// TODO
// ...
});
// 返回结果对象
return result;
};
_.countBy = function(obj, iteratee, context) {
// 返回结果是一个对象
var result = {};
// 根据 iteratee 值确定迭代函数
iteratee = cb(iteratee, context);
// 遍历元素
_.each(obj, function(value, index) {
// 经过迭代,获取结果值,存为 key
var key = iteratee(value, index, obj);
// TODO
// ...
});
// 返回结果对象
return result;
};
大堆功能相似的代码,简直不能忍!每当这个时候,就要想到闭包,函数嵌套函数!
首先定义个函数 group,返回一个函数,为以上三个方法能调用的函数。听起来有点拗口,其实就是用 group 做个中间层,上代码体会下。
var group = function() {
return function(obj, iteratee, context) {
// 返回结果是一个对象
var result = {};
// 根据 iteratee 值确定迭代函数
iteratee = cb(iteratee, context);
// 遍历元素
_.each(obj, function(value, index) {
// 经过迭代,获取结果值,存为 key
var key = iteratee(value, index, obj);
// TODO
// ...
});
// 返回结果对象
return result;
};
};
_.groupBy = group();
_.indexBy = group();
_.countBy = group();
其实三个方法主要的操作,就是对上面的 group 中 result 对象的操作,我们可以传入一个方法,利用该方法对 result 对象进行操作。
以 _.groupBy 方法为例:
var group = function(behavior) {
return function(obj, iteratee, context) {
// 返回结果是一个对象
var result = {};
// 根据 iteratee 值确定迭代函数
iteratee = cb(iteratee, context);
// 遍历元素
_.each(obj, function(value, index) {
// 经过迭代,获取结果值,存为 key
var key = iteratee(value, index, obj);
// operate
behavior(result, value, key);
});
// 返回结果对象
return result;
};
};
var behavior = function(result, value, key) {
if (_.has(result, key))
result[key].push(value);
else result[key] = [value];
}
_.groupBy = group(behavior);
将对象当做参数传入函数,能在函数内改变对象值,正是利用了这个特点。而 _.indexBy 和 _.countBy,无非是改一下 behavior 函数的事了。
酱油了一篇解读,下文开始讲扩展函数了,其实最开始有解读 underscore 的欲望,正是因为函数部分的节流和去抖。
也是好久没更新 源码解读,看着房价蹭蹭暴涨,心里也是五味杂陈,对未来充满恐惧和迷茫 ...(敢问一句你们上岸了吗)
言归正传,今天要介绍的是 underscore 中两个重要的方法,函数节流和函数去抖。这篇文章不会涉及具体的代码实现(关于代码实现请期待下文),会从零开始介绍函数节流和函数去抖的概念,辨析应用场景。为什么我对这两个方法情有独钟要花大篇幅去介绍?因为就是它们带我入了「underscore 源码解读」的坑(详见 一次发现underscore源码bug的经历以及对学术界『拿来主义』的思考)。
函数节流和去抖的出现场景,一般都伴随着客户端 DOM 的事件监听。举个例子,实现一个原生的拖拽功能(不能用 H5 Drag&Drop API),需要一路监听 mousemove 事件,在回调中获取元素当前位置,然后重置 dom 的位置(样式改变)。如果我们不加以控制,每移动一定像素而触发的回调数量是会非常惊人的,回调中又伴随着 DOM 操作,继而引发浏览器的重排与重绘,性能差的浏览器可能就会直接假死,这样的用户体验是非常糟糕的。我们需要做的是降低触发回调的频率,比如让它 500ms 触发一次,或者 200ms,甚至 100ms,这个阈值不能太大,太大了拖拽就会失真,也不能太小,太小了低版本浏览器可能就会假死,这样的解决方案就是函数节流,英文名字叫「throttle」。函数节流的核心是,让一个函数不要执行得太频繁,减少一些过快的调用来节流。
说完函数节流,再看它的好基友函数去抖(debounce)。思考这样一个场景,对于浏览器窗口,每做一次 resize 操作,发送一个请求,很显然,我们需要监听 resize 事件,但是和 mousemove 一样,每缩小(或者放大)一次浏览器,实际上会触发 N 多次的 resize 事件,用节流?节流只能保证定时触发,我们一次就好,这就要用去抖。简单的说,函数去抖就是对于一定时间段的连续的函数调用,只让其执行一次。
函数节流有哪些应用场景?哪些时候我们需要间隔一定时间触发回调来控制函数调用频率?
函数去抖有哪些应用场景?哪些时候对于连续的事件响应我们只需要执行一次回调?
函数节流和函数去抖的核心其实就是限制某一个方法被频繁触发,而一个方法之所以会被频繁触发,大多数情况下是因为 DOM 事件的监听回调,而这也是函数节流以及去抖多数情况下的应用场景。至于函数节流和去抖方法的具体代码实现以及调用方式,下文我们再做分享。
最近开始看 underscore.js 源码,并将 underscore.js 源码解读 放在了我的 2016 计划中。
阅读一些著名框架类库的源码,就好像和一个个大师对话,你会学到很多。为什么是 underscore?最主要的原因是 underscore 简短精悍(约 1.5k 行),封装了 100 多个有用的方法,耦合度低,非常适合逐个方法阅读,适合楼主这样的 JavaScript 初学者。从中,你不仅可以学到用 void 0 代替 undefined 避免 undefined 被重写等一些小技巧 ,也可以学到变量类型判断、函数节流&函数去抖等常用的方法,还可以学到很多浏览器兼容的 hack,更可以学到作者的整体设计思路以及 API 设计的原理(向后兼容)。
之后楼主会写一系列的文章跟大家分享在源码阅读中学习到的知识。
欢迎围观~ (如果有兴趣,欢迎 star & watch~)您的关注是楼主继续写作的动力
很快,Array Functions 部分到了尾声,今天来做个了(xiao)结。
underscore 给数组(以及 arguments,这里特别说明下,underscore 的数组扩展方法,同样适用于 arguments)增加了 20 个扩展方法,值得一提的是,很多有意思的方法,比如 map,shuffle 等,都被放在了 Collection Functions 中。本文来看看 Array Functions 中还有哪些有意思的方法(之前没有被提及)。
这个方法很有意思,它的作用是剔除数组中的假值,返回数组副本。
实现非常的简单:
_.compact = function(array) {
return _.filter(array, _.identity);
};
_.filter 我们在以后会讲到,这里你可以把它理解为 Array.prototype.filter 的一个 polyfill,来看看 _.identity 是个什么东东。
_.identity = function(value) {
return value;
};
乍一看,_.identity 似乎没什么卵用,传入一个参数,原封不动返回这个参数,什么鬼?而再看 _.compact 的实现,就会发现非常巧妙!细细品味下,直接过滤了数组的假值,而 _.identity 在源码中能在多个地方复用。
从这个方法可以想到 PHP 的 array_filter 函数。array_filter 的基本用法和 Array.prototype.filter 相似,都是为了过滤数组中的元素。
function isOdd($num) {
return $num & 1;
}
$a = Array(1, 2, 3);
$a = array_filter($a, 'isOdd');
var_dump($a);
// array
// 0 => int 1
// 2 => int 3
但是,值得注意的是:
If no callback is supplied, all entries of array equal to FALSE (see converting to boolean) will be removed.
这就有点 6 了,直接把 _.filter 和 _.compact 两个方法合二为一了。
$a = Array(0, 1, 2, 3, null, false, 4);
$a = array_filter($a);
var_dump($a);
// array
// 1 => int 1
// 2 => int 2
// 3 => int 3
// 6 => int 4
Array.prototype.filter 为何不设计成这样呢?没有 callback 传入的时候,直接过滤假值...
先来看 _.without,它的作用是从数组中剔除指定的元素。
var a = [1, 2, 3, 4, 5];
var ans = _.without(a, 1, 2, 3);
console.log(ans); // [4, 5]
恩,没错,剔除数组 a 中的 value 为 1, 2, 3 的元素,这个过程中用 ===
来进行比较。该方法传入的第一个参数是数组,后面的参数为单个元素。
而 _.difference 呢?和 _.without 的唯一区别是,第二个参数开始传入的是数组。(分别和数组中的元素比较)
var a = [1, 2, 3, 4, 5];
var ans = _.difference(a, [1, 2, 3], [5, 6]);
console.log(ans); // [4]
从 a 数组中剔除 1,2,3,5,6。
仔细一想,如果已经实现了 _.difference,我们把 _.without 的参数放入数组,然后传入 _.difference 就 ok 了!倒过来就不行了(思考下为什么)。 倒过来也是可以实现的,不过会相对比较繁琐些,详见 comments 部分的讨论,感谢 WangBoxue 同学指出~
来看 _.difference 的实现,非常简单:
// _.difference(array, *others)
_.difference = function(array) {
// 将 others 数组展开一层
// rest[] 保存展开后的元素组成的数组
// strict 参数为 true
// 不可以这样用 _.difference([1, 2, 3, 4, 5], [5, 2], 10);
// 10 就会取不到
var rest = flatten(arguments, true, true, 1);
// 遍历 array,过滤
return _.filter(array, function(value){
// 如果 value 存在在 rest 中,则过滤掉
return !_.contains(rest, value);
});
};
不熟悉 flatten 的可以看看 前文,当 shallow 和 strict 均为 true 时,展开一层,并且过滤非数组元素,即可以起到将多个数组合并的作用。之后利用 ._filter 进行过滤即可。
而 _.without 方法则建立在 _.difference 基础上。
_.without = function(array) {
// slice.call(arguments, 1)
// 将 arguments 转为数组(同时去掉第一个元素)
// 之后便可以调用 _.difference 方法
return _.difference(array, slice.call(arguments, 1));
};
数组的扩展方法就解读到这里了,相关源码可以参考 https://github.com/hanzichi/underscore-analysis/blob/master/underscore-1.8.3.js/src/underscore-1.8.3.js#L450-L693 这部分。接下去要解读的是 Collection Functions 部分,所谓 Collection,正是 Object & Array,也就是说这部分方法既可以用于 Object 也能用于 Array,比如我们熟悉的 map,filter,shuffle 等等,都在这部分内。
放个预告,下一篇会暂缓下 Collection Functions,讲下 array-like 相关的东西,敬请期待。
PS:坚持一件事真的挺难,一个月来,每天坚持看点源码,几乎把所有业余时间花在了上面,写了 10 篇随笔,每篇文章写的时间不短,关键还需要构思,如何提炼出一个主题,如何写让人看了会有所收获,恩,继续坚持。请关注我的 Repo https://github.com/hanzichi/underscore-analysis 支持我~
我在拜讀您整體的underscore源碼中提到了数组或者对象的扩展方法 共 25 个扩展方法
.我覺得對於其中的一些方法不是已經存在了嗎?
那麼重寫的原因是在出underscore.js之前瀏覽器不支持es5的一些方法嗎?
终于,楼主的「Underscore 源码解读系列」underscore-analysis 即将进入尾声,关注下 timeline 会发现楼主最近加快了解读速度。十一月,多事之秋,最近好多事情搞的楼主心力憔悴,身心俱疲,也想尽快把这个系列完结掉,也好了却一件心事。
本文预计是解读系列的倒数第二篇,最后一篇那么显然就是大总结了。楼主的 Underscore 系列解读完整版地址 https://github.com/hanzichi/underscore-analysis
之前写的文章,关注点大多在具体的方法,具体的知识细节,也有读者留言建议楼主讲讲整体架构,这是必须会讲的,只是楼主把它安排在了最后,也就是本文,因为楼主觉得不掌握整体架构对于具体方法的理解也是没有大的问题的。
Underscore 大多数时候的调用形式为 _.funcName(xx, xx)
,这也是 文档中 的调用方式。
_.each([1, 2, 3], alert);
最简单的实现方式,我们可以把 _
看做一个简单的对象:
var _ = {};
_.each = function() {
// ...
};
在 JavaScript 中,一切皆对象,实际上,源码中的 _
变量是一个方法:
var _ = function(obj) {
if (obj instanceof _) return obj;
if (!(this instanceof _)) return new _(obj);
this._wrapped = obj;
};
为什么会是方法?我们接下去看。
Underscore 支持 OOP 形式的调用:
_([1, 2, 3]).each(alert);
这其实是非常经典的「无 new 构造」,_
其实就是一个 构造函数,_([1, 2, 3])
的结果就是一个对象实例,该实例有个 _wrapped
属性,属性值是 [1, 2, 3]
。实例要调用 each
方法,其本身没有这个方法,那么应该来自原型链,也就是说 _.prototype
上应该有这个方法,那么,方法是如何挂载上去的呢?
现在我们已经明确以下两点:
_
是一个函数(支持无 new 调用的构造函数)_
的属性有很多方法,比如 _.each
,_.template
等等我们的目标是让 _
的构造实例也能调用这些方法。仔细想想,其实也不难,我们可以遍历 _
上的属性,如果属性值类型是函数,那么就将函数挂到 _
的原型链上去。
源码中用来完成这件事的是 _.mixin
方法:
// Add your own custom functions to the Underscore object.
// 可向 underscore 函数库扩展自己的方法
// obj 参数必须是一个对象(JavaScript 中一切皆对象)
// 且自己的方法定义在 obj 的属性上
// 如 obj.myFunc = function() {...}
// 形如 {myFunc: function(){}}
// 之后便可使用如下: _.myFunc(..) 或者 OOP _(..).myFunc(..)
_.mixin = function(obj) {
// 遍历 obj 的 key,将方法挂载到 Underscore 上
// 其实是将方法浅拷贝到 _.prototype 上
_.each(_.functions(obj), function(name) {
// 直接把方法挂载到 _[name] 上
// 调用类似 _.myFunc([1, 2, 3], ..)
var func = _[name] = obj[name];
// 浅拷贝
// 将 name 方法挂载到 _ 对象的原型链上,使之能 OOP 调用
_.prototype[name] = function() {
// 第一个参数
var args = [this._wrapped];
// arguments 为 name 方法需要的其他参数
push.apply(args, arguments);
// 执行 func 方法
// 支持链式操作
return result(this, func.apply(_, args));
};
});
};
// Add all of the Underscore functions to the wrapper object.
// 将前面定义的 underscore 方法添加给包装过的对象
// 即添加到 _.prototype 中
// 使 underscore 支持面向对象形式的调用
_.mixin(_);
_.mixin 方法可以向 Underscore 库增加自己定义的方法:
_.mixin({
capitalize: function(string) {
return string.charAt(0).toUpperCase() + string.substring(1).toLowerCase();
}
});
_("fabio").capitalize();
=> "Fabio"
同时,Underscore 也加入了一些 Array 原生的方法:
// Add all mutator Array functions to the wrapper.
// 将 Array 原型链上有的方法都添加到 underscore 中
_.each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) {
var method = ArrayProto[name];
_.prototype[name] = function() {
var obj = this._wrapped;
method.apply(obj, arguments);
if ((name === 'shift' || name === 'splice') && obj.length === 0)
delete obj[0];
// 支持链式操作
return result(this, obj);
};
});
// Add all accessor Array functions to the wrapper.
// 添加 concat、join、slice 等数组原生方法给 Underscore
_.each(['concat', 'join', 'slice'], function(name) {
var method = ArrayProto[name];
_.prototype[name] = function() {
return result(this, method.apply(this._wrapped, arguments));
};
});
Underscore 也支持链式调用:
// 非 OOP 链式调用
_.chain([1, 2, 3])
.map(function(a) {return a * 2;})
.reverse()
.value(); // [6, 4, 2]
// OOP 链式调用
_([1, 2, 3])
.chain()
.map(function(a){return a * 2;})
.first()
.value(); // 2
乍一看似乎有 OOP 和非 OOP 两种链式调用形式,其实只是一种,_.chain([1, 2, 3])
和 _([1, 2, 3]).chain()
的结果是一样的。如何实现的?我们深入 chain
方法看下。
_.chain = function(obj) {
// 无论是否 OOP 调用,都会转为 OOP 形式
// 并且给新的构造对象添加了一个 _chain 属性
var instance = _(obj);
// 标记是否使用链式操作
instance._chain = true;
// 返回 OOP 对象
// 可以看到该 instance 对象除了多了个 _chain 属性
// 其他的和直接 _(obj) 的结果一样
return instance;
};
我们看下 _.chain([1, 2, 3])
的结果,将参数代入函数中,其实就是对参数进行无 new 构造,然后返回实例,只是实例多了个 _chain
属性,其他的和直接 _([1, 2, 3])
一模一样。再来看 _([1, 2, 3]).chain()
,_([1, 2, 3])
返回构造实例,该实例有 chain
方法,调用方法,为实例添加 _chain
属性,返回该实例对象。所以,这两者效果是一致的,结果都是转为了 OOP 的形式。
说了这么多,似乎还没讲到正题上,它是如何「链」下去的?我们以如下代码为例:
_([1, 2, 3])
.chain()
.map(function(a){return a * 2;})
.first()
.value(); // 2
当调用 map
方法的时候,实际上可能会有返回值。我们看下 _.mixin
源码:
// 执行 func 方法
// 支持链式操作
return result(this, func.apply(_, args));
result
是一个重要的内部帮助函数(Helper function ):
// Helper function to continue chaining intermediate results.
// 一个帮助方法(Helper function)
var result = function(instance, obj) {
// 如果需要链式操作,则对 obj 运行 chain 方法,使得可以继续后续的链式操作
// 如果不需要,直接返回 obj
return instance._chain ? _(obj).chain() : obj;
};
如果需要链式操作(实例会有带有 _chain 属性),则对运算结果调用 chain
函数,使之可以继续链式调用。
Underscore 整体架构,或者说是基础实现大概就是这个样子,代码部分就讲到这了,接下去系列解读最后一篇,讲讲这段时间(几乎也是历时半年了)的一些心得体会吧,没钱的就捧个人场吧!
最近开始看 underscore.js 源码,并将 underscore.js 源码解读 放在了我的 2016 计划中。
阅读一些著名框架类库的源码,就好像和一个个大师对话,你会学到很多。为什么是 underscore?最主要的原因是 underscore 简短精悍(约 1.5k 行),封装了 100 多个有用的方法,耦合度低,非常适合逐个方法阅读,适合楼主这样的 JavaScript 初学者。从中,你不仅可以学到用 void 0 代替 undefined 避免 undefined 被重写等一些小技巧 ,也可以学到变量类型判断、函数节流&函数去抖等常用的方法,还可以学到很多浏览器兼容的 hack,更可以学到作者的整体设计思路以及 API 设计的原理(向后兼容)。
之后楼主会写一系列的文章跟大家分享在源码阅读中学习到的知识。
欢迎围观~ (如果有兴趣,欢迎 star & watch~)您的关注是楼主继续写作的动力
本文跟大家聊聊 JavaScript 中如何判断两个参数 "相同",即 underscore 源码中的 _.isEqual 方法。这个方法可以说是 underscore 源码中实现最复杂的方法(用了百来行),几乎没有之一。
那么,我说的 "相同" 到底是什么意思?举个栗子,1
和 new Number(1)
被认为是 equal,[1]
和 [1]
被认为是 equal(尽管它们的引用并不相同),当然,两个引用相同的对象肯定是 equal 的了。
那么,如何设计这个 _.isEqual 函数呢?我们跟着 underscore 源码,一步步来看它的实现。后文中均假设比较的两个参数为 a 和 b。
首先我们判断 a === b
,为 true 的情况有两种,其一是 a 和 b 都是基本类型,那么就是两个基本类型的值相同,其二就是两个引用类型,那么就是引用类型的引用相同。那么如果 a === b
为 true,是否就是说 a 和 b 是 equal 的呢?事实上,99% 的情况是这样的,还得考虑 0 和 -0 这个 special case,0 === -0
为 true,而 0 和 -0 被认为是 unequal,至于原因,可以参考 http://wiki.ecmascript.org/doku.php?id=harmony:egal。
这部分代码可以这样表示:
// Identical objects are equal. `0 === -0`, but they aren't identical.
// See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal).
// a === b 时
// 需要注意 `0 === -0` 这个 special case
// 0 和 -0 不相同
// 至于原因可以参考上面的链接
if (a === b) return a !== 0 || 1 / a === 1 / b;
接下去的情况,也就是 a !== b
的情况了。
如果 a 和 b 中有一个是 null 或者 undefined,那么可以特判下,不用继续比较了。源码实现:
// A strict comparison is necessary because `null == undefined`.
// 如果 a 和 b 有一个为 null(或者 undefined)
// 判断 a === b
if (a == null || b == null) return a === b;
个人觉得这里写的有点多余,因为根据上面的判断过滤,a === b 肯定是返回 false 的。
ok,我们继续,接下来我们可以先根据 a 和 b 的类型来判断,如果类型不一样,那么就没必要继续判断了。如何获取变量类型?没错,就是神奇的 Object.prototype.toString.call
!
如果类型是 RegExp 和 String,我们可以将 a 和 b 分别转为字符串进行比较(如果是 String 就已经是字符串了),举个栗子:
var a = /a/;
var b = new RegExp("a");
console.log(_.isEqual(a, b)); // => true
其实它在 underscore 内部是这样判断的:
var a = /a/;
var b = new RegExp("a");
var _a = '' + a; // => /a/
var _b = '' + b; // => /a/
console.log(_a === _b); // => true
如果是 Number 类型呢?这里又有个 special case,就是 NaN!这里规定,NaN 仅和 NaN 相同,与别的 Number 类型均 unequal。这里我们将引用类型均转为基本类型,看如下代码:
var a = new Number(1);
console.log(+a); // 1
没错,加个 +
就解决了,其他的不难理解,都在注释里了。
// `NaN`s are equivalent, but non-reflexive.
// Object(NaN) is equivalent to NaN
// 如果 +a !== +a
// 那么 a 就是 NaN
// 判断 b 是否也是 NaN 即可
if (+a !== +a) return +b !== +b;
// An `egal` comparison is performed for other numeric values.
// 排除了 NaN 干扰
// 还要考虑 0 的干扰
// 用 +a 将 Number() 形式转为基本类型
// 如果 a 为 0,判断 1 / +a === 1 / b
// 否则判断 +a === +b
return +a === 0 ? 1 / +a === 1 / b : +a === +b;
// 如果 a 为 Number 类型
// 要注意 NaN 这个 special number
// NaN 和 NaN 被认为 equal
接下来我们看 Date 和 Boolean 两个类型。跟 Number 类型相似,它们也可以用 +
转化为基本类型的数字!看下面代码:
var a = new Date();
var b = true;
var c = new Boolean(false);
console.log(+a); // 1464180857222
console.log(+b); // 1
console.log(+c); // 0
非常简单,其实 +new Date() (或者也可以写成 +new Date)获取的正是当前时间和 1970 年 1 月 1 日 0 点的毫秒数(millisecond),可能你听说过时间戳,其实时间戳就是这个数据除以 1000,也就是秒数。在用 canvas 做动画时,我经常用 +new Date 来当时间戳。
so,如果 a 和 b 均是 Date 类型或者 Boolean 类型,我们可以用 +a === +b
来判断是否 equal。
程序接着走,我们接着看,似乎还有两类重要的类型没有判断?没错,Array 和 Object!underscore 对此采用递归方法展开来比较。
还是举个栗子吧,举例比较直观。
假设 a,b 如下:
var a = {name: "hanzichi", loveCity: [{cityName: "hangzhou", province: "zhenjiang"}], age: 30};
var b = {name: "hanzichi", loveCity: [{cityName: "hangzhou", province: "zhenjiang"}], age: 25};
首先 a,b 是对象,我们可以分别比较其键值对,如果有一个键值对不同(或者说一个键值对 a 和 b 有一个没有),则 a 和 b unequal。如果是数组呢?那就一个一个元素比较喽。因为数组可能嵌套对象,对象的 value 又可能是数组,所以这里用了递归。
还是以上面的例子,我们可以把它拆成三次比较,分别比较三个 key 的 value 值是否相同。对于 loveCity 这个 key 的 value,因为其 value 又是个数组,所以我们将这个 value 传入比较函数,通过这个比较的结果,来判断最后的比较结果。递归就是这样,可以将大的东西,拆成一个个小的,根据小的结果,来汇总得到大的结果。
最后,给出代码位置。关于 _.isEqual 方法的源码,大家可以参考 https://github.com/hanzichi/underscore-analysis/blob/master/underscore-1.8.3.js/src/underscore-1.8.3.js#L1094-L1190
最近开始看 underscore.js 源码,并将 underscore.js 源码解读 放在了我的 2016 计划中。
阅读一些著名框架类库的源码,就好像和一个个大师对话,你会学到很多。为什么是 underscore?最主要的原因是 underscore 简短精悍(约 1.5k 行),封装了 100 多个有用的方法,耦合度低,非常适合逐个方法阅读,适合楼主这样的 JavaScript 初学者。从中,你不仅可以学到用 void 0 代替 undefined 避免 undefined 被重写等一些小技巧 ,也可以学到变量类型判断、函数节流&函数去抖等常用的方法,还可以学到很多浏览器兼容的 hack,更可以学到作者的整体设计思路以及 API 设计的原理(向后兼容)。
之后楼主会写一系列的文章跟大家分享在源码阅读中学习到的知识。
欢迎围观~ (如果有兴趣,欢迎 star & watch~)您的关注是楼主继续写作的动力
(PS:本文有点水,没多少干货,主要想跟大家分享下这里的闭包)
今天要跟大家聊的是 underscore 源码中一个重要的内部方法,createAssigner。
这个方法是用来干嘛的呢?该方法涉及的 api 包括 _.extend & _.extendOwn & _.defaults,那么这三个 api 又是用来干嘛的呢?ok,先简单介绍下这三个 api 的用处。
首先思考这样一个场景,有 a,b 两个对象,将 b 所有的键值对都添加到 a 上面去,返回 a,如何写这个方法?恩,应该不难,刷刷刷写下如下代码:
function extend(a, b) {
for (var key in b)
a[key] = b[key];
return a;
}
我擦,这么少,这是真的吗?是真的,除了没有兼容 IE < 9 下某些 key 不能被 for ... in 枚举到的 bug(这个可以参考 前文)。你不禁开始怀疑人生,讲这个有毛的意义?
事实上,_.extend 大概就是用来干上面 extend 函数的事情的;而 _.extendOwn 则只会取 b 对象的 own properties,大同小异; _.defaults 呢?跟 _.extend 类似,但是如果 key 相同,后面的不会覆盖前面的,取第一次出现某 key 的 value,为 key-value 键值对。除此之外,三个方法都能接受 >= 1 个参数,以 .extend 为例,.extend(a, b, c) 将会将 b,c 两个对象的键值对分别覆盖到 a 上。
那么问题来了,如何设计这三个用途相似的 api?我们来看看 underscore 是怎么做的。
_.extend = createAssigner(_.allKeys);
_.extendOwn = createAssigner(_.keys);
_.defaults = createAssigner(_.allKeys, true);
我们完整地看下带注释的 createAssigner 函数:
// An internal function for creating assigner functions.
// 有三个方法用到了这个内部函数
// _.extend & _.extendOwn & _.defaults
// _.extend = createAssigner(_.allKeys);
// _.extendOwn = _.assign = createAssigner(_.keys);
// _.defaults = createAssigner(_.allKeys, true);
var createAssigner = function(keysFunc, undefinedOnly) {
// 返回函数
// 经典闭包(undefinedOnly 参数在返回的函数中被引用)
// 返回的函数参数个数 >= 1
// 将第二个开始的对象参数的键值对 "继承" 给第一个参数
return function(obj) {
var length = arguments.length;
// 只传入了一个参数(或者 0 个?)
// 或者传入的第一个参数是 null
if (length < 2 || obj == null) return obj;
// 枚举第一个参数除外的对象参数
// 即 arguments[1], arguments[2] ...
for (var index = 1; index < length; index++) {
// source 即为对象参数
var source = arguments[index],
// 提取对象参数的 keys 值
// keysFunc 参数表示 _.keys
// 或者 _.allKeys
keys = keysFunc(source),
l = keys.length;
// 遍历该对象的键值对
for (var i = 0; i < l; i++) {
var key = keys[i];
// _.extend 和 _.extendOwn 方法
// 没有传入 undefinedOnly 参数,即 !undefinedOnly 为 true
// 即肯定会执行 obj[key] = source[key]
// 后面对象的键值对直接覆盖 obj
// ==========================================
// _.defaults 方法,undefinedOnly 参数为 true
// 即 !undefinedOnly 为 false
// 那么当且仅当 obj[key] 为 undefined 时才覆盖
// 即如果有相同的 key 值,取最早出现的 value 值
// *defaults 中有相同 key 的也是一样取首次出现的
if (!undefinedOnly || obj[key] === void 0)
obj[key] = source[key];
}
}
// 返回已经继承后面对象参数属性的第一个参数对象
return obj;
};
};
函数返回函数,并且返回的函数引用了外面的一个变量,这不正是经典的闭包?因为变量的个数可以 >=1,于是我们用 arguments 去获取变量。当变量个数为 1,或者第一个参数是 null 时,这时我们不需要做任何 "extend",直接返回第一个参数。之后,我们便可以用 arguments 枚举除去第一个参数外的其他参数,将它们的键值对覆盖到第一个参数对象上,具体可以看我的源码注释。
不看不知道,一看吓一跳,已经整整一个月没有更新 underscore 源码解读系列文章了。前面我们已经完成了 Object ,Array,Collection 上的扩展方法的源码剖析,本文开始来解读 Function 上的扩展方法。
完整的 underscore 源码解读系列文章请移步 https://github.com/hanzichi/underscore-analysis,觉得还阔以的话,给个 star 鼓励下楼主呗 ^_^
Ok,今天要讲的正是 bind。关于 bind,可以先移步楼主以前写的文章 ECMAScript 5(ES5) 中 bind 方法简介备忘,有个基本的概念。
bind() 方法会创建一个新函数,当这个新函数被调用时,它的 this 值是传递给 bind() 的第一个参数, 它的参数是 bind() 的其他参数和其原本的参数。
语法是这样样子的:
fun.bind(thisArg[, arg1[, arg2[, ...]]])
bind 的第一个参数会作为原函数运行时的 this 指向,不多说;而第二个开始的参数是可选的,当绑定函数被调用时,这些参数加上绑定函数本身的参数会按照顺序作为原函数运行时的参数。怎么理解?
function fn(a, b, c) {
return a + b + c;
}
var _fn = fn.bind(null, 10);
var ans = _fn(20, 30); // 60
fn 函数需要三个参数,_fn 函数将 10 作为默认的第一个参数,所以只需要传入两个参数即可,如果你不小心传入了三个参数,放心,也只会取前两个。
function fn(a, b, c) {
return a + b + c;
}
var _fn = fn.bind(null, 10);
var ans = _fn(20, 30, 40); // 60
这有啥用呢?如果某些函数,前几个参数已经 “内定” 了,我们便可以用 bind 返回一个新的函数。也就是说,bind() 能使一个函数拥有预设的初始参数。这些参数(如果有的话)作为 bind() 的第二个参数跟在 this 后面,之后它们会被插入到目标函数的参数列表的开始位置,传递给绑定函数的参数会跟在它们的后面。
function list() {
return Array.prototype.slice.call(arguments);
}
var list1 = list(1, 2, 3); // [1, 2, 3]
// Create a function with a preset leading argument
var leadingThirtysevenList = list.bind(undefined, 37);
var list2 = leadingThirtysevenList(); // [37]
var list3 = leadingThirtysevenList(1, 2, 3); // [37, 1, 2, 3]
使用 bind 返回的结果还是个 function,是个 function 就可以被 new 运算符调用,那么结果呢?规范中说的很清楚了,当使用 new 操作符调用绑定函数时,bind 的第一个参数无效。
function Person(name, age) {
this.name = name;
this.age = age;
}
var _Person = Person.bind({});
var p = new _Person('hanzichi', 30); // Person {name: "hanzichi", age: 30}
一般我们不会去这么用,但是如果要写个 bind 的 polyfill(http://caniuse.com/#search=bind),还是需要考虑用 new 调用的情况。
我们也可以设置默认值(参考上一小节),原先提供的那些参数仍然会被前置到构造函数调用的前面。
function Person(name, age) {
this.name = name;
this.age = age;
}
var _Person = Person.bind(null, 'hanzichi');
var p = new _Person(30); // Person {name: "hanzichi", age: 30}
什么时候容易丢失 this 指向?恩,setTimeout 是一个场景,很容易把 this 指向 window,当然,setInterval 也是一样。当使用对象的方法时,需要 this 引用对象,你可能需要显式地把 this 绑定到回调函数以便继续使用对象。
var canvas = {
render: function() {
this.update();
this.draw();
},
update: function() {
// ...
},
draw: function() {
// ...
}
};
window.setInterval(canvas.render, 1000 / 60);
用 canvas 写特效或者做游戏时经常会碰到类似的问题。上面的代码是有问题的,render 方法中的 this 其实被指向了 window!我们可以用 bind,显式地把 this 绑定到回调函数以便继续使用该对象。
window.setInterval(canvas.render.bind(canvas), 1000);
类似的情况还有 dom 的事件监听,一不小心可能 this 就被指向了 dom 元素。可以参考下以前做 bigrender 时写的这部分代码 https://github.com/hanzichi/hanzichi.github.io/blob/master/2016/bigrender/js/bigrender.js#L179-L184。
bind 还能做一些有意思的事情。
通常来说,将一个类数组转为数组,我们会用 slice(ie9- 不支持)。参考 #14
var slice = Array.prototype.slice;
// slice.apply(arguments);
// slice.call(arguments, 1);
bind 能让调用变的更加简单。
// same as "slice" in the previous example
var unboundSlice = Array.prototype.slice;
var slice = Function.prototype.call.bind(unboundSlice);
// ...
slice(arguments);
// slice(arguments, 1);
再举个类似的例子,比如说我们要添加事件到多个节点,for 循环当然没有任何问题,我们还可以 “剽窃” forEach
方法:
Array.prototype.forEach.call(document.querySelectorAll('input[type="button"]'), function(el){
el.addEventListener('click', fn);
});
更进一步,我们可以用 bind 将函数封装的更好:
var unboundForEach = Array.prototype.forEach
, forEach = Function.prototype.call.bind(unboundForEach);
forEach(document.querySelectorAll('input[type="button"]'), function (el) {
el.addEventListener('click', fn);
});
同样类似的,我们可以将 x.y(z) 变成 y(x,z) 的形式:
var obj = {
num: 10,
getCount: function() {
return this.num;
}
};
var unboundBind = Function.prototype.bind
, bind = Function.prototype.call.bind(unboundBind);
var getCount = bind(obj.getCount, obj);
console.log(getCount()); // 10
再举个栗子。每隔一秒在控制台打印 1-5,看起来是道考察闭包的经典题目。
for(var i = 1; i <= 5; i++) {
!function(i) {
setTimeout(function() {
console.log(i);
}, i * 1000);
}(i);
}
ES6 下能用 let
:
for(let i = 1; i <= 5; i++) {
setTimeout(function() {
console.log(i);
}, i * 1000);
}
也可以用 bind,瞬间逼格提升:
for(var i = 1; i <= 5; i++) {
setTimeout(console.log.bind(console, i), i * 1000);
}
2016-08-30 add:好吧,setTimeout 还能传入第三个参数,又涨姿势了 ...
for (var i = 1; i <= 5; i++) {
setTimeout(function(num) {
console.log(num)
}, i * 1000, i);
}
关于 bind 的介绍就到这里,下一篇文章将结合 underscore 来讲讲如何实现一个 bind 的 polyfill。
underscore 源码解读之 function 的扩展方法部分即将进入尾声,我们重点剖析了解了 bind 方法的使用场景以及兼容方式,函数节流、函数去抖,以及 memoize 方法,本文我们来看看 underscore 还为 function 扩展了哪些有用(有意思)的方法。
_.delay 能使得函数延迟执行,其实就是封装了一个定时器。
// Delays a function for the given number of milliseconds, and then calls
// it with the arguments supplied.
// 延迟触发某方法
// _.delay(function, wait, *arguments)
// 如果传入了 arguments 参数,则会被当作 func 的参数在触发时调用
// 其实是封装了「延迟触发某方法」,使其复用
_.delay = function(func, wait) {
// 获取 *arguments
// 是 func 函数所需要的参数
var args = slice.call(arguments, 2);
return setTimeout(function(){
// 将参数赋予 func 函数
return func.apply(null, args);
}, wait);
};
_.defer 其实就是设定了一个 0ms 的定时器(源码中其实设定的是 1ms)
// Defers a function, scheduling it to run after the current call stack has
// cleared.
// 和 setTimeout(func, 0) 相似(源码看来似乎应该是 setTimeout(func, 1))
// _.defer(function, *arguments)
// 如果传入 *arguments,会被当做参数,和 _.delay 调用方式类似(少了第二个参数)
// 其实核心还是调用了 _.delay 方法,但第二个参数(wait 参数)设置了默认值为 1
// 如何使得方法能设置默认值?用 _.partial 方法
_.defer = _.partial(_.delay, _, 1);
.partial 可以理解为一个「占位方法」,.delay 至少需要两个参数,第一个参数用 _
来占位( _.defer 的参数),第二个参数为 1,表示 _.delay 的 wait 参数为 1,也就是说设定了一个 1ms 的定时器。应用在哪里?其实平时你需要用到 setTimeout(function(){}, 0)
的地方,都可以用 _.defer 方法代替,比如为了避免 UI 卡顿的场景。
思考这样一个场景,如果有 N 个异步请求,全部请求完成后需要触发某个方法,怎么做?以前做爬虫碰到过这样的场景,当时用了 eventproxy 模块,ES6 的 Promise 应该也提供了相应的 API,underscore 的 _.after 方法也能做到。
function print() {
console.log('hello world');
}
var renderNotes = _.after(3, print);
renderNotes();
renderNotes();
// 第三次后才真正触发 print 方法
renderNotes(); // hello world
如果不仔细看,可能会错误地使用(N 次之后每次都会执行方法)。
function print() {
console.log('hello world');
}
var renderNotes = _.after(3, print);
renderNotes();
renderNotes();
renderNotes(); // hello world
renderNotes(); // hello world
原来的文档有点问题,我报了个 bug https://github.com/jashkenas/underscore/issues/2607,提了个 pr https://github.com/jashkenas/underscore/pull/2608,被合并了。_.after 源码非常简单,就不贴了。
_.before 和 .after 相反,其实两个方法的参数相同。假设传入第一个参数 times 的值为 N,.after 是在 [N, +∞)
次触发方法时执行,而 _.before 是在 [1, N)
触发时执行方法,刚好互为补集。
function print() {
console.log('hello world');
}
var renderNotes = _.before(3, print);
renderNotes(); // hello world
renderNotes(); // hello world
renderNotes();
renderNotes();
也就是 _.before 应用于 至多触发某函数 N-1 次。
而 _.once 基于 _.before 进行了封装,用于某函数至多只能被执行一次的场景。
function print() {
console.log('hello world');
}
var renderNotes = _.once(print);
// 至多执行一次 print
renderNotes(); // hello world
renderNotes();
renderNotes();
适用于类似初始化的场景,只需要被执行一次。
前文 我们对 JavaScript 中的函数节流和函数去抖的概念和应用场景进行了简单的了解,本文我们来深入探究下函数去抖的实现。(不懂函数去抖概念的建议看下前文 JavaScript 函数节流和函数去抖应用场景辨析 )
我们以 scroll 事件为例,探究如何实现滚动一次窗口打印一个 hello world 字符串。
如果不对其进行节流或者去抖控制:
window.onscroll = function() {
console.log('hello world');
};
这样每滚动一次,实际上会打印 N 多个 hello world。函数去抖背后的基本**是指,某些代码不可以在没有间断的情况连续重复执行。第一次调用函数,创建一个定时器,在指定的时间间隔之后运行代码。当第二次调用该函数时,它会清除前一次的定时器并设置另一个。如果前一个定时器已经执行过了,这个操作就没有任何意义。然而,如果前一个定时器尚未执行,其实就是将其替换为一个新的定时器。目的是只有在执行函数的请求停止了一段时间之后才执行。
《高程三》给出了最简洁最经典的去抖代码(书中说是节流,实则为去抖),调用如下:
function debounce(method, context) {
clearTimeout(method.tId);
method.tId = setTimeout(function() {
method.call(context);
}, 1000);
}
function print() {
console.log('hello world');
}
window.onscroll = function() {
debounce(print);
};
在窗口内滚动一次,停止,1000ms 后,打印了 hello world,因为我们设置了一个 1000ms 延迟的定时器,细思非常巧妙。
underscore 在其基础上进行了扩充,直接看代码,含大量注释:
// Returns a function, that, as long as it continues to be invoked, will not
// be triggered. The function will be called after it stops being called for
// N milliseconds. If `immediate` is passed, trigger the function on the
// leading edge, instead of the trailing.
// 函数去抖(连续事件触发结束后只触发一次)
// sample 1: _.debounce(function(){}, 1000)
// 连续事件结束后的 1000ms 后触发
// sample 1: _.debounce(function(){}, 1000, true)
// 连续事件触发后立即触发(此时会忽略第二个参数)
_.debounce = function(func, wait, immediate) {
var timeout, args, context, timestamp, result;
var later = function() {
// 定时器设置的回调 later 方法的触发时间,和连续事件触发的最后一次时间戳的间隔
// 如果间隔为 wait(或者刚好大于 wait),则触发事件
var last = _.now() - timestamp;
// 时间间隔 last 在 [0, wait) 中
// 还没到触发的点,则继续设置定时器
// last 值应该不会小于 0 吧?
if (last < wait && last >= 0) {
timeout = setTimeout(later, wait - last);
} else {
// 到了可以触发的时间点
timeout = null;
// 可以触发了
// 并且不是设置为立即触发的
// 因为如果是立即触发(callNow),也会进入这个回调中
// 主要是为了将 timeout 值置为空,使之不影响下次连续事件的触发
// 如果不是立即执行,随即执行 func 方法
if (!immediate) {
// 执行 func 函数
result = func.apply(context, args);
// 这里的 timeout 一定是 null 了吧
// 感觉这个判断多余了
if (!timeout)
context = args = null;
}
}
};
// 嗯,闭包返回的函数,是可以传入参数的
return function() {
// 可以指定 this 指向
context = this;
args = arguments;
// 每次触发函数,更新时间戳
// later 方法中取 last 值时用到该变量
// 判断距离上次触发事件是否已经过了 wait seconds 了
// 即我们需要距离最后一次触发事件 wait seconds 后触发这个回调方法
timestamp = _.now();
// 立即触发需要满足两个条件
// immediate 参数为 true,并且 timeout 还没设置
// immediate 参数为 true 是显而易见的
// 如果去掉 !timeout 的条件,就会一直触发,而不是触发一次
// 因为第一次触发后已经设置了 timeout,所以根据 timeout 是否为空可以判断是否是首次触发
var callNow = immediate && !timeout;
// 设置 wait seconds 后触发 later 方法
// 无论是否 callNow(如果是 callNow,也进入 later 方法,去 later 方法中判断是否执行相应回调函数)
// 在某一段的连续触发中,只会在第一次触发时进入这个 if 分支中
if (!timeout)
// 设置了 timeout,所以以后不会进入这个 if 分支了
timeout = setTimeout(later, wait);
// 如果是立即触发
if (callNow) {
// func 可能是有返回值的
result = func.apply(context, args);
// 解除引用
context = args = null;
}
return result;
};
};
等等,一下子多了这么多代码,那么我们比基础版多了哪些功能(优势)呢?
首先,基础版能做的,我们一样能做,一样让它在连续滚动后停止的 1000ms 后打印 hello world:
function print() {
console.log('hello world');
}
window.onscroll = _.debounce(print, 1000);
我们还可以在滚动刚触发的时候打印字符串,而不是连续滚动结束后,只需传入第三个参数,会自动忽略第二个参数:
function print() {
console.log('hello world');
}
window.onscroll = _.debounce(print, 1000, true);
这样对于连续的滚动,也只会打印一次,但是是在事件第一次触发的时候。
回调函数需要传入参数?一点问题都没有。
function print(a) {
console.log('The passed item is: ' + a);
}
var callback = _.debounce(print, 1000);
window.onscroll = function() {
var item = 'zichi';
callback(item);
};
当然,除了功能上的优势,性能也是提高不少,最显而易见的是基础版每此触发事件都会取消定时器,然后重新设置定时器,而 underscore 中会在一定时间后才取消定时器,重新设置定时器。其他更多可以细究下源码。(对性能有兴趣的可以看看这个 pr https://github.com/jashkenas/underscore/pull/1269)
最近开始看 underscore源码,并将 underscore源码解读 放在了我的 2016计划 中。
阅读一些著名框架类库的源码,就好像和一个个大师对话,你会学到很多。为什么是 underscore?最主要的原因是 underscore 简短精悍(约 1.5k 行),封装了 100 多个有用的方法,耦合度低,非常适合逐个方法阅读,适合楼主这样的 JavaScript 初学者。从中,你不仅可以学到用 void 0 代替 undefined 避免 undefined 被重写等一些小技巧 ,也可以学到变量类型判断、函数节流&函数去抖等常用的方法,还可以学到很多浏览器兼容的 hack,更可以学到作者的整体设计思路以及 API 设计的原理(向后兼容)。
之后楼主会写一系列的文章跟大家分享在源码阅读中学习到的知识。
欢迎围观~ (如果有兴趣,欢迎 star & watch~)您的关注是楼主继续写作的动力
说来惭愧,underscore 源码解读这个 Repo 放在 Github 都已经 20 天没有更新了,要不是今天 "不小心" 注意到,我居然都快忘了(是不是 lu 多了),所以今晚无论如何都要 lu 出第一篇(毕竟万事开头难)。相对于其他源码解读的文章,基本都会从整体设计开始讲起,楼主觉得 underscore 这个库有点特殊,so 决定按照自己的思路,从用 void 0 代替 undefined 说起。
underscore 源码没有出现 undefined(注意,其实有出现一处,是为 "undefined",而不是 undefined),而用 void 0 代替之。为什么要这么做?我们可以从两部分解读,其一是 undefined 哪里不好了,你非得找个替代品?其二就是替代品为毛要找 void 0?
我们先看第一点,答案很简单,undefined 并不是保留词(reserved word),它只是全局对象的一个属性,在低版本 IE 中能被重写。
var undefined = 10;
// undefined -- chrome
// 10 -- IE 8
alert(undefined);
事实上,undefined 在 ES5 中已经是全局对象的一个只读(read-only)属性了,它不能被重写。但是在局部作用域中,还是可以被重写的。
(function() {
var undefined = 10;
// 10 -- chrome
alert(undefined);
})();
(function() {
undefined = 10;
// undefined -- chrome
alert(undefined);
})();
接下来思考第二个问题,为毛找的替代品是 void 0?
我们来看看 MDN 的解释:
The void operator evaluates the given expression and then returns undefined.
意思是说 void 运算符能对给定的表达式进行求值,然后返回 undefined。也就是说,void 后面你随便跟上一个表达式,返回的都是 undefined,都能完美代替 undefined!那么,这其中最短的是什么呢?毫无疑问就是 void 0 了。其实用 void 1,void (1+1),void (0) 或者 void "hello",void (new Date()) 等等,都是一样的效果。更重要的前提是,void 是不能被重写的(cannot be overidden)。
那么,ES5 大环境下,void 0 就没有用武之地了吗?答案是否定的,用 void 0 代替 undefined 能节省不少字节的大小,事实上,不少 JavaScript 压缩工具在压缩过程中,正是将 undefined 用 void 0 代替掉了。
一篇不长的文章写了两个小时,心累,不点个赞、不关注下楼主的 Repo 你觉得好意思吗?https://github.com/hanzichi/underscore-analysis
_.isFunction = function(obj) {
// 这最后的逻辑或跟一个false,有必要吗?
return typeof obj == 'function' || false;
};
underscore 中的 这个地方最后一个 false 是必要的吗?如果是有什么意义么?
你有时间的话,希望你能解答一下,谢谢!!
最近开始看 underscore.js 源码,并将 underscore.js 源码解读 放在了我的 2016 计划中。
阅读一些著名框架类库的源码,就好像和一个个大师对话,你会学到很多。为什么是 underscore?最主要的原因是 underscore 简短精悍(约 1.5k 行),封装了 100 多个有用的方法,耦合度低,非常适合逐个方法阅读,适合楼主这样的 JavaScript 初学者。从中,你不仅可以学到用 void 0 代替 undefined 避免 undefined 被重写等一些小技巧 ,也可以学到变量类型判断、函数节流&函数去抖等常用的方法,还可以学到很多浏览器兼容的 hack,更可以学到作者的整体设计思路以及 API 设计的原理(向后兼容)。
之后楼主会写一系列的文章跟大家分享在源码阅读中学习到的知识。
欢迎围观~ (如果有兴趣,欢迎 star & watch~)您的关注是楼主继续写作的动力
今天要跟大家聊聊 for ... in 在浏览器中的兼容问题。
for ... in 大家应该都不陌生,循环只遍历可枚举属性。像 Array 和 Object 使用内置构造函数所创建的对象都会继承自 Object.prototype 和 String.prototype 的不可枚举属性,例如 String 的 indexOf() 方法或者 Object 的 toString 方法。循环将迭代对象的所有可枚举属性和从它的构造函数的 prototype 继承而来的(包括被覆盖的内建属性)。
我们举个简单的例子:
var obj = {name: 'hanzichi', age: 30};
for (var k in obj) {
console.log(k, obj[k]);
}
// 输出
// name hanzichi
// age 30
等等,你跟我说 for ... in 这玩意有浏览器兼容性?!从来没注意过啊,好像工作中也没碰到过这样的兼容性问题啊!确实如此,for ... in 要出问题,得满足两个条件,其一是在 IE < 9 浏览器中(又是万恶的 IE!!),其二是被枚举的对象重写了某些键,比如 toString。
还是举个简单的例子:
var obj = {toString: 'hanzichi'};
for (var k in obj) {
alert(k);
}
ok,在 chrome 中我们 alert 出了预期的 "toString",而在 IE 8 中啥都没有弹出。
我们回头看看 for ... in 的作用,循环遍历 可枚举属性,那么显然 IE 8 将 toString "内定" 成了不可枚举属性(尽管已经被重写)。那么如何判断是否在类似 IE 8 这样的环境中呢?underscore 中有个 hasEnumBug 函数就是用来做这个判断的:
// Keys in IE < 9 that won't be iterated by `for key in ...` and thus missed.
// IE < 9 下 不能用 for key in ... 来枚举对象的某些 key
// 比如重写了对象的 `toString` 方法,这个 key 值就不能在 IE < 9 下用 for in 枚举到
// IE < 9,{toString: null}.propertyIsEnumerable('toString') 返回 false
// IE < 9,重写的 `toString` 属性被认为不可枚举
var hasEnumBug = !{toString: null}.propertyIsEnumerable('toString');
代码一目了然,用了 propertyIsEnumerable 方法。
那么哪些属性被重写之后不能用 for ... in 在 IE < 9 下枚举到呢?有如下这些:
// IE < 9 下不能用 for in 来枚举的 key 值集合
var nonEnumerableProps = ['valueOf', 'isPrototypeOf', 'toString',
'propertyIsEnumerable', 'hasOwnProperty', 'toLocaleString'];
恩,应该还漏了个 constructor。
我们来看看 underscore 是怎么做的。
function collectNonEnumProps(obj, keys) {
var nonEnumIdx = nonEnumerableProps.length;
var constructor = obj.constructor;
// proto 是否是继承的 prototype
var proto = (_.isFunction(constructor) && constructor.prototype) || ObjProto;
// Constructor is a special case.
// `constructor` 属性需要特殊处理
// 如果 obj 有 `constructor` 这个 key
// 并且该 key 没有在 keys 数组中
// 存入数组
var prop = 'constructor';
if (_.has(obj, prop) && !_.contains(keys, prop)) keys.push(prop);
// nonEnumerableProps 数组中的 keys
while (nonEnumIdx--) {
prop = nonEnumerableProps[nonEnumIdx];
// prop in obj 应该肯定返回 true 吧?是否不必要?
// obj[prop] !== proto[prop] 判断该 key 是否来自于原型链
if (prop in obj && obj[prop] !== proto[prop] && !_.contains(keys, prop)) {
keys.push(prop);
}
}
}
proto 变量保存了原型,一个对象的原型可以通过 obj.constructor.prototype 获取,但是如果重写了 constructor 很显然就无法这样获取了,则用 Object.prototype 替换。这样比如说重写了 toString,我们只需要比较 obj.toString 是否和 proto.toString 引用相同即可。个人觉得源码中的 prop in obj 判断多余了,这不肯定返回 true 吗?如果有理解错误,望指出。
而对于重写了 constructor 的情况,underscore 用 hasOwnProperty 进行判断。
对于重写了以上几种属性的情况,underscore 确实能够获取其在 IE < 9 中的键,但是爱钻牛角尖的楼主也十分不解,constructor 真的有必要和其他属性分开来检测吗?
对于 toString 这样的属性被重写,underscore 的判断非常好,如果没有被重写,那么对象的 toString 方法肯定是继承于原型链的,判断对象的 toString 方法是否和原型链上的一致即可,但是用 hasOwnProperty 能判断吗?楼主觉得也是可以的,hasOwnProperty 方法用来判断对象的 key 是否是自有属性,即是否来自于原型链,如果被重写了,那么应该会返回 true,否则 false。
而被重写的 constructor 能否用 obj[prop] !== proto[prop] 来判断呢?楼主觉得也是可以的,如果没有被重写,那么 obj.constructor === obj.constructor.prototype.constructor 返回 true,如果被重写,obj.constructor === Object.prototype.constructor 返回 false。
关于这点,楼主也是百思不得其解,但是很显然 constructor 属性和其他属性是有明显区别的,从代码理解角度来看,也是 underscore 这样处理比较容易接受。如果是楼主理解有出入的地方,还望指出!
最后,小结下,对于 for ... in 在 IE < 9 下的兼容问题,楼主感觉并没有那么重要,毕竟谁会没事去重写这些属性呢!所以,知道有这么一回事就可以了。
最后的最后,给出这部分源码位置,有兴趣的同学可以看下 https://github.com/hanzichi/underscore-analysis/blob/master/underscore-1.8.3.js/src/underscore-1.8.3.js#L904-L946
斐波那契数列 应该都很熟悉,如何能够快速求得斐波那契数列中某项的值呢?
一个很显然的方法是正向地叠加求解:
"use strict";
const N = 1000;
let fibonacciNumber = [];
for (let i = 0; i < 1000; i++) {
if (i < 2)
fibonacciNumber[i] = i;
else
fibonacciNumber[i] = fibonacciNumber[i - 2] + fibonacciNumber[i - 1];
}
这样做有个「预处理」的过程,我们不知道需要预处理多大的数据,这就比较棘手,如果能给个数据范围,这无疑是最高效又简便的方法。
接着我们考虑每次独立求解(非严格模式下,函数内的 fibonacci 也能用 arguments.callee 来代替):
function fibonacci(n) {
return n < 2 ? n : fibonacci(n - 2) + fibonacci(n - 1);
}
很经典的递归,但是这样做出现了大量重复递归运算。我们可以缓存中间运算结果,大大提高效率,有点「记忆化」的意思:
"use strict";
let fibonacci = function() {
// 缓存过程中计算结果
let cache = {};
return function(n) {
// 没有被计算过
if (!cache[n])
cache[n] = n < 2 ? n : fibonacci(n - 2) + fibonacci(n - 1);
return cache[n];
}
}();
我们可以将 memoize 函数封装起来:
"use strict";
let memoize = function(func) {
let cache = {};
return function(key) {
if (!cache[key])
cache[key] = func.apply(this, arguments);
return cache[key];
}
}
let fibonacci = memoize(function(n) {
return n < 2 ? n : fibonacci(n - 2) + fibonacci(n - 1);
});
与最开始的递归版相比,只是外面包了个 memoize 方法而已,使得整个 func 的计算能保存中间已经计算过的值。
最后我们来看看 underscore 对其的实现:
// Memoize an expensive function by storing its results.
//「记忆化」,存储中间运算结果,提高效率
// 参数 hasher 是个 function,用来计算 key
// 如果传入了 hasher,则用 hasher 来计算 key
// 否则用 key 参数直接当 key(即 memoize 方法传入的第一个参数)
// _.memoize(function, [hashFunction])
// 适用于需要大量重复求值的场景
// 比如递归求解菲波那切数
// @http://www.jameskrob.com/memoize.html
// create hash for storing "expensive" function outputs
// run expensive function
// check whether function has already been run with given arguments via hash lookup
// if false - run function, and store output in hash
// if true, return output stored in hash
_.memoize = function(func, hasher) {
var memoize = function(key) {
// 储存变量,方便使用
var cache = memoize.cache;
// 求 key
// 如果传入了 hasher,则用 hasher 函数来计算 key
// 否则用 参数 key(即 memoize 方法传入的第一个参数)当 key
var address = '' + (hasher ? hasher.apply(this, arguments) : key);
// 如果这个 key 还没被 hash 过(还没求过值)
if (!_.has(cache, address))
cache[address] = func.apply(this, arguments);
// 返回
return cache[address];
};
// cache 对象被当做 key-value 键值对缓存中间运算结果
memoize.cache = {};
// 返回一个函数(经典闭包)
return memoize;
};
实现原理和 memoize 差不多,underscore 将缓存中间结果的 cache 对象绑在了闭包返回的函数上(当做函数的属性),同时增加了一个 hasher 函数选项,如果函数有好几个参数,那么我们可以传入这个 hasher 函数来计算 key 值,从而来 hash。
参考:关于 memoize 前后性能对比的可以看下这篇文章 Faster JavaScript Memoization For Improved Application Performance
自从进入七月以来,我的 underscore 源码解读系列 更新缓慢,再这样下去,今年更完的目标似乎要落空,赶紧写一篇压压惊。
前文 跟大家简单介绍了下 ES5 中的 bind 方法以及使用场景(没读过的同学建议先看看),毕竟 bind 是 ES5 的东西,低版本 IE 不支持。今天就根据 underscore 的实现,来聊一聊如何实现一个 bind 的 polyfill。
之前在 ECMAScript 5(ES5) 中 bind 方法简介备忘 一文中,给出了一个 "穷人版" 的 polyfill,如下。
Function.prototype.bind = Function.prototype.bind || function(context) {
var that = this;
return function() {
return that.apply(context, arguments);
}
}
说实话,基本可以满足多数的场景需求了。bind 方法返回的还是一个方法(经典闭包),很巧妙地用 apply 改变(绑定)了 this 指向。但是毫无疑问这样简单的实现是有问题的。
首先,该方法只支持传入一个参数,为方法需要绑定的 this 指向,原生的 bind 方法可以传入多个参数,如果要问这些参数干嘛用,回头翻翻 前文。如何实现传参?非常简单,传入,然后提取,不就 ok 了?
underscore 源码中重点看这几行:
var args = slice.call(arguments, 2);
var bound = function() {
// args.concat(slice.call(arguments))
// 最终函数的实际调用参数由两部分组成
// 一部分是传入 _.bind 的参数
// 另一部分是传入 bound(_.bind 所返回方法)的参数
return executeBound(func, bound, context, this, args.concat(slice.call(arguments)));
};
return bound;
其实核心实现差不多,都是闭包返回函数。第一行将参数(args)提取保存(这些参数将会在方法中被优先调用),返回的是一个叫做 bound 的方法,bound 也能传参啊,用 args.concat(slice.call(arguments))
将两个参数合并当做原方法的参数,因为 args 会优先调用,所以合并结果 args 中元素在先。
接着来看 executeBound 函数,为何 "穷人版" 一行的代码,这里却要整个函数出来?原因是 "穷人版" 没有考虑 bind 返回函数被 new 操作的情况。如果不是被 new 操作,那就简单了,和 "穷人版" 是一样一样的,直接看 underscore 源码。
// 非 new 调用 _.bind 返回的方法(即 bound)
// callingContext 不是 boundFunc 的一个实例
if (!(callingContext instanceof boundFunc))
return sourceFunc.apply(context, args);
如果进行 new 运算操作呢?这里我们还要复习一下 new 运算,有兴趣的可以看下我以前的文章 一道有意思的笔试题引发的对于 new 操作符的思考。概括地讲,如果构造函数有返回值,且返回值是对象(不能是 null),那么对其进行 new 操作返回该对象,否则返回构造实例。所以在方法 executeBound 中,我们需要进一步判断这个构造函数有没有返回值,返回值是不是对象。
var executeBound = function(sourceFunc, boundFunc, context, callingContext, args) {
// 非 new 调用 _.bind 返回的方法(即 bound)
// callingContext 不是 boundFunc 的一个实例
if (!(callingContext instanceof boundFunc))
return sourceFunc.apply(context, args);
// 如果是用 new 调用 _.bind 返回的方法
// self 为 sourceFunc 的实例,继承了它的原型链
// self 理论上是一个空对象(还没赋值),但是有原型链
var self = baseCreate(sourceFunc.prototype);
// 用 new 生成一个构造函数的实例
// 正常情况下是没有返回值的,即 result 值为 undefined
// 如果构造函数有返回值
// 如果返回值是对象(非 null),则 new 的结果返回这个对象
// 否则返回实例
// @see http://www.cnblogs.com/zichi/p/4392944.html
var result = sourceFunc.apply(self, args);
// 如果构造函数返回了对象
// 则 new 的结果是这个对象
// 返回这个对象
if (_.isObject(result)) return result;
// 否则返回 self
// var result = sourceFunc.apply(self, args);
// self 对象当做参数传入
// 会直接改变值
return self;
};
关于这部分的源码,有兴趣的同学可以参考 https://github.com/hanzichi/underscore-analysis/blob/master/underscore-1.8.3.js/src/underscore-1.8.3.js#L698-L719。
关于 Function 这部分,接下去的打算是去抖一篇,节流一篇,然后其他零碎的方法概要一篇,希望能在十月中旬左右结束掉吧。
最近因为一些需要,想用java实现underscore的collections的方法,请问有什么建议吗
最近开始看 underscore.js 源码,并将 underscore.js 源码解读 放在了我的 2016 计划中。
阅读一些著名框架类库的源码,就好像和一个个大师对话,你会学到很多。为什么是 underscore?最主要的原因是 underscore 简短精悍(约 1.5k 行),封装了 100 多个有用的方法,耦合度低,非常适合逐个方法阅读,适合楼主这样的 JavaScript 初学者。从中,你不仅可以学到用 void 0 代替 undefined 避免 undefined 被重写等一些小技巧 ,也可以学到变量类型判断、函数节流&函数去抖等常用的方法,还可以学到很多浏览器兼容的 hack,更可以学到作者的整体设计思路以及 API 设计的原理(向后兼容)。
之后楼主会写一系列的文章跟大家分享在源码阅读中学习到的知识。
欢迎围观~ (如果有兴趣,欢迎 star & watch~)您的关注是楼主继续写作的动力
第一篇跟大家简单地聊了下为什么 underscore.js 用 void 0 代替了 undefined,意外地收到了不错的反响,有朋友私信我说以前还真不知道这回事,也有人催促我赶紧继续下一篇解读文章。今天就跟大家聊一聊 underscore.js 中一些 JavaScript 常用类型检查方法,以及一些工具类的判断方法。
我们先说个老生常谈的问题,JavaScript 中数组类型的判断方法,事实上,我在 Javascript中判断数组的正确姿势 一文中已经详细分析了各种判断方式的优缺点,并给出了正确的判断代码:
function isArray(a) {
Array.isArray ? Array.isArray(a) : Object.prototype.toString.call(a) === '[object Array]';
}
而 underscore 其实也正是这么做的:
// Is a given value an array?
// Delegates to ECMA5's native Array.isArray
// 判断是否为数组
_.isArray = nativeIsArray || function(obj) {
return toString.call(obj) === '[object Array]';
};
nativeIsArray 正是 ES5 中 Array.isArray 方法,如果支持则优先调用;而 toString 变量就保存了 Object.prototype.toString。
如何判断对象?underscore 把类型为 function 和 object 的变量都算作对象,当然得除去 null。
// Is a given variable an object?
// 判断是否为对象
// 这里的对象包括 function 和 object
_.isObject = function(obj) {
var type = typeof obj;
return type === 'function' || type === 'object' && !!obj;
};
再看 'Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp', 'Error' 这些类型的判断,其实都可以用 Object.prototype.toString.call 来判断,所以写在了一起:
// Add some isType methods: 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 + ']';
};
});
但是看 isArguments 方法,在 IE < 9 下对 arguments 调用 Object.prototype.toString.call,结果是 [object Object],而并非我们期望的 [object Arguments]。咋整?我们可以用该元素是否含有 callee 属性来判断,众所周时,arguments.callee 能返回当前 arguments 所在的函数。
// Define a fallback version of the method in browsers (ahem, IE < 9), where
// there isn't any inspectable "Arguments" type.
// _.isArguments 方法在 IE < 9 下的兼容
// IE < 9 下对 arguments 调用 Object.prototype.toString.call 方法
// 结果是 [object Object]
// 而并非我们期望的 [object Arguments]。
// so 用是否含有 callee 属性来判断
if (!_.isArguments(arguments)) {
_.isArguments = function(obj) {
return _.has(obj, 'callee');
};
}
接下来看下一些常用的工具类判断方法。
判断一个元素是否是 DOM 元素,非常简单,只需要保证它不为空,且 nodeType 属性为 1:
// Is a given value a DOM element?
// 判断是否为 DOM 元素
_.isElement = function(obj) {
// 确保 obj 不是 null
// 并且 obj.nodeType === 1
return !!(obj && obj.nodeType === 1);
};
如何判断一个元素为 NaN?NaN 其实是属于 Number 类型,Object.prototype.toString.call(NaN) 返回的是 "[object Number]",而且 NaN 不等于本身,利用这两点即可进行判断:(2016-06-21 该实现有 BUG,详见 https://github.com/hanzichi/underscore-analysis/issues/13)
// Is the given value `NaN`? (NaN is the only number which does not equal itself).
// 判断是否是 NaN
// NaN 是唯一的一个 `自己不等于自己` 的 number 类型
_.isNaN = function(obj) {
return _.isNumber(obj) && obj !== +obj;
};
当然,underscore 还有很多其他的有用的工具类判断方法,具体可以看源码 https://github.com/hanzichi/underscore-analysis/blob/master/underscore-1.8.3.js/src/underscore-1.8.3.js#L1192-L1263 这部分。
如果您觉得我分享的东西对您有所帮助,请关注我的 Repo https://github.com/hanzichi/underscore-analysis
首先,非常感谢楼主的分享,也说明一下,我没有挑刺的意思。看了一下,你这里说的是underscore1.8.3的源码,事实上,你这个是1.8.2的版本啊,underscore1.8.3里面,对于root的定义已经变成了这样了:
var root = typeof self == 'object' && self.self === self && self ||
typeof global == 'object' && global.global === global && global ||
this;
我也最近才开始看underscore源码,我就是看的1.8.3,结果发现和楼主这里的源码不一样。
最后,还请多多指教。谢谢!
这篇文章本来不打算写的,实话说楼主对前端模板的认识还处在非常初级的阶段,但是为了整个 源码解读系列 的完整性,在深入 Underscore _.template 方法源码后,觉得还是有必要记下此文,为了自己备忘也好,为了还没用上前端模板引擎的同学的入门也好。(熟悉模板引擎的可以帮楼主看看文中有没有 BUG ..)
说起模板渲染,楼主首先接触的其实并不是前端模板引擎,而是后端。后端 MVC 模式中,一般从 Model 层中读取数据,然后将数据传到 View 层渲染(渲染成 HTML 文件),而 View 层,一般都会用到模板引擎,比如楼主项目中用到的 PHP 的 smarty 模板引擎。随便上段代码感受一下。
<div>
<ul class="well nav nav-list" style="height:95%;">
{{foreach from=$pageArray.result item=leftMenu key=key name=leftMenu}}
<li class="nav-header">{{$key}}</li>
{{foreach from=$leftMenu key=key2 item=item2}}
<li><a target="main" href='{{$item2}}'>{{$key2}}</a></li>
{{/foreach}}
{{/foreach}}
</ul>
</div>
传入 View 层的其实就是个叫做 $pageArray 的 JSON 数据。而 MVC 模式也是非常容易理解,推荐看下阮一峰老师的 谈谈MVC模式,然后再看看下面这张图。
以前的 WEB 项目大多会采用这种后台 MVC 模式,这样做有利于 SEO,并且与前端请求接口的方式相比,少了个 HTTP 请求,理论上加载速度可能会稍微快些。但是缺点也非常明显,前端写完静态页面,要让后台去「套模板」,每次前端稍有改动,后台对应的模板页面同时也需要改动,非常麻烦。页面中如果有复杂的 JS,前端写还是后端写?前端写的话,没有大量的数据,调试不方便,后端写的话... 所以楼主看到的 PHPer 通常都会 JS。
AJAX 的出现使得前后端分离成为可能。后端专注于业务逻辑,给前端提供接口,而前端通过 AJAX 的方式向后端请求数据,然后动态渲染页面。
我们假设接口数据如下:
[{name: "apple"}, {name: "orange"}, {name: "peach"}]
假设渲染后的页面如下:
<div>
<ul class="list">
<li>apple</li>
<li>orange</li>
<li class="last-item">peach</li>
</ul>
</div>
前端模板引擎出现之前,我们一般会这么做:
<div></div>
<script>
// 假设接口数据
var data = [{name: "apple"}, {name: "orange"}, {name: "peach"}];
var str = "";
str += '<ul class="list">';
for (var i = 0, len = data.length; i < len; i++) {
if (i !== len - 1)
str += "<li>" + data[i].name + "</li>";
else
str += '<li class="last-item">' + data[i].name + "</li>";
}
str += "</ul>";
document.querySelector("div").innerHTML = str;
</script>
其实楼主个人也经常这么干,看上去简单方便,但是这样做显然有缺点,将 HTML 代码(View 层)和 JS 代码(Controller 层)混杂在了一起,UI 与逻辑代码混杂在一起,阅读起来会非常吃力。一旦业务复杂起来,或者多人维护的情况下,几乎会失控。而且如果需要拼接的 HTML 代码里有很多引号的话(比如有大量的 href 属性,src 属性),那么就非常容易出错了(这样干过的应该深有体会)。
这个时候,前端模板引擎出现了,Underscore 的 _.template 可能是最简单的前端模板引擎了(可能还上升不到引擎的高度,或者说就是个前端模板函数)。我们先不谈 _.template 的实现,将以上的代码用其改写。
<div></div>
<script src="//cdn.bootcss.com/underscore.js/1.8.3/underscore.js"></script>
<script type="text/template" id="tpl">
<ul class="list">
<%_.each(obj, function(e, i, a){%>
<% if (i === a.length - 1) %>
<li class="last-item"><%=e.name%></li>
<% else %>
<li><%=e.name%></li>
<%})%>
</ul>
</script>
<script>
// 模拟数据
var data = [{name: "apple"}, {name: "orange"}, {name: "peach"}];
var compiled = _.template(document.getElementById("tpl").innerHTML);
var str = compiled(data);
document.querySelector("div").innerHTML = str;
</script>
这样一来,如果前端需要改 HTML 代码,只需要改模板即可。这样做的优点很明显,前端 UI 和逻辑代码不再混杂,阅读体验良好,改动起来也方便了许多。
前后端分离最大的缺点可能就是 SEO 无力了,毕竟爬虫只会抓取 HTML 代码,不会去渲染 JS。(PS:现在的 Google 爬虫已经可以抓取 AJAX 了 Making AJAX applications crawlable,具体效果未知)
单纯的后端模板引擎(后端 MVC)以及前端模板引擎方式都有一定的局限性,Node 的出现让我们有了第三种选择,让 Node 作为中间层。
具体如何操作?简单地说就是让一门后台语言(比如 Java?PHP?)单纯提供渲染页面所需要的接口,Node 中间层用模板引擎来渲染页面,使得页面直出。这样一来,后台提供的接口,不仅 Web 端可以使用,APP,浏览器也可以调用,同时页面 Node 直出也不会影响 SEO,并且前后端也分离,不失为一种比较完美的方案。
本文简单介绍了模板引擎在前后端的使用,下文我们回到 Underscore,重点分析下 _.template 的使用方式以及源码原理。
PS:楼主对于模板引擎的认识比较浅显,有不正之处希望指出~感谢!
终于可以开始 Collection Functions 部分了。
可能有的童鞋是第一次看楼主的系列文章,这里再做下简单的介绍。楼主在阅读 underscore.js 源码的时候,学到了很多,同时觉得有些知识点可以独立出来,写成文章与大家分享,而本文正是其中之一(完整的系列请猛戳 https://github.com/hanzichi/underscore-analysis)。之前楼主已经和大家分享了 Object 和 Array 的扩展方法中一些有意思的知识点,今天开始解读 Collection 部分。
看完 Collection Functions 部分的源码,首先迫不及待想跟大家分享的正是本文主题 —— 数组乱序。这是一道经典的前端面试题,给你一个数组,将其打乱,返回新的数组,即为数组乱序,也称为洗牌问题。
一个好的方案需要具备两个条件,一是正确性,毋庸置疑,这是必须的,二是高效性,在确保正确的前提下,如何将复杂度降到最小,是我们需要思考的。
几年前楼主还真碰到过洗牌问题,还真的是 "洗牌"。当时是用 cocos2d-js(那时还叫 cocos2d-html5)做牌类游戏,发牌前毫无疑问需要洗牌。
当时我是这样做的。每次 random 一个下标,看看这个元素有没有被选过,如果被选过了,继续 random,如果没有,将其标记,然后存入返回数组,直到所有元素都被标记了。后来经同事指导,每次选中后,可以直接从数组中删除,无需标记了,于是得到下面的代码。
function shuffle(a) {
var b = [];
while (a.length) {
var index = ~~(Math.random() * a.length);
b.push(a[index]);
a.splice(index, 1);
}
return b;
}
这个解法的正确性应该是没有问题的(有兴趣的可以自己去证明下)。我们假设数组的元素为 0 - 10,对其乱序 N 次,那么每个位置上的结果加起来的平均值理论上应该接近 (0 + 10) / 2 = 5,且 N 越大,越接近 5。为了能有个直观的视觉感受,我们假设乱序 1w 次,并且将结果做成了图表,猛戳 http://hanzichi.github.io/test-case/shuffle/splice/ 查看,结果还是很乐观的。
验证了正确性,还要关心一下它的复杂度。由于程序中用了 splice,如果把 splice 的复杂度看成是 O(n),那么整个程序的复杂度是 O(n^2)。
另一个为人津津乐道的方法是 "巧妙应用" JavaScript 中的 Math.random() 函数。
function shuffle(a) {
return a.concat().sort(function(a, b) {
return Math.random() - 0.5;
});
}
同样是 [0, 1, 2 ... 10] 作为初始值,同样跑了 1w 组 case,结果请猛戳 http://hanzichi.github.io/test-case/shuffle/Math.random/。
看平均值的图表,很明显可以看到曲线浮动,而且多次刷新,折现的大致走向一致,平均值更是在 5 上下 0.4 的区间浮动。如果我们将 [0, 1, 2 .. 9] 作为初始数组,可以看到更加明显不符预期的结果(有兴趣的可以自己去试下)。究其原因,要追究 JavaScript 引擎对于 Math.random() 的实现原理,这里就不展开了(其实是我也不知道)。因为 ECMAScript 并没有规定 JavaScript 引擎对于 Math.random() 应该实现的方式,所以我猜想不同浏览器经过这样的乱序后,结果也不一样。
什么时候可以用这种方法乱序呢?"非正式" 场合,一些手写 DEMO 需要乱序的场合,这不失为一种 clever solution。
但是这种解法不但不正确,而且 sort 的复杂度,平均下来应该是 O(nlogn),跟我们接下来要说的正解还是有不少差距的。
关于数组乱序,正确的解法应该是 Fisher–Yates Shuffle,复杂度 O(n)。
其实它的**非常的简单,遍历数组元素,将其与之前的任意元素交换。因为遍历有从前向后和从后往前两种方式,所以该算法大致也有两个版本的实现。
从后往前的版本:
function shuffle(array) {
var _array = array.concat();
for (var i = _array.length; i--; ) {
var j = Math.floor(Math.random() * (i + 1));
var temp = _array[i];
_array[i] = _array[j];
_array[j] = temp;
}
return _array;
}
underscore 中采用从前往后遍历元素的方式,实现如下:
// Shuffle a collection, using the modern version of the
// [Fisher-Yates shuffle](http://en.wikipedia.org/wiki/Fisher–Yates_shuffle).
_.shuffle = function(obj) {
var set = isArrayLike(obj) ? obj : _.values(obj);
var length = set.length;
var shuffled = Array(length);
for (var index = 0, rand; index < length; index++) {
rand = _.random(0, index);
if (rand !== index) shuffled[index] = shuffled[rand];
shuffled[rand] = set[index];
}
return shuffled;
};
将其解耦分离出来,如下:
function shuffle(a) {
var length = a.length;
var shuffled = Array(length);
for (var index = 0, rand; index < length; index++) {
rand = ~~(Math.random() * (index + 1));
if (rand !== index)
shuffled[index] = shuffled[rand];
shuffled[rand] = a[index];
}
return shuffled;
}
跟前面一样,做了下数据图表,猛戳 http://hanzichi.github.io/test-case/shuffle/Fisher-Yates/。
关于证明,引用自月影老师的文章:
随机性的数学归纳法证明
对 n 个数进行随机:
关于数组乱序,如果面试中被问到,能说出 "Fisher–Yates Shuffle",并且能基本说出原理(你也看到了,其实代码非常的简单),那么基本应该没有问题了;如果能更进一步,将其证明呈上(甚至一些面试官都可能一时证明不了),那么就牛逼了。千万不能只会用 Math.random() 投机取巧!
Read More:
如标题,
该函数中,也就是66行if (!(this instanceof _)) return new _(obj);
为什么这里不是obj instanceof _而变成了this?
(觉得这部分眼熟的可以直接跳到下一段了...)
最近开始看 underscore.js 源码,并将 underscore.js 源码解读 放在了我的 2016 计划中。
阅读一些著名框架类库的源码,就好像和一个个大师对话,你会学到很多。为什么是 underscore?最主要的原因是 underscore 简短精悍(约 1.5k 行),封装了 100 多个有用的方法,耦合度低,非常适合逐个方法阅读,适合楼主这样的 JavaScript 初学者。从中,你不仅可以学到用 void 0 代替 undefined 避免 undefined 被重写等一些小技巧 ,也可以学到变量类型判断、函数节流&函数去抖等常用的方法,还可以学到很多浏览器兼容的 hack,更可以学到作者的整体设计思路以及 API 设计的原理(向后兼容)。
之后楼主会写一系列的文章跟大家分享在源码阅读中学习到的知识。
欢迎围观~ (如果有兴趣,欢迎 star & watch~)您的关注是楼主继续写作的动力
今天要聊的,也是我以前笔试时碰到过的一个问题,数组去重,不知道现在的笔试题还考不考这个?
数组去重,一般需求是给你一个数组,调用去重方法,返回数值副本,副本中没有重复元素。一般来说,两个元素通过 ===
比较返回 true 的视为相同元素,需要去重,所以,1
和 "1"
是不同的元素,1
和 new Number(1)
是不同的元素,{}
和 {}
是不同的元素(引用不同)。(当然如果需求认为 {}
和 {}
算作相同的元素,那么解法就不一样了)
无需思考,我们可以得到 O(n^2) 复杂度的解法。定义一个变量数组 res 保存结果,遍历需要去重的数组,如果该元素已经存在在 res 中了,则说明是重复的元素,如果没有,则放入 res 中。
function unique(a) {
var res = [];
for (var i = 0, len = a.length; i < len; i++) {
var item = a[i];
for (var j = 0, jLen = res.length; j < jLen; j++) {
if (res[j] === item)
break;
}
if (j === jLen)
res.push(item);
}
return res;
}
var a = [1, 1, '1', '2', 1];
var ans = unique(a);
console.log(ans); // => [1, "1", "2"]
代码非常简单,那么是否能更简洁些?如果不考虑浏览器兼容,我们可以用 ES5 提供的 Array.prototype.indexOf 方法来简化代码。
function unique(a) {
var res = [];
for (var i = 0, len = a.length; i < len; i++) {
var item = a[i];
(res.indexOf(item) === -1) && res.push(item);
}
return res;
}
var a = [1, 1, '1', '2', 1];
var ans = unique(a);
console.log(ans); // => [1, "1", "2"]
既然用了 indexOf,那么不妨再加上 filter。
function unique(a) {
var res = a.filter(function(item, index, array) {
return array.indexOf(item) === index;
});
return res;
}
var a = [1, 1, '1', '2', 1];
var ans = unique(a);
console.log(ans); // => [1, "1", "2"]
法一是将原数组中的元素和结果数组中的元素一一比较,我们可以换个思路,将原数组中重复元素的最后一个元素放入结果数组中。
function unique(a) {
var res = [];
for (var i = 0, len = a.length; i < len; i++) {
for (var j = i + 1; j < len; j++) {
// 这一步十分巧妙
// 如果发现相同元素
// 则 i 自增进入下一个循环比较
if (a[i] === a[j])
j = ++i;
}
res.push(a[i]);
}
return res;
}
var a = [1, 1, '1', '2', 1];
var ans = unique(a);
console.log(ans); // => ["1", "2", 1]
虽然复杂度还是 O(n^2),但是可以看到结果不同,1 出现在了数组最后面,因为结果数组取的是元素最后一次出现的位置。
如果笔试面试时只答出了上面这样 O(n^2) 的方案,可能还不能使面试官满意,下面就来说几种进阶方案。
将数组用 sort 排序后,理论上相同的元素会被放在相邻的位置,那么比较前后位置的元素就可以了。
function unique(a) {
return a.concat().sort().filter(function(item, pos, ary) {
return !pos || item != ary[pos - 1];
});
}
var a = [1, 1, 3, 2, 1, 2, 4];
var ans = unique(a);
console.log(ans); // => [1, 2, 3, 4]
但是问题又来了,1
和 "1"
会被排在一起,不同的 Object 会被排在一起,因为它们 toString() 的结果相同,所以会出现这样的错误:
var a = [1, 1, 3, 2, 1, 2, 4, '1'];
var ans = unique(a);
console.log(ans); // => [1, 2, 3, 4]
当然你完全可以针对数组中可能出现的不同类型,来写这个比较函数。不过这似乎有点麻烦。
用 JavaScript 中的 Object 对象来当做哈希表,这也是几年前笔试时的解法,跟 sort 一样,可以去重完全由 Number 基本类型组成的数组。
function unique(a) {
var seen = {};
return a.filter(function(item) {
return seen.hasOwnProperty(item) ? false : (seen[item] = true);
});
}
var a = [1, 1, 3, 2, 1, 2, 4];
var ans = unique(a);
console.log(ans); // => [1, 3, 2, 4]
还是和方法三一样的问题,因为 Object 的 key 值都是 String 类型,所以对于 1
和 "1"
无法分别,我们可以稍微改进下,将类型也存入 key 中。
function unique(a) {
var ret = [];
var hash = {};
for (var i = 0, len = a.length; i < len; i++) {
var item = a[i];
var key = typeof(item) + item;
if (hash[key] !== 1) {
ret.push(item);
hash[key] = 1;
}
}
return ret;
}
var a = [1, 1, 3, 2, '4', 1, 2, 4, '1'];
var ans = unique(a);
console.log(ans); // => [1, 3, 2, "4", 4, "1"]
虽然解决了讨厌的 1
和 "1"
的问题,但是还有别的问题!
var a = [{name: "hanzichi"}, {age: 30}, new String(1), new Number(1)];
var ans = unique(a);
console.log(ans); // => [Object, String]
但是如果数组元素全部是基础类型的 Number 值,键值对法应该是最高效的!
ES6 部署了 Set 以及 Array.from 方法,太强大了!如果浏览器支持,完全可以这样:
function unique(a) {
return Array.from(new Set(a));
}
var a = [{name: "hanzichi"}, {age: 30}, new String(1), new Number(1)];
var ans = unique(a);
console.log(ans); // => [Object, Object, String, Number]
最后来看看 underscore 对此的实现方式,underscore 将此封装到了 _.unique 方法中,调用方式为 _.unique(array, [isSorted], [iteratee])。其中第一个参数是必须的,是需要去重的数组,第二个参数可选,如果数组有序,则可以传入布尔值 true,第三个参数可选,如果需要对数组迭代的结果去重,则可以传入一个迭代函数。而数组元素去重是基于 ===
运算符的。
其实很简单,underscore 中的实现方式和上面的方法一相似。
我们来看它的核心代码:
for (var i = 0, length = getLength(array); i < length; i++) {
var value = array[i],
// 如果指定了迭代函数
// 则对数组每一个元素进行迭代
computed = iteratee ? iteratee(value, i, array) : value;
// 如果是有序数组,则当前元素只需跟上一个元素对比即可
// 用 seen 变量保存上一个元素
if (isSorted) {
// 如果 i === 0,则直接 push
// 否则比较当前元素是否和前一个元素相等
if (!i || seen !== computed) result.push(value);
// seen 保存当前元素,供下一次对比
seen = computed;
} else if (iteratee) {
// 如果 seen[] 中没有 computed 这个元素值
if (!_.contains(seen, computed)) {
seen.push(computed);
result.push(value);
}
} else if (!_.contains(result, value)) {
// 如果不用经过迭代函数计算,也就不用 seen[] 变量了
result.push(value);
}
}
外面的循环遍历数组元素,对于每个元素,如果数组有序,则和前一个元素比较,如果相同,则已经出现过,不加入到结果数组中,否则则加入。而如果有迭代函数,则计算传入迭代函数后的值,对值去重,调用 _.contains 方法,而该方法的核心就是调用 _.indexOf 方法,和我们上面说的方法一异曲同工。
关于 _.unique 方法的详细代码,可以参考 https://github.com/hanzichi/underscore-analysis/blob/master/underscore-1.8.3.js/src/underscore-1.8.3.js#L519-L547
underscore源码翻译版本中,224行翻译说javascript中数组的最大长度为Math.pow(2,53)-1,但是我在控制台上测试过,出现异常
在ECMA262标准中,length的最大长度应该为 Math.pow(2,32)-1
参考链接
(觉得这部分眼熟的可以直接跳到下一段了...)
最近开始看 underscore.js 源码,并将 underscore.js 源码解读 放在了我的 2016 计划中。
阅读一些著名框架类库的源码,就好像和一个个大师对话,你会学到很多。为什么是 underscore?最主要的原因是 underscore 简短精悍(约 1.5k 行),封装了 100 多个有用的方法,耦合度低,非常适合逐个方法阅读,适合楼主这样的 JavaScript 初学者。从中,你不仅可以学到用 void 0 代替 undefined 避免 undefined 被重写等一些小技巧 ,也可以学到变量类型判断、函数节流&函数去抖等常用的方法,还可以学到很多浏览器兼容的 hack,更可以学到作者的整体设计思路以及 API 设计的原理(向后兼容)。
之后楼主会写一系列的文章跟大家分享在源码阅读中学习到的知识。
欢迎围观~ (如果有兴趣,欢迎 star & watch~)您的关注是楼主继续写作的动力
先说点题外话。
自从 5 月 16 日开始 underscore 系列解读文章,目前已经收获了 160+ star,在这里子迟也感谢大家的支持,并将继续努力分享源码里的干货。有朋友私信我说好几天没看到更新,在此也请大家原谅,毕竟我把它当成了今年的计划之一,而且平时也要上班工作,只能利用闲暇时间,而且楼主本人对文章的质量要求比较高,如果是一律的流水文章,读者学不到什么东西,自己的那关都过不了。其实如果有心,应该能发现 underscore-1.8.3 源码全文注释 一直有在更新(注释行数已经快破 1000 了)。
言归正传,上一章 中我们结束了 Object 扩展方法部分,今天开始来解读 Array 部分的扩展方法。其实 JavaScript 中的数组是我最喜欢的类型,能模拟栈、队列等数据结构,还能随意插入元素(splice),非常的灵活,这点做过 leetcode 的应该都深有体会(这里也顺便安利下我的 leetcode 题解 Repo https://github.com/hanzichi/leetcode)。
今天要讲的是,如何在数组中寻找元素,对应 underscore 中的 _.findIndex,_.findLastIndex,_.indexOf,_.lastIndexOf 以及 _.sortIndex 方法。
等等,是不是有点眼熟,没错,JavaScript 中已经部署了 indexOf 方法(ES5)以及 findIndex 方法(ES6),这点不介绍了,大家可以自行学习。
我们先来看 _.findIndex 和 _.findLastIndex 函数。如果了解过 Array.prototype.findIndex() 方法,会非常容易。_.findIndex 的作用就是从一个数组中找到第一个满足某个条件的元素,_.findLastIndex 则是找到最后一个(或者说倒序查找)。
举个简单的例子:
var arr = [1, 3, 5, 2, 4, 6];
var isEven = function(num) {
return !(num & 1);
};
var idx = _.findIndex(arr, isEven);
// => 3
直接看源码,注释已经写的非常清楚了。这里要注意这个 predicate 函数,其实就是把数组中的元素传入这个参数,返回一个布尔值。如果返回 true,则表示满足这个条件,如果 false 则相反。
// Generator function to create the findIndex and findLastIndex functions
// dir === 1 => 从前往后找
// dir === -1 => 从后往前找
function createPredicateIndexFinder(dir) {
// 经典闭包
return function(array, predicate, context) {
predicate = cb(predicate, context);
var length = getLength(array);
// 根据 dir 变量来确定数组遍历的起始位置
var index = dir > 0 ? 0 : length - 1;
for (; index >= 0 && index < length; index += dir) {
// 找到第一个符合条件的元素
// 并返回下标值
if (predicate(array[index], index, array)) return index;
}
return -1;
};
}
// Returns the first index on an array-like that passes a predicate test
// 从前往后找到数组中 `第一个满足条件` 的元素,并返回下标值
// 没找到返回 -1
// _.findIndex(array, predicate, [context])
_.findIndex = createPredicateIndexFinder(1);
// 从后往前找到数组中 `第一个满足条件` 的元素,并返回下标值
// 没找到返回 -1
// _.findLastIndex(array, predicate, [context])
_.findLastIndex = createPredicateIndexFinder(-1);
接下来看 _.sortIndex 方法,这个方法无论使用还是实现都非常的简单。如果往一个有序数组中插入元素,使得数组继续保持有序,那么这个插入位置是?这就是这个方法的作用,有序,很显然用二分查找即可。不多说,直接上源码。
// _.sortedIndex(list, value, [iteratee], [context])
_.sortedIndex = function(array, obj, iteratee, context) {
// 注意 cb 方法
// iteratee 为空 || 为 String 类型(key 值)时会返回不同方法
iteratee = cb(iteratee, context, 1);
// 经过迭代函数计算的值
var value = iteratee(obj);
var low = 0, high = getLength(array);
while (low < high) {
var mid = Math.floor((low + high) / 2);
if (iteratee(array[mid]) < value) low = mid + 1; else high = mid;
}
return low;
};
最后我们说说 _.indexOf 和 _.lastIndexOf 方法。
ES5 引入了 indexOf 和 lastIndexOf 方法,但是 IE < 9 不支持,面试时让你写个 Polyfill,你会怎么做(可以把 underscore 的实现看做 Polyfill)?如何能让面试官满意?首先如果分开来写,即两个方法相对独立地写,很显然代码量会比较多,因为两个方法功能相似,所以可以想办法调用一个方法,将不同的部分当做参数传入,减少代码量。其次,如果数组已经有序,是否可以用更快速的二分查找算法?这点会是加分项。
源码实现:
// Generator function to create the indexOf and lastIndexOf functions
// _.indexOf = createIndexFinder(1, _.findIndex, _.sortedIndex);
// _.lastIndexOf = createIndexFinder(-1, _.findLastIndex);
function createIndexFinder(dir, predicateFind, sortedIndex) {
// API 调用形式
// _.indexOf(array, value, [isSorted])
// _.indexOf(array, value, [fromIndex])
// _.lastIndexOf(array, value, [fromIndex])
return function(array, item, idx) {
var i = 0, length = getLength(array);
// 如果 idx 为 Number 类型
// 则规定查找位置的起始点
// 那么第三个参数不是 [isSorted]
// 所以不能用二分查找优化了
// 只能遍历查找
if (typeof idx == 'number') {
if (dir > 0) { // 正向查找
// 重置查找的起始位置
i = idx >= 0 ? idx : Math.max(idx + length, i);
} else { // 反向查找
// 如果是反向查找,重置 length 属性值
length = idx >= 0 ? Math.min(idx + 1, length) : idx + length + 1;
}
} else if (sortedIndex && idx && length) {
// 能用二分查找加速的条件
// 有序 & idx !== 0 && length !== 0
// 用 _.sortIndex 找到有序数组中 item 正好插入的位置
idx = sortedIndex(array, item);
// 如果正好插入的位置的值和 item 刚好相等
// 说明该位置就是 item 第一次出现的位置
// 返回下标
// 否则即是没找到,返回 -1
return array[idx] === item ? idx : -1;
}
// 特判,如果要查找的元素是 NaN 类型
// 如果 item !== item
// 那么 item => NaN
if (item !== item) {
idx = predicateFind(slice.call(array, i, length), _.isNaN);
return idx >= 0 ? idx + i : -1;
}
// O(n) 遍历数组
// 寻找和 item 相同的元素
// 特判排除了 item 为 NaN 的情况
// 可以放心地用 `===` 来判断是否相等了
for (idx = dir > 0 ? i : length - 1; idx >= 0 && idx < length; idx += dir) {
if (array[idx] === item) return idx;
}
return -1;
};
}
// Return the position of the first occurrence of an item in an array,
// or -1 if the item is not included in the array.
// If the array is large and already in sort order, pass `true`
// for **isSorted** to use binary search.
// _.indexOf(array, value, [isSorted])
// 找到数组 array 中 value 第一次出现的位置
// 并返回其下标值
// 如果数组有序,则第三个参数可以传入 true
// 这样算法效率会更高(二分查找)
// [isSorted] 参数表示数组是否有序
// 同时第三个参数也可以表示 [fromIndex] (见下面的 _.lastIndexOf)
_.indexOf = createIndexFinder(1, _.findIndex, _.sortedIndex);
// 和 _indexOf 相似
// 反序查找
// _.lastIndexOf(array, value, [fromIndex])
// [fromIndex] 参数表示从倒数第几个开始往前找
_.lastIndexOf = createIndexFinder(-1, _.findLastIndex);
这里有一点要注意,_.indexOf 方法的第三个参数可以表示 [fromIndex] 或者 [isSorted],而 _.lastIndexOf 的第三个参数只能表示 [fromIndex],我们从代码中便可以轻易看出:
_.indexOf = createIndexFinder(1, _.findIndex, _.sortedIndex);
_.lastIndexOf = createIndexFinder(-1, _.findLastIndex);
关于这点我也百思不得其解,不知道做这个限制是为了什么考虑,欢迎探讨~
最后给出本文涉及的五个方法的源码位置 https://github.com/hanzichi/underscore-analysis/blob/master/underscore-1.8.3.js/src/underscore-1.8.3.js#L613-L673
上文 我们聊了聊函数去抖(debounce),去抖的作用简单说是 使连续的函数执行降低到一次(通常情况下此函数为 DOM 事件的回调函数),核心实现也非常简单,重复添加定时器即可(具体可以参考 上文)。本文我们聊聊函数节流(throttle)。
简单的说,函数节流能使得连续的函数执行,变为 固定时间段 间断地执行。
还是以 scroll 事件为例,如果不加以节流控制:
window.onscroll = function() {
console.log('hello world');
};
轻轻滚动下窗口,控制台打印了 N 多个 hello world 字符串。如果 scroll 回调不是简单的打印字符串,而是涉及一些 DOM 操作,这样频繁的调用,低版本浏览器可能就会直接假死,我们希望回调可以间隔时间段触发,比如上面的例子每 1000ms 打印一次,如何实现之?
大概有两种方式(underscore 也并用了这两种方式)。其一是用时间戳来判断是否已到回调该执行时间,记录上次执行的时间戳,然后每次触发 scroll 事件执行回调,回调中判断当前时间戳距离上次执行时间戳的间隔是否已经到达 1000ms,如果是,则执行,并更新上次执行的时间戳,如此循环;第二种方法是使用定时器,比如当 scroll 事件刚触发时,打印一个 hello world,然后设置个 1000ms 的定时器,此后每次触发 scroll 事件触发回调,如果已经存在定时器,则回调不执行方法,直到定时器触发,handler 被清除,然后重新设置定时器。
如果是一般的使用场景,则上面的两个方式大同小异,都可以应用,但是 underscore 考虑了高级配置,即可以选择是否需要响应事件刚开始的那次回调(配置 leading 参数),以及事件结束后的那次回调(配置 trailing 参数)。 还是以 scroll 举例,设置 1000ms 触发一次,并且不配置 leading 和 trailing 参数,那么 scroll 开始的时候会响应回调,scroll 停止后还会触发一次回调。如果配置 {leading: false},那么 scroll 开始的那次回调会被忽略,如果配置 {trailing: false},那么 scroll 结束的后的那次回调会被忽略。需要特别注意的是,两者不能同时配置!
所以说,underscore 的函数节流有三种调用方式,默认的(有头有尾),设置 {leading: false} 的,以及设置 {trailing: false} 的。再来看上面说的 throttle 的两种实现,第一种方式有缺陷,当事件停止触发时,便不能响应回调,所以如果没有设置 {trailing: false} (需要执行最后一次方法)也不能执行最后一次方法,这时我们需要用到定时器;而单纯的定时器方式,也有漏洞,因为使用了定时器延迟执行,所以当事件触发结束时还存在定时器,{trailing: false} 设置无法生效(还会执行最后一次方法)。所以我们需要两者并用。
上 underscore 源码,包含大量注释:
// Returns a function, that, when invoked, will only be triggered at most once
// during a given window of time. Normally, the throttled function will run
// as much as it can, without ever going more than once per `wait` duration;
// but if you'd like to disable the execution on the leading edge, pass
// `{leading: false}`. To disable execution on the trailing edge, ditto.
// 函数节流(如果有连续事件响应,则每间隔一定时间段触发)
// 每间隔 wait(Number) milliseconds 触发一次 func 方法
// 如果 options 参数传入 {leading: false}
// 那么不会马上触发(等待 wait milliseconds 后第一次触发 func)
// 如果 options 参数传入 {trailing: false}
// 那么最后一次回调不会被触发
// **Notice: options 不能同时设置 leading 和 trailing 为 false**
// 示例:
// var throttled = _.throttle(updatePosition, 100);
// $(window).scroll(throttled);
// 调用方式(注意看 A 和 B console.log 打印的位置):
// _.throttle(function, wait, [options])
// sample 1: _.throttle(function(){}, 1000)
// print: A, B, B, B ...
// sample 2: _.throttle(function(){}, 1000, {leading: false})
// print: B, B, B, B ...
// sample 3: _.throttle(function(){}, 1000, {trailing: false})
// print: A, A, A, A ...
// ----------------------------------------- //
_.throttle = function(func, wait, options) {
var context, args, result;
// setTimeout 的 handler
var timeout = null;
// 标记时间戳
// 上一次执行回调的时间戳
var previous = 0;
// 如果没有传入 options 参数
// 则将 options 参数置为空对象
if (!options)
options = {};
var later = function() {
// 如果 options.leading === false
// 则每次触发回调后将 previous 置为 0
// 否则置为当前时间戳
previous = options.leading === false ? 0 : _.now();
timeout = null;
// console.log('B')
result = func.apply(context, args);
// 这里的 timeout 变量一定是 null 了吧
// 是否没有必要进行判断?
if (!timeout)
context = args = null;
};
// 以滚轮事件为例(scroll)
// 每次触发滚轮事件即执行这个返回的方法
// _.throttle 方法返回的函数
return function() {
// 记录当前时间戳
var now = _.now();
// 第一次执行回调(此时 previous 为 0,之后 previous 值为上一次时间戳)
// 并且如果程序设定第一个回调不是立即执行的(options.leading === false)
// 则将 previous 值(表示上次执行的时间戳)设为 now 的时间戳(第一次触发时)
// 表示刚执行过,这次就不用执行了
if (!previous && options.leading === false)
previous = now;
// 距离下次触发 func 还需要等待的时间
var remaining = wait - (now - previous);
context = this;
args = arguments;
// 要么是到了间隔时间了,随即触发方法(remaining <= 0)
// 要么是没有传入 {leading: false},且第一次触发回调,即立即触发
// 此时 previous 为 0,wait - (now - previous) 也满足 <= 0
// 之后便会把 previous 值迅速置为 now
// ========= //
// remaining > wait,表示客户端系统时间被调整过
// 则马上执行 func 函数
// @see https://blog.coding.net/blog/the-difference-between-throttle-and-debounce-in-underscorejs
// ========= //
// console.log(remaining) 可以打印出来看看
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
// 解除引用,防止内存泄露
timeout = null;
}
// 重置前一次触发的时间戳
previous = now;
// 触发方法
// result 为该方法返回值
// console.log('A')
result = func.apply(context, args);
// 引用置为空,防止内存泄露
// 感觉这里的 timeout 肯定是 null 啊?这个 if 判断没必要吧?
if (!timeout)
context = args = null;
} else if (!timeout && options.trailing !== false) { // 最后一次需要触发的情况
// 如果已经存在一个定时器,则不会进入该 if 分支
// 如果 {trailing: false},即最后一次不需要触发了,也不会进入这个分支
// 间隔 remaining milliseconds 后触发 later 方法
timeout = setTimeout(later, remaining);
}
// 回调返回值
return result;
};
};
调用也是非常的简单:
function log() {
console.log('hello world');
}
window.onscroll = _.throttle(log, 1000);
window.onscroll = _.throttle(log, 1000, {leading: false});
window.onscroll = _.throttle(log, 1000, {trailing: false});
有兴趣的可以琢磨下它是如何实现两种方式并用的,可以将我代码块中的三处注释打开看下(分别打印了 A,B 以及 remaining )。
这篇文章并不在我的 underscore 源码解读计划中,直到 @pod4g 同学回复了我的 issue(详见 https://github.com/hanzichi/underscore-analysis/issues/2#issuecomment-227361035)。其实之前也有同学提出 isNaN 有 native 的 function,正好借此文辨析下几个常见的概念、方法,她们是 NaN,Number.NaN,isNaN,Number.isNaN,以及 underscore 中的 _.isNaN,顺便揪出了一个 BUG。
顺便安利,完整的 underscore 源码解读系列文章请戳 https://github.com/hanzichi/underscore-analysis
ok,首先来了解下 NaN 和 Number.NaN 两个属性。
全局属性 NaN 表示 Not-A-Number 的值,顾名思义,就是表示 不是一个数字。
在编码中很少直接使用到 NaN。通常都是在计算失败时,作为 Math 的某个方法的返回值出现的(例如:Math.sqrt(-1))或者尝试将一个字符串解析成数字但失败了的时候(例如:parseInt("blabla"))。这样做的好处是,不会抛出错误,只需要在下一步的运算中判断上个步骤的运算结果是否是 NaN 即可。
接着来看 Number.NaN,这货和 NaN 完全一样。其实,归根结底这俩货都是属于 Number 类型:
Object.prototype.toString.call(NaN)
// "[object Number]"
Object.prototype.toString.call(Number.NaN)
// "[object Number]"
接着来聊 isNaN 和 Number.isNaN 俩方法。
我们都知道,虽然 NaN 作为 Number 类型,但是她不等于她自己, NaN == NaN
或者 NaN === NaN
都会返回 false,那么怎么检测一个 NaN 值呢?答案大家都知道了,isNaN 方法。
isNaN(NaN)
// true
isNaN(undefined)
// true
isNaN({})
// true
isNaN("abc")
// true
好多东西传入 isNaN 的结果都是 true,并不只是 NaN,为什么?因为参数会先被强制转换成 Number 类型,然后再进行判断。
Number(NaN)
// NaN
Number(undefined)
// NaN
Number({})
// NaN
Number("abc")
// NaN
ok,强制转换后其实都变成了 NaN。
那么 Number.isNaN 和 isNaN 有何区别呢?和全局函数 isNaN() 相比,该方法不会强制将参数转换成数字,只有在参数是真正的数字类型,且值为 NaN 的时候才会返回 true。
isNaN = function(value) {
Number.isNaN(Number(value));
}
Number.isNaN = Number.isNaN || function(value) {
return typeof value === "number" && isNaN(value);
}
值得注意的是,Number.isNaN 是 ES6 引入的,可以用上面的 Polyfill。
最后来看看 underscore 对于 _.isNaN 的实现。
写代码首先得看需求,我们先看看 _.isNaN 的作用,查阅 API 文档 http://underscorejs.org/#isNaN:
this is not the same as the native isNaN function, which will also return true for many other not-number values, such as
undefined
.
文档指出,_.isNaN 和 native 的 isNaN 并不一样,必须是个 Number 类型(才可能返回 true),等等,似乎和 Number.isNaN 一样?且慢下结论。
我们来看看 edge 版本对其的实现(https://github.com/jashkenas/underscore/blob/master/underscore.js):
// Is the given value `NaN`?
_.isNaN = function(obj) {
return _.isNumber(obj) && isNaN(obj);
};
obj 得是个 Number 类型,并且能通过 isNaN 函数的判断,才能返回 true。其实能通过这个函数的,只有两个值,NaN 和 new Number(NaN)(当然还有 Number.NaN,前面说了,NaN 和 Number.NaN 是一样的东西,下同)。
而能通过 Number.isNaN 函数的只有 NaN。(Number.isNaN(new Number(NaN)
会返回 false)
但是我看的 1.8.3 其实是这样实现的:
_.isNaN = function(obj) {
return _.isNumber(obj) && obj !== +obj;
};
其实这是有 BUG 的,很显然 new Number(0) 并不应该是 Not-A-Number。
_.isNaN(new Number(0));
// true
为什么会这样写?这引发了我的好奇,找了下历史记录,是为了修复这个 issue https://github.com/jashkenas/underscore/issues/749。该 issue 认为,_.isNaN(new Number(NaN))
应该返回 true。
我们可以看下再之前的版本对于 _.isNaN 的实现(jashkenas/underscore@6ebb43f9b3ba88cc0cca712383534619b82f7e9b):
_.isNaN = function(obj) {
return obj !== obj;
};
我又翻了下当时的测试数据(https://github.com/jashkenas/underscore/blob/6ebb43f9b3ba88cc0cca712383534619b82f7e9b/test/objects.js),发现当时没有类似 new Number(0) 的测试数据(现在已经有了)。
对于 NaN 的判断,如果只针对 Number 类型,用 underscore 最新版的 _.isNaN 判断完全没有问题,或者用 ES6 的 Number.isNaN,两者的区别就在于一个 new Number(NaN),不过话又说回来,没人会这么蛋疼去这样 new 一个 NaN 吧?
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.