2016年7月

Underscore 源码学习(三) - 剩余参数

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

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

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

function add(first, ...args) {
    var sum = first*2;
    for(let arg of args) {
        sum += arg;
    }
    return sum;
}

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

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
我们再试试下面的

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 并不是真正的数组。伪数组实质是一个对象。
要把一个伪数组转为数组,可以这样用

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

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

var arr = Array.from(arguments);

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

- 阅读剩余部分 -

Underscore 源码学习(二) - 迭代

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

optimizeCb 函数

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 只是一个性能的优化,其实简化来看就是这样的

var optimizeCb = function(func, context, argCount) {
    if (context === void 0) return func;
    return function() {
      return func.apply(context, arguments);
    };
};

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

  _.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;
  };

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

iteratee = optimizeCb(iteratee, context);

- 阅读剩余部分 -

一次项目重构

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

git-diff.jpg

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 闭包和原型链学习心得,如果有不对的地方望指出。

闭包

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

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

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,以此类推。

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。
更能说明问题些,我们稍作修改下:

- 阅读剩余部分 -

Underscore 源码学习(一)

暑假打算研究一下 Underscore 源码,我会对一些我觉得比较有意思的点拿出来讨论下,不过我不会过多介绍,也不会去分析 Underscore 的各个方法,但我会附上一些相关的不错的参考资料。由于我也是初学阶段,所以如果有说的不正确的地方望指出。

要点1:立即执行函数

Underscore 的内容都用这么一个东西包装起来了。

(function(){..}());

其实也可以这样写

(function(){})();

Underscore 把全部内容封装在立即执行函数里面,就形成了一个独立的作用域,与外部隔离,并且这样做还形成了闭包,可以模拟私有方法。
推荐阅读:

要点2:兼容浏览器和 Node 环境

浏览器和服务端的一个主要区别是全局对象命名的不同,在浏览器全局变量是 window,在服务端即 Node 环境则是 global。

var root = typeof self == 'object' && self.self === self && self ||
           typeof global == 'object' && global.global === global && global ||
           this;

这个地方之前的写法是这样的

var root = this;

我认为之所以改成前面那种写法,可能是为了确保 root 指向 global 或者 window(self) 。大部分框架和库都采用这种做法,这种做法更加安全。

// 这一步确保self是一个object,这样self.self才不会出错
typeof self == 'object'
// 这一步确保self.self严格等于自身,貌似只有window具备这个特性
// 即window === window.window.window
self.self === self
// 为什么还要进行这一步?
self

推荐阅读:

要点3:提供命名冲突解决方法

Underscore 在给 root 赋值前,先保存了原先 root 的 _ 对象。之所以这样做,是因为可能我们用的其他库也使用了 _ 这个作为命名空间。

var previousUnderscore = root._;

我们结合 Underscore 最下面的这个方法来看。

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

如果 _ 出现了冲突,可以使用下面方法

var _new = _.noConflict();

这样一来应该很明显了,noConflictpreviousUnderscore 即原先的 root._ 重新放回去,然后重新定义 Underscore 命名给 _new,这样就解决了 _ 冲突问题。

- 阅读剩余部分 -

ARP 攻击和无线网卡混杂模式

一次小小的 Hack 尝试...
前段时间换上了 Arch,就开始想这捣鼓一些黑科技。脑海中立即浮现出两个词, monitor 和 ARP 。

ARP 攻击

学过计算机网络了,大概了解ARP攻击这么一回事,但是当我真正去试的时候,才发现局域网原来真的这么脆弱,因为进行ARP攻击实在太容易了。实际上就是几条命令的事情。

主机发现和端口扫描

进行攻击第一步当然就是找目标了。可以使用 nmap 这个工具来进行,这东西简直渗透利器。

nmap -sP 192.168.1.0/24

这样就可以扫描192.168.1.0这个网段里面的所有主机了,以 ping 方式扫描,当然他有很多参数很多用法,网上很多这方面的资料我就不叙述了。
nmap.png

发起攻击

如果是进行ARP攻击,那也是加多三条命令就搞定的事情。

echo 1 >> /proc/sys/net/ipv4/ip_forward
arpspoof -i wlp3s0 -t 192.168.1.110 192.168.1.1
arpspoof -i wlp3s0 -t 192.168.1.1   192.168.1.110

第一条是开启转发,不然会受害者流量到我们这里出不去就断网了。
第二条是告诉受害者说我是网管。
第三条是告诉网管我是受害者。
这样就完成了 ARP 欺骗,图片以后再补。这之后对方的网络就完全在你的监控之下了。这告诉我们使用 https 和不连接免费 wifi 的重要性!

抓包

抓包推荐使用 wireshark 。具体就不介绍了=.=。你还可以使用 Driftnet 这东西来将对方访问的图片在你这边显示出来~

监听空气包

大部分无线网卡都可以设置 monitor 模式,即无线网卡默认接受下全部经过他的以太网帧而不丢弃,这种方式可以抓到附近范围里面的所以以太网帧。在 Arch 下也是三条命令搞定的事情。

ifconfig wlp3s0 down
iwconfig wlp3s0 mode monitor
ifconfig wlp3s0 up

接着同样开启 wireshark 抓包,你会发现很多802.11帧,不过这个并没有什么卵用,理论上应该对于没有加密的帧应该可以直接得到应用层报文才对,但是很奇怪我一直抓不到,即使是对于有加密的 Wifi,我填入 WAPKEY 解密也不行。这个有待继续研究。。。

- 阅读剩余部分 -