Node.js-作用域與閉包:this,var,(function () {})

2018-09-28 22:47 更新

作用域與閉包:this,var,(function () {})

目標(biāo)

無(wú)具體目標(biāo)

知識(shí)點(diǎn)

  1. 理解 js 中 var 的作用域
  2. 了解閉包的概念
  3. 理解 this 的指向

課程內(nèi)容

var 作用域

先來(lái)看個(gè)簡(jiǎn)單的例子:

var parent = function () {
  var name = "parent_name";
  var age = 13;

  var child = function () {
    var name = "child_name";
    var childAge = 0.3;

    // => child_name 13 0.3
    console.log(name, age, childAge);
  };

  child();

  // will throw Error
  // ReferenceError: childAge is not defined
  console.log(name, age, childAge);
};

parent();

直覺(jué)地,內(nèi)部函數(shù)可以訪(fǎng)問(wèn)外部函數(shù)的變量,外部不能訪(fǎng)問(wèn)內(nèi)部函數(shù)的變量。上面的例子中內(nèi)部函數(shù) child 可以訪(fǎng)問(wèn)變量 age,而外部函數(shù) parent 不可以訪(fǎng)問(wèn) child 中的變量 childAge,因此會(huì)拋出沒(méi)有定義變量的異常。

有個(gè)重要的事,如果忘記var,那么變量就被聲明為全局變量了。

function foo() {
  value = "hello";
}
foo();
console.log(value); // 輸出hello
console.log(global.value) // 輸出hello

這個(gè)例子可以很正常的輸出 hello,是因?yàn)?value 變量在定義時(shí),沒(méi)有使用 var 關(guān)鍵詞,所以被定義成了全局變量。在 Node 中,全局變量會(huì)被定義在 global 對(duì)象下;在瀏覽器中,全局變量會(huì)被定義在 window 對(duì)象下。

如果你確實(shí)要定義一個(gè)全局變量的話(huà),請(qǐng)顯示地定義在 global 或者 window 對(duì)象上。

這類(lèi)不小心定義全局變量的問(wèn)題可以被 jshint 檢測(cè)出來(lái),如果你使用 sublime 編輯器的話(huà),記得裝一個(gè) SublimeLinter 插件,這是插件支持多語(yǔ)言的語(yǔ)法錯(cuò)誤檢測(cè),js 的檢測(cè)是原生支持的。

JavaScript 中,變量的局部作用域是函數(shù)級(jí)別的。不同于 C 語(yǔ)言,在 C 語(yǔ)言中,作用域是塊級(jí)別的。JavaScript 中沒(méi)有塊級(jí)作用域。

js 中,函數(shù)中聲明的變量在整個(gè)函數(shù)中都有定義。比如如下代碼段,變量 i 和 value 雖然是在 for 循環(huán)代碼塊中被定義,但在代碼塊外仍可以訪(fǎng)問(wèn) i 和 value。

function foo() {
  for (var i = 0; i < 10; i++) {
    var value = "hello world";
  }
  console.log(i); //輸出10
  console.log(value);//輸出hello world
}
foo();

所以有種說(shuō)法是:應(yīng)該提前聲明函數(shù)中需要用到的變量,即,在函數(shù)體的頂部聲明可能用到的變量,這樣就可以避免出現(xiàn)一些奇奇怪怪怪的 bug。

但我個(gè)人不喜歡遵守這一點(diǎn),一般都是現(xiàn)用現(xiàn)聲明的。這類(lèi)錯(cuò)誤的檢測(cè)交給 jshint 來(lái)做就好了。

閉包

閉包這個(gè)概念,在函數(shù)式編程里很常見(jiàn),簡(jiǎn)單的說(shuō),就是使內(nèi)部函數(shù)可以訪(fǎng)問(wèn)定義在外部函數(shù)中的變量。

假如我們要實(shí)現(xiàn)一系列的函數(shù):add10,add20,它們的定義是 int add10(int n)。

為此我們構(gòu)造了一個(gè)名為 adder 的構(gòu)造器,如下:

var adder = function (x) {
  var base = x;
  return function (n) {
    return n + base;
  };
};

var add10 = adder(10);
console.log(add10(5));

var add20 = adder(20);
console.log(add20(5));

每次調(diào)用 adder 時(shí),adder 都會(huì)返回一個(gè)函數(shù)給我們。我們傳給 adder 的值,會(huì)保存在一個(gè)名為 base 的變量中。由于返回的函數(shù)在其中引用了 base 的值,于是 base 的引用計(jì)數(shù)被 +1。當(dāng)返回函數(shù)不被垃圾回收時(shí),則 base 也會(huì)一直存在。

我暫時(shí)想不出什么實(shí)用的例子來(lái),如果想深入理解這塊,可以看看這篇 http://coolshell.cn/articles/6731.html

閉包的一個(gè)坑

for (var i = 0; i < 5; i++) {
  setTimeout(function () {
    console.log(i);
  }, 5);
}

