1. boolean構(gòu)造函數(shù)與邏輯或運算符(||)
如果boolean構(gòu)造函數(shù)的參數(shù)不是一個布爾值,參數(shù)會被轉(zhuǎn)換為布爾值。
如果參數(shù)為以下情況時:
- 0
- -0
- false
- NaN
- undefined
- null
- ''(空字符串)
生成的boolean對象的值為false。
除此以外的任何參數(shù)都會創(chuàng)建值為true的boolean對象。
注意不要將原始值true false,和值為true false的boolean對象相混淆。
例如一個名為a,值為false的boolean對象實際上擁有如下屬性:
{
__proto__:Boolean,
[[PrimitiveValue]]:false
}
因此:
a === false; //false
a == false; //true
另外注意 //TODO
new Boolean('') === false //false
Boolean('') === false //true
邏輯或運算符(||)
邏輯運算符的操作數(shù)如果不是boolean類型,會先被轉(zhuǎn)換成boolean數(shù)。
過程與boolean構(gòu)造函數(shù)轉(zhuǎn)換參數(shù)時相似。
因此在定義一個函數(shù)時,可以利用(||)代替if運算符給可選參數(shù)設定默認值,例如:
function fun(o, /* optional */a) {
if (a === undefined) {a = [];}
...
};
用與運算代替
function fun(o, /*optional */a) {
a = a || [];
...
};
但實際上第二種寫法存在缺陷,當傳入的值本身就是false時,傳入的值卻被認為是無效的,變量被設為了默認值。
在ES6中可以直接為變量設定默認值了,例如:
function fun(o, a = []) {
};
所以TMD寫了這么一長串其實除了幫自己鞏固了一遍以外完全沒有什么卵用
2. 預定義與詞法作用域
先來看一段代碼
var a = 'global';
var getValue = function () {
console.log(a); //輸出undefined
var a = 'local';
console.log(a); //輸出local
};
getValue();
如果不了解js解析器的預定義行為,以及js的詞法作用域,可能會覺得第一個console.log(a);將會輸出'global'。
下面來解釋js解析器的預定義
在上述代碼中getValue()的詞法作用域就是它定義時的這一段語句(從var getValue = ...到console.log(a); };)。
在這段代碼中有一個對函數(shù)內(nèi)局部變量a的聲明,js解析器提前對var定義的變量進行了初始化,但是沒有賦值。
即實際上調(diào)用getValue()時作用域中存在一個a變量,它的值為undefined。
第一個console.log()執(zhí)行時,會現(xiàn)在當前作用域里尋找a。
而a已經(jīng)被js解析器提前初始化卻沒有賦值,所以找到了一個變量a = undefined;。
因此第一個console.log()輸出undefined。
如果當前作用域內(nèi)沒有所需要的變量(在本例子中是變量a),將會一直在作用域鏈上一級中尋找這個變量,直到找到該變量,或者抵達最外層作用域為止。關于詞法作用域?qū)⒃谙鹿?jié)詳細介紹。
所以如果想讓console.log()輸出'global',應該:
var a = 'global';
var getValue = function () {
console.log(a); //輸出'global'
};
getValue();
本節(jié)參考:
- javascript基礎拾遺——詞法作用域
- Javascript權(quán)威指南第六版淘寶前端團隊翻譯版第3.10.1節(jié)-p58
3. 詞法作用域(lexical scoping)與閉包(closure)
首先為了讓我們更加清楚詞法作用域的概念,這里引入動態(tài)作用域的概念來進行對比
- 詞法作用域就是作用域在定義階段已經(jīng)決定好了,是由代碼中變量和塊作用域?qū)懺谀睦餂Q定的。
無論函數(shù)在哪里,通過什么方式被調(diào)用,它的詞法作用域都只由函數(shù)被聲明時所處的位置決定。
作用域和作用域鏈不會再發(fā)生變化。 - 動態(tài)作用域并不關心函數(shù)和作用域是如何、在何處聲明的,只關心它們從何處調(diào)用。動態(tài)作用域的
作用域鏈是基于調(diào)用棧的,而不是由代碼中的作用域嵌套的位置。
接下來請看一個例子:
var a = 2;
function foo() {
console.log( a );
}
function bar() {
var a = 3;
foo();
}
bar();
如果這段代碼處于詞法作用域中,變量
a首先在foo()函數(shù)中查找,沒有找到,于是沿著作用域鏈往上級作用域繼續(xù)查找,找到了值為2的變量a,控制臺輸出2。如果這段代碼處于動態(tài)作用域中,變量
a首先在foo()函數(shù)中查找,沒有找到,于是沿著調(diào)用棧找到調(diào)用foo()的函數(shù)bar(),在bar()中尋找變量a,找到了值為3的變量a,控制臺輸出3。
通過上述例子明確地展現(xiàn)出了兩種作用域的區(qū)別,為了方便大家記憶,簡而言之:
- 詞法作用域在定義時確定。
- 動態(tài)作用域在運行時確定。
JavaScript中的作用域
- 特別注意JavaScript中沒有塊級作用域(實際上是ES5中沒有,ES6已經(jīng)支持),只有函數(shù)可以限定一個變量的作用域。
- 根據(jù)詞法作用域的作用域鏈的特點,子作用域可以訪問父作用域。
接下來看一道習題,思考10秒再看答案:
if(! "a" in window) {
var a = "233";
}
console.log(a);
正確答案是輸出undefined。
因為var a = "233";這條語句雖然在if語句的大括號中,但JavaScript中沒有塊級作用域,因此這條語句實際上聲明了一個處于全局作用域中的變量。由于JS解析器的預定義特點,變量a被提前聲明,但由于不滿足if語句的條件,沒有進行賦值,因此變量a的值為undefined,所以控制臺輸出undefined。
接下來討論閉包
假設我們現(xiàn)在需要一個計數(shù)器函數(shù)counter,每次只需要調(diào)用它,計數(shù)器就加一:
var a = 0;
var counter = function () {
return a++;
};
現(xiàn)在我們每次執(zhí)行counter(),就可以為計數(shù)器加一了。
counter(); // a: 1
counter(); // a: 2
counter(); // a: 3
但是在實際開發(fā)中會遇到一些問題,比如在另一個地方有一個函數(shù)fun:
var fun = function () {
a *= a;
};
...
...
fun(); // a: 9糟糕!不小心修改了a。
在實際開發(fā)中萬一遇到這種問題會非常麻煩,特別是已經(jīng)開發(fā)了一段時間,有些變量名你已經(jīng)忘記了的時候。
那對于這種幾乎專用的變量,我們能不能想辦法把它們藏起來,現(xiàn)在來修改一下counter函數(shù)
var counter = function () {
var a = 0; //不再在全局聲明變量a,在counter的作用域里面聲明,保護他!
return a++;
};
好,我們成功地保護了變量a!這下外部的其他函數(shù)沒法對它進行操作了。
不過等等,現(xiàn)在這樣每次counter執(zhí)行完,里面的變量不會再被其他地方引用,
就被垃圾回收機制回收了,再次執(zhí)行的時候,變量又會變回默認值,沒有辦法計數(shù)了。
我們得想一個辦法讓變量a不會被回收:
var counter = function () {
var a = 0;
var foo = function() {
return a++; //foo可以訪問到counter的作用域,可以訪問到a。
};
foo();
};
現(xiàn)在foo已經(jīng)引用了a,但是foo被回收的時候,a也會被回收了,仍然沒有解決問題。
var counter = function () {
var a = 0;
return function() { //我們讓counter返回下面這個匿名函數(shù)。
return a++; //這個匿名函數(shù)可以訪問到counter的作用域,可以訪問到a。
};
};
var counter1 = counter();讓一個全局變量引用counter的返回值,即剛才的匿名函數(shù)。
好了,現(xiàn)在形成了這樣的一個關系:
counter1->匿名函數(shù)->變量a
由于counter1是在全局聲明的,他的生命周期直到整個程序才結(jié)束。
現(xiàn)在我們既可以長久的保存變量a又不會造成全局污染。
最后我們得到的這一段代碼,就是一個閉包。
總結(jié)一下,閉包就是一個可以訪問其他函數(shù)作用域的函數(shù),并且能保持那個作用域里的變量不被釋放,
可以重復使用,并且只能由調(diào)用閉包者來訪問。
所以剛才var counter1 = counter();這一步是形成閉包最重要的一步。
閉包的特點:
- 不容易被釋放
- 占用更多內(nèi)存
使用閉包的場景:
- 使用閉包可以在JavaScript中模擬塊級作用域
- 閉包可以用于在對象中創(chuàng)建私有變量
這是我對閉包最簡單的理解,當然閉包還有其更深層次的理解,需要了解JS的執(zhí)行環(huán)境(execution context)、活動對象(activation object)、垃圾回收(garbege collection)以及作用域(scope)和作用域鏈(scope chain)的運行機制。
本節(jié)參考:
- JavaScript高級之詞法作用域和作用域鏈
- 深入理解javascript作用域系列第一篇——內(nèi)部原理
- 深入理解javascript作用域系列第二篇——詞法作用域和動態(tài)作用域
- 動態(tài)作用域和詞法域的區(qū)別是什么?
- JS閉包的真正意義?
- JS閉包的真正意義? - 七律的回答
- Javascript閉包——懂不懂由你,反正我是懂了
- 圖解JS閉包 - 知乎專欄
- JavaScript內(nèi)存管理 - MDN web doc