Underscore 源码学习(三)

这次主要说剩余参数。
在 ES5 中,如果想要函数接收任意数量的参数,必须使用特殊变量 arguments,举个例子,我们要实现一个加法函数,要求第一个数乘2,然后与其他数相加。

1
2
3
4
5
6
7
function add() {
var sum = arguments[0] * 2;
for(var i = 1; i < arguments.length; i++) {
sum += arguments[i];
}
return sum;
}

在 ES6 中,我们可以使用 ... 操作符,例如:

1
2
3
4
5
6
7
function add(first, ...args) {
var sum = first*2;
for(let arg of args) {
sum += arg;
}
return sum;
}

使用 ES5 我们无法给函数定义参数,而只能通过 arguments 来获取参数,这样写明显带来了可读性的降低。而 ES6 我们就可以在函数声明里面写明参数,对于不定长的参数,则可以使用 ... 操作符。
... 还有另一个常用的应用场景,比如下面例子:

1
2
3
4
5
6
7
8
9
10
function test() {
console.log(arguments);
console.log(arguments instanceof Array);
console.log(arguments instanceof Object);
}
test(1, 2, 3);
// Result:
[1, 2, 3]
false
true

如果细看输出的[1, 2, 3]会发现他是这样的:
result1.png
我们再试试下面的

1
2
3
4
5
6
7
8
var arr = [1, 2, 3];
console.log(arr);
console.log(arr instanceof Array);
console.log(arr instanceof Object);
// Result:
[1, 2, 3]
true
true

再看下[1, 2, 3]这行输出里面是什么:
result2.png
instanceof 我们就知道了 arguments 并不是真正的数组。伪数组实质是一个对象。
要把一个伪数组转为数组,可以这样用

1
var arr = Array.prototype.slice.call(arguments);

上面这种做法在很多地方都可以看到。除了上面这样做之外,我们还可以使用 ES6 的 Array.from 来处理,如下:

1
var arr = Array.from(arguments);

但在 ES6 中,我们使用 ... 运算符并不存在这个问题,比如上面第二个例子,args 是一个数组。
鉴于此,我们应该尽量使用 ES6 剩余参数写法和 Array.from 的写法,因为这样更容易理解,而且写起来更简洁。
另外,我们还可以使用 ... 操作符来复制数组,如下:

阅读更多

Underscore 源码学习(二)

Underscore 源码的学习落下了好几天,因为前几天一直正在重构项目和搞 React,不过这几天应该会花较多时间在 Underscore 上面了。
这次主要说下 Underscore 两个比较重要的函数吧,一个是optimizeCb,另一个是cb,这两个花了我挺长时间看的,而且是整个 Underscore 非常重要的函数,后面很多地方都使用到了它。

optimizeCb 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var optimizeCb = function(func, context, argCount) {
if (context === void 0) return func;
switch (argCount == null ? 3 : argCount) {
case 1: return function(value) {
return func.call(context, value);
};
case 3: return function(value, index, collection) {
return func.call(context, value, index, collection);
};
case 4: return function(accumulator, value, index, collection) {
return func.call(context, accumulator, value, index, collection);
};
}
return function() {
return func.apply(context, arguments);
};
};

这个地方 switch 只是一个性能的优化,其实简化来看就是这样的

1
2
3
4
5
6
var optimizeCb = function(func, context, argCount) {
if (context === void 0) return func;
return function() {
return func.apply(context, arguments);
};
};

之所以有那段 switch 前面一篇已经有提到了,只是一个优化而已。使用 call 快于 apply。不过好像最新的 Chrome 已经可以自己优化这个过程,但为了提升性能,加上也无妨。
解释下段代码的意思,字如起名 optimizeCb 优化回调。这个函数传入三个参数依次是函数,上下文,参数个数。如果没有指定上下文则返回函数本身,如果有,则对该上下文绑定到传入的函数,根据传入的参数个数,在做一个性能优化。这个函数就是这个意思。我们看下他的使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
_.each = _.forEach = function(obj, iteratee, context) {
iteratee = optimizeCb(iteratee, context);
var i, length;
if (isArrayLike(obj)) {
for (i = 0, length = obj.length; i < length; i++) {
iteratee(obj[i], i, obj);
}
} else {
var keys = _.keys(obj);
for (i = 0, length = keys.length; i < length; i++) {
iteratee(obj[keys[i]], keys[i], obj);
}
}
return obj;
};

这个函数是用来实现数组或者对象的遍历的,他是怎么做到呢?
首先是

1
iteratee = optimizeCb(iteratee, context);

阅读更多

一次项目重构

上学期由于期末停工的项目又要继续开展了,然而停了一个多月的时间,我已经看不下去他的代码了,简直惨不忍睹,花了我将近40个小时的时间去做了重构。虽然重构说明有进步了,但是一改就要改几十个页面啊…累觉不爱..说一说这次将近40小时的重构吧。
git-diff

Angular 重构

