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

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

结果是第一秒输出6,第二秒输出7...
我们也可以这么做

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

或者这样做

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

结果第一秒输出1,第二秒输出2...

这是为什么呢?第二个例子说明了闭包内使用的是外部变量的引用,他们都使用了同一个引用,所以最终输出取决与此时这个引用的值。第三个例子,每次循环都会新建一个变量 j,分别被每个闭包所引用,这些引用互不干扰,我们可以在 console.log(j) 后面加上 j++,结果是不会有变化的。

继承与原型链

在 javaScript 中,每个对象都有一个指向它的原型(prototype)对象的内部链接。这个原型对象又有自己的原型,直到某个对象的原型为 null 为止(也就是不再有原型指向),组成这条链的最后一环。这种一级一级的链结构就称为原型链(prototype chain)。

第一次接触 JavaScript 的原型链是在使用 Angular 的时候,如果有看到前面我的一篇写 Angular 的就会看到。在使用 Angular 的 ngIf 和 textarea 时,会创建新的子作用域,子作用域的原型是父级作用域一般就是 scope,以 textarea 为例子,假设一个用户修改评论的 textarea 框,我们首先把原先的评论写了回去,假设我们这样使用

<textarea ng-model="content", name="content" class="form-control" row="3"></textarea>

我们把用户的评论内容放到 $scope.content 里面去。
结果是我们可以看到 textarea 确实一开始就被填入了用户原先的评论,可是如果此时我们更改 textarea 内部的内容,然后提交修改。你会发现 $scope.content 没有发生变化。
这是因为 Angular 默认在 textarea 创建了一个新的子作用域,这个作用域本身一开始并不存在 content 这个值,即没有 hasOwnProperty('content'),但他并不会因此就不做显示了,他会去找其原型,一般是 scope (如果在 ng-if 使用了 textarea,则其原型的原型才是 scope ),如果原型存在 content 则继承原型。所以你会看到初始状态是没问题的,当你修改评论内容时候,此时 textarea 自身的作用与就会新建了一个 content,内容就为你的评论内容,而其原型的 scope 将不再被使用,也不会被修改,所以你会发现 $scope.content 并没有发生变化。
如果 textarea 的原型也不存在 content,它会再往上找,直到原型链最顶端为止,处在原型链最顶端的对象的原型是 null。
以 Angular 的这个例子,我们就介绍完了 JavaScript 的原型链和继承,我们再举个例子说明下。

var stu = {name: "stu", age: 18, school: "SCNU"};
var father = {name: "parent", age: 40, job: "engineer", company: "Google"};
stu.__proto__ = father;
console.log(stu.job)    /* logs "engineer" */
stu.job = "student"
console.log(stu.job)    /* logs "student" */
console.log(father.job) /* logs "engineer" */
for( prop in stu ) {
    console.log(stu.prop);  /* logs stu, 18, SCNU, student, Google */
}

使用for...in...会遍历对象的所有属性,一个解决方法是使用 hasOwnProperty 判断是否是该层的属性。