上面這個(gè)代碼塊會(huì)打印五個(gè) 5 出來(lái),而我們預(yù)想的結(jié)果是打印 1 2 3 4 5。

之所以會(huì)這樣,是因?yàn)?setTimeout 中的 i 是對(duì)外層 i 的引用。當(dāng) setTimeout 的代碼被解釋的時(shí)候,運(yùn)行時(shí)只是記錄了 i 的引用,而不是值。而當(dāng) setTimeout 被觸發(fā)時(shí),五個(gè) setTimeout 中的 i 同時(shí)被取值,由于它們都指向了外層的同一個(gè) i,而那個(gè) i 的值在迭代完成時(shí)為 5,所以打印了五次 5。

為了得到我們預(yù)想的結(jié)果,我們可以把 i 賦值成一個(gè)局部的變量,從而擺脫外層迭代的影響。

for (var i = 0; i < 5; i++) {
  (function (idx) {
    setTimeout(function () {
      console.log(idx);
    }, 5);
  })(i);
}

this

在函數(shù)執(zhí)行時(shí),this 總是指向調(diào)用該函數(shù)的對(duì)象。要判斷 this 的指向,其實(shí)就是判斷 this 所在的函數(shù)屬于誰(shuí)。

在《javaScript語(yǔ)言精粹》這本書(shū)中,把 this 出現(xiàn)的場(chǎng)景分為四類(lèi),簡(jiǎn)單的說(shuō)就是:

  • 有對(duì)象就指向調(diào)用對(duì)象
  • 沒(méi)調(diào)用對(duì)象就指向全局對(duì)象
  • 用new構(gòu)造就指向新對(duì)象
  • 通過(guò) apply 或 call 或 bind 來(lái)改變 this 的所指。

1)函數(shù)有所屬對(duì)象時(shí):指向所屬對(duì)象

函數(shù)有所屬對(duì)象時(shí),通常通過(guò) . 表達(dá)式調(diào)用,這時(shí) this 自然指向所屬對(duì)象。比如下面的例子:

var myObject = {value: 100};
myObject.getValue = function () {
  console.log(this.value);  // 輸出 100

  // 輸出 { value: 100, getValue: [Function] },
  // 其實(shí)就是 myObject 對(duì)象本身
  console.log(this);

  return this.value;
};

console.log(myObject.getValue()); // => 100

getValue() 屬于對(duì)象 myObject,并由 myOjbect 進(jìn)行 . 調(diào)用,因此 this 指向?qū)ο?myObject

2) 函數(shù)沒(méi)有所屬對(duì)象:指向全局對(duì)象

var myObject = {value: 100};
myObject.getValue = function () {
  var foo = function () {
    console.log(this.value) // => undefined
    console.log(this);// 輸出全局對(duì)象 global
  };

  foo();

  return this.value;
};

console.log(myObject.getValue()); // => 100

在上述代碼塊中,foo 函數(shù)雖然定義在 getValue 的函數(shù)體內(nèi),但實(shí)際上它既不屬于 getValue 也不屬于 myObject。foo 并沒(méi)有被綁定在任何對(duì)象上,所以當(dāng)調(diào)用時(shí),它的 this 指針指向了全局對(duì)象 global。

據(jù)說(shuō)這是個(gè)設(shè)計(jì)錯(cuò)誤。

3)構(gòu)造器中的 this:指向新對(duì)象

js 中,我們通過(guò) new 關(guān)鍵詞來(lái)調(diào)用構(gòu)造函數(shù),此時(shí) this 會(huì)綁定在該新對(duì)象上。


var SomeClass = function(){
  this.value = 100;
}

var myCreate = new SomeClass();

console.log(myCreate.value); // 輸出100

順便說(shuō)一句,在 js 中,構(gòu)造函數(shù)、普通函數(shù)、對(duì)象方法、閉包,這四者沒(méi)有明確界線(xiàn)。界線(xiàn)都在人的心中。

4) apply 和 call 調(diào)用以及 bind 綁定:指向綁定的對(duì)象

apply() 方法接受兩個(gè)參數(shù)第一個(gè)是函數(shù)運(yùn)行的作用域,另外一個(gè)是一個(gè)參數(shù)數(shù)組(arguments)。

call() 方法第一個(gè)參數(shù)的意義與 apply() 方法相同,只是其他的參數(shù)需要一個(gè)個(gè)列舉出來(lái)。

簡(jiǎn)單來(lái)說(shuō),call 的方式更接近我們平時(shí)調(diào)用函數(shù),而 apply 需要我們傳遞 Array 形式的數(shù)組給它。它們是可以互相轉(zhuǎn)換的。

var myObject = {value: 100};

var foo = function(){
  console.log(this);
};

foo(); // 全局變量 global
foo.apply(myObject); // { value: 100 }
foo.call(myObject); // { value: 100 }

var newFoo = foo.bind(myObject);
newFoo(); // { value: 100 }

完。

以上內(nèi)容是否對(duì)您有幫助:
在線(xiàn)筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)