项目是基于 Angular 的 SPA,项目参考Angular规范进行重构,主要是以下几点:

  • 把控制器的业务逻辑(主要是 HTTP 请求)分离到 Factory
  • Controller 和 Directive 以及 Factory 全部用立即函数包装
  • Controller 和 Directive 以及 Factory 内部书写格式
  • 使用 controllerAs 代替 $scope
  • 全部 JavaScript 文件使用 use strict 严格模式
  • 利用单体做部分数据的缓存
  • 提取大部分可复用模块到 directive
  • 全部 ng-repeat 加上 track by
  • 过大的试图使用 ng-include 进行分离
  • 去掉全部辅助变量,用 angular-promise-buttons 来达到按钮状态变化
  • 去掉全部页面切换动画
  • 手动进行依赖注入
  • 使用 ES6 语法,用 babel 转为 ES5
  • 使用 eslint 来做代码格式检查

之前我几乎没有使用 Factory 这一层,全部业务逻辑都在 Controller 里面做,随着项目越来越大(有26个页面),页面之间函数重复的情况很多,而且控制器太厚,可读性差,给维护带来了巨大的困难。在这次重构之中,我把全部的 HTTP 请求全部放在 Factory 实现,从而做到了以下几点:

  • 函数复用,多个控制器用一个 Factory,避免同个函数多次书写
  • HTTP 请求返回 promise,结合 angular-promise-buttons 做到了按钮状态的自动变化以及过渡效果,去掉了先前实现同样目的的全部辅助变量
  • 对部分相对不变的数据,在第一次缓存后直接在该 Factory 进行缓存,第二次获取的时候直接返回内存中的数据,加快了部分页面的二次加载速度,对跨页面你的同个请求同样有效
  • 容易做单元测试和更改逻辑,因为全部 HTTP 请求都放在 Factory 实现,对后期修改以及代码测试都带来了很大的方便

阅读更多

JavaScript 闭包,继承与原型链

JavaScript 闭包和原型链学习心得,如果有不对的地方望指出。

闭包

什么是闭包,有很多说法,我的理解是一个函数可以记住和使用外部变量,保存这个变量的引用在自己的一个环境之中。
例如:

1
2
3
4
5
6
7
8
9
10
11
function makeAdder(x) {
return function(y) {
return x + y;
};
}
var add5 = makeAdder(5);
var add10 = makeAdder(10);
console.log(add5(2)); // 7
console.log(add10(2)); // 12

这个例子中, makeAdder 是一个函数工厂,add5 和 add10 就是闭包,他们记住了外部变量 x。通常一个函数执行完毕后其运行期上下文会被释放,但由于闭包的属性包含了与运行期上下文作用域链相同的对象引用,导致这个激活对象无法销毁,这就会导致内存消耗,另外,闭包内部的作用域链并不处在闭包作用域链的前端,并且闭包经常使用外部变量的话,导致对象属性的遍历经常到其原型上面去(一个解决方法是把他赋值到闭包自身的作用域上面),从而增加性能消耗。
既然闭包会导致内存增加和性能消耗,那为什么那么多人还使用它呢?上面的例子可能不太能说明问题,我们看下其他例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var Counter = (function() {
var privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment: function() {
changeBy(1);
},
decrement: function() {
changeBy(-1);
},
value: function() {
return privateCounter;
}
}
})();
console.log(Counter.value()); /* logs 0 */
Counter.increment();
Counter.increment();
console.log(Counter.value()); /* logs 2 */
Counter.decrement();
console.log(Counter.value()); /* logs 1 */

这个例子中外部只能通过 value 方法获取 privateCounter 的值,只能通过 increment 和 decrement 方法来改变 privateCounter 的值,无法直接获取到 priavateCounter 和调用 changeBy 函数。这种模式叫 module模式,因为大部分模块都是这样写的,包括 Underscore 也是这样。上篇中就说道了 Underscore 使用了立即执行函数,其用途其一是为了不污染外部变量,因为 JavaScript 是函数作用域,其次它利用了闭包的特性又可以保持函数内部闭包的可调用和被闭包所引用变量在闭包环境中的存在,同时函数内部可以定义一些私有变量和私有方法。我们无需担心这些变量和函数在外部函数执行完毕结束后的失效。
当你看到函数里面又 return 函数时,同时该函数又使用了外部变量,则该函数就是一个闭包。
关于闭包还有一个很容易犯错的地方,比如你想实现第一秒输出1,第二秒输出2,以此类推。

1
2
3
4
5
for (var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, i*1000);
}

但是实际运行结果是第一秒输出6,第二秒输出6…
闭包是记住了外部变量的引用,每次循环都建立了一个 timer 函数,但 console 并还没有被执行,当循环结束后确实是建立了5个计时器或者说5个闭包,但当开始执行 console 的时候,由于这些闭包所引用的 i 此时结果为6,所以会输出5次 6。
更能说明问题些,我们稍作修改下:

阅读更多