JS
系列暫定 27 篇,從基礎,到原型,到異步,到設計模式,到架構模式等,此爲第一篇:是對 var、let、const、解構、展開、函數 的總結。html
let
在不少方面與 var
是類似的,可是 let
能夠幫助你們避免在 JavaScript 裏常見一些問題。const
是對 let
的一個加強,它能阻止對一個變量再次賦值。前端
var
聲明一直以來咱們都是經過 var
關鍵字定義 JavaScript 變量。git
var num = 1; 複製代碼
定義了一個名爲 num
值爲 1
的變量。程序員
咱們也能夠在函數內部定義變量:github
function f() { var message = "Hello, An!"; return message; } 複製代碼
而且咱們也能夠在其它函數內部訪問相同的變量。typescript
function f() { var num = 10; return function g() { var b = num + 1; return b; } } var g = f(); g(); // 11; 複製代碼
上面的例子裏,g
能夠獲取到 f
函數裏定義的 num
變量。 每當 g
被調用時,它均可以訪問到 f
裏的 num
變量。 即便當 g
在 f
已經執行完後才被調用,它仍然能夠訪問及修改 num
。設計模式
function f() { var num = 1; num = 2; var b = g(); num = 3; return b; function g() { return num; } } f(); // 2 複製代碼
對於熟悉其它語言的人來講,var
聲明有些奇怪的做用域規則。 看下面的例子:數組
function f(init) { if (init) { var x = 10; } return x; } f(true); // 10 f(false); // undefined 複製代碼
在這個例子中,變量 x
是定義在 if
語句裏面,可是咱們卻能夠在語句的外面訪問它。markdown
這是由於 var
聲明能夠在包含它的函數,模塊,命名空間或全局做用域內部任何位置被訪問,包含它的代碼塊對此沒有什麼影響。 有些人稱此爲 var
做用域或函數做用域 。 函數參數也使用函數做用域。架構
這些做用域規則可能會引起一些錯誤。 其中之一就是,屢次聲明同一個變量並不會報錯:
function sumArr(arrList) { var sum = 0; for (var i = 0; i < arrList.length; i++) { var arr = arrList[i]; for (var i = 0; i < arr.length; i++) { sum += arr[i]; } } return sum; } 複製代碼
這裏很容易看出一些問題,裏層的 for
循環會覆蓋變量 i
,由於全部 i
都引用相同的函數做用域內的變量。 有經驗的開發者們很清楚,這些問題可能在代碼審查時漏掉,引起無窮的麻煩。
快速的思考一下下面的代碼會返回什麼:
for (var i = 0; i < 10; i++) { setTimeout(function() { console.log(i); }, 100 * i); } 複製代碼
介紹一下,setTimeout
會在若干毫秒的延時後執行一個函數(等待其它代碼執行完畢)。
好吧,看一下結果:
10 10 10 10 10 10 10 10 10 10 複製代碼
不少 JavaScript 程序員對這種行爲已經很熟悉了,但若是你很不解,你並非一我的。 大多數人指望輸出結果是這樣:
0 1 2 3 4 5 6 7 8 9 複製代碼
還記得咱們上面提到的捕獲變量嗎?
咱們傳給
setTimeout
的每個函數表達式實際上都引用了相同做用域裏的同一個i
。
讓咱們花點時間思考一下這是爲何。 setTimeout
在若干毫秒後執行一個函數,而且是在 for
循環結束後。for
循環結束後,i
的值爲 10
。 因此當函數被調用的時候,它會打印出 10
!
一個一般的解決方法是使用當即執行的函數表達式(IIFE)來捕獲每次迭代時i
的值:
for (var i = 0; i < 10; i++) { (function(i) { setTimeout(function() { console.log(i); }, 100 * i); })(i); } 複製代碼
這種奇怪的形式咱們已經司空見慣了。 參數 i
會覆蓋 for
循環裏的 i
,可是由於咱們起了一樣的名字,因此咱們不用怎麼改 for
循環體裏的代碼。
let
聲明如今你已經知道了 var
存在一些問題,這剛好說明了爲何用 let
語句來聲明變量。 除了名字不一樣外, let
與 var
的寫法一致。
let hello = "Hello,An!"; 複製代碼
主要的區別不在語法上,而是語義,咱們接下來會深刻研究。
當用 let
聲明一個變量,它使用的是詞法做用域或塊做用域。 不一樣於使用 var
聲明的變量那樣能夠在包含它們的函數外訪問,塊做用域變量在包含它們的塊或 for
循環以外是不能訪問的。
function f(input) { let a = 100; if (input) { // a 被正常引用 let b = a + 1; return b; } return b; } 複製代碼
這裏咱們定義了2個變量 a
和 b
。 a
的做用域是 f
函數體內,而 b
的做用域是 if
語句塊裏。
在 catch
語句裏聲明的變量也具備一樣的做用域規則。
try { throw "oh no!"; } catch (e) { console.log("Oh well."); } // Error: 'e' doesn't exist here console.log(e); 複製代碼
擁有塊級做用域的變量的另外一個特色是,它們不能在被聲明以前讀或寫。 雖然這些變量始終「存在」於它們的做用域裏,但在直到聲明它的代碼以前的區域都屬於 暫時性死區。 它只是用來講明咱們不能在 let
語句以前訪問它們:
a++; // Uncaught ReferenceError: Cannot access 'a' before initialization let a; 複製代碼
注意一點,咱們仍然能夠在一個擁有塊做用域變量被聲明前獲取它。 只是咱們不能在變量聲明前去調用那個函數。
function foo() { return a; } // 不能在'a'被聲明前調用'foo' // 運行時應該拋出錯誤 foo(); // Uncaught ReferenceError: Cannot access 'a' before initialization let a; 複製代碼
關於暫時性死區的更多信息,查看這裏Mozilla Developer Network.
咱們提過使用 var
聲明時,它不在意你聲明多少次;你只會獲得1個。
function f(x) { var x; var x; if (true) { var x; } } 複製代碼
在上面的例子裏,全部 x
的聲明實際上都引用一個相同的 x
,而且這是徹底有效的代碼。 這常常會成爲 bug 的來源。 好的是, let
聲明就不會這麼寬鬆了。
let x = 10; let x = 20; // Uncaught SyntaxError: Identifier 'x' has already been declared 複製代碼
並非要求兩個均是塊級做用域的聲明纔會給出一個錯誤的警告。
function f(x) { let x = 100; // Uncaught SyntaxError: Identifier 'x' has already been declared } function g() { let x = 100; var x = 100; // Uncaught SyntaxError: Identifier 'x' has already been declared } 複製代碼
並非說塊級做用域變量不能用函數做用域變量來聲明。 而是塊級做用域變量須要在明顯不一樣的塊裏聲明。
function f(condition, x) { if (condition) { let x = 100; return x; } return x; } f(false, 0); // 0 f(true, 0); // 100 複製代碼
在一個嵌套做用域裏引入一個新名字的行爲稱作 屏蔽 。 它是一把雙刃劍,它可能會不當心地引入新問題,同時也可能會解決一些錯誤。 例如,假設咱們如今用 let
重寫以前的 sumArr
函數。
function sumArr(arrList) { let sum = 0; for (let i = 0; i < arrList.length; i++) { var arr = arrList[i]; for (let i = 0; i < arr.length; i++) { sum += arr[i]; } } return sum; } 複製代碼
此時將獲得正確的結果,由於內層循環的 i
能夠屏蔽掉外層循環的 i
。
一般來說應該避免使用屏蔽,由於咱們須要寫出清晰的代碼。 同時也有些場景適合利用它,你須要好好打算一下。
在咱們最初談及獲取用 var
聲明的變量時,咱們簡略地探究了一下在獲取到了變量以後它的行爲是怎樣的。 直觀地講,每次進入一個做用域時,它建立了一個變量的環境。 就算做用域內代碼已經執行完畢,這個環境與其捕獲的變量依然存在。
function theCityThatAlwaysSleeps() { let getCity; if (true) { let city = "Seattle"; getCity = function() { return city; } } return getCity(); } 複製代碼
由於咱們已經在 city
的環境裏獲取到了 city
,因此就算 if
語句執行結束後咱們仍然能夠訪問它。
回想一下前面 setTimeout
的例子,咱們最後須要使用當即執行的函數表達式來獲取每次 for
循環迭代裏的狀態。 實際上,咱們作的是爲獲取到的變量建立了一個新的變量環境。
當 let
聲明出如今循環體裏時擁有徹底不一樣的行爲。 不只是在循環裏引入了一個新的變量環境,而是針對每次迭代都會建立這樣一個新做用域。 這就是咱們在使用當即執行的函數表達式時作的事,因此在 setTimeout
例子裏咱們僅使用 let
聲明就能夠了。
for (let i = 0; i < 10 ; i++) { setTimeout(function() {console.log(i); }, 100 * i); } 複製代碼
會輸出與預料一致的結果:
0 1 2 3 4 5 6 7 8 9 複製代碼
const
聲明const
聲明是聲明變量的另外一種方式。
const numLivesForCat = 9; 複製代碼
它們與 let
聲明類似,可是就像它的名字所表達的,它們被賦值後不能再改變。 換句話說,它們擁有與 let
相同的做用域規則,可是不能對它們從新賦值。
這很好理解,它們引用的值是不可變的。
const numLivesForCat = 9; const kitty = { name: "Aurora", numLives: numLivesForCat, } // Error kitty = { name: "Danielle", numLives: numLivesForCat }; // all "okay" kitty.name = "Rory"; kitty.name = "Kitty"; kitty.name = "Cat"; kitty.numLives--; 複製代碼
除非你使用特殊的方法去避免,實際上 const
變量的內部狀態是可修改的。
let
vs. const
如今咱們有兩種做用域類似的聲明方式,咱們天然會問到底應該使用哪一個。 與大多數泛泛的問題同樣,答案是:依狀況而定。
使用最小特權原則,全部變量除了你計劃去修改的都應該使用const
。 基本原則就是若是一個變量不須要對它寫入,那麼其它使用這些代碼的人也不可以寫入它們,而且要思考爲何會須要對這些變量從新賦值。 使用 const
也可讓咱們更容易的推測數據的流動。
跟據你的本身判斷,若是合適的話,與團隊成員商議一下。
最簡單的解構莫過於數組的解構賦值了:
let input = [1, 2]; let [first, second] = input; console.log(first); // 1 console.log(second); // 2 複製代碼
這建立了2個命名變量 first
和 second
。 至關於使用了索引,但更爲方便:
first = input[0]; second = input[1]; 複製代碼
解構做用於已聲明的變量會更好:
[first, second] = [second, first];
複製代碼
做用於函數參數:
function f([first, second]) { console.log(first); console.log(second); } f(input); 複製代碼
你能夠在數組裏使用 ...
語法建立剩餘變量:
let [first, ...rest] = [1, 2, 3, 4]; console.log(first); // 1 console.log(rest); // [ 2, 3, 4 ] 複製代碼
固然,因爲是JavaScript, 你能夠忽略你不關心的尾隨元素:
let [first] = [1, 2, 3, 4]; console.log(first); // 1 複製代碼
或其它元素:
let [, second, , fourth] = [1, 2, 3, 4]; 複製代碼
你也能夠解構對象:
let o = { a: "foo", b: 12, c: "bar" }; let { a, b } = o; 複製代碼
這經過 o.a
and o.b
建立了 a
和 b
。 注意,若是你不須要 c
你能夠忽略它。
就像數組解構,你能夠用沒有聲明的賦值:
({ a, b } = { a: "baz", b: 101 }); 複製代碼
注意,咱們須要用括號將它括起來,由於 Javascript 一般會將以 {
起始的語句解析爲一個塊。
你能夠在對象裏使用 ...
語法建立剩餘變量:
let { a, ...passthrough } = o; let total = passthrough.b + passthrough.c.length; 複製代碼
展開操做符正與解構相反。 它容許你將一個數組展開爲另外一個數組,或將一個對象展開爲另外一個對象。 例如:
let first = [1, 2]; let second = [3, 4]; let bothPlus = [0, ...first, ...second, 5]; 複製代碼
這會令 bothPlus
的值爲 [0, 1, 2, 3, 4, 5]
。 展開操做建立了 first
和 second
的一份淺拷貝。 它們不會被展開操做所改變。
你還能夠展開對象:
let defaults = { food: "spicy", price: "?", ambiance: "noisy" }; let search = { ...defaults, food: "rich" }; 複製代碼
search
的值爲 { food: "rich", price: "?", ambiance: "noisy" }
。 對象的展開比數組的展開要複雜的多。 像數組展開同樣,它是從左至右進行處理,但結果仍爲對象。 這就意味着出如今展開對象後面的屬性會覆蓋前面的屬性。 所以,若是咱們修改上面的例子,在結尾處進行展開的話:
let defaults = { food: "spicy", price: "?", ambiance: "noisy" }; let search = { food: "rich", ...defaults }; 複製代碼
那麼,defaults
裏的 food
屬性會重寫 food: "rich"
,在這裏這並非咱們想要的結果。
對象展開還有其它一些意想不到的限制。 首先,它僅包含對象 自身的可枚舉屬性。 大致上是說當你展開一個對象實例時,你會丟失其方法:
class C { p = 12; m() { } } let c = new C(); let clone = { ...c }; clone.p; // ok clone.m(); // error! 複製代碼
new 關鍵字建立的對象其實是對新對象 this 的不斷賦值,並將 __proto__
指向類的 prototype 所指向的對象。
var SuperType = function (name) { var nose = 'nose' // 私有屬性 function say () {} // 私有方法 // 特權方法 this.getName = function () {} this.setName = function () {} this.mouse = 'mouse' // 對象公有屬性 this.listen = function () {} // 對象公有方法 // 構造器 this.setName(name) } SuperType.age = 10 // 類靜態公有屬性(對象不能訪問) SuperType.read = function () {} // 類靜態公有方法(對象沒法訪問) SuperType.prototype = { // 對象賦值(也能夠一一賦值) isMan: 'true', // 公有屬性 write: function () {} // 公有方法 } var instance = new SuperType() 複製代碼
在函數調用前增長 new
,至關於把 SuperType
當成一個構造函數(雖然它僅僅只是個函數),而後建立一個 {} 對象並把 SuperType
中的 this
指向那個對象,以即可以經過相似 this.mouse
的形式去設置一些東西,而後把這個對象返回。
具體來說,只要在函數調用前加上 new
操做符,你就能夠把任何函數當作一個類的構造函數來用。
在上例中,咱們能夠看到:在構造函數內定義的 私有變量或方法 ,以及類定義的 靜態公有屬性及方法 ,在 new 的實例對象中都將 沒法訪問 。
若是你調用 SuperType()
時沒有加 new
,其中的 this
會指向某個全局且無用的東西(好比,window
或者 undefined
),所以咱們的代碼會崩潰,或者作一些像設置 window.mouse
之類的傻事。
let instance1 = SuperType(); console.log(instance1.mouse); // Uncaught TypeError: Cannot read property 'mouse' of undefined console.log(window.mouse); // mouse 複製代碼
function Bottle(name) { this.name = name; } // + new let bottle = new Bottle('bottle'); // ✅ 有效: Bottle {name: "bottle"} console.log(bottle.name) // bottle // 不加 new let bottle1 = Bottle('bottle'); // 🔴 這種調用方法讓人很難理解 console.log(bottle1.name); // Uncaught TypeError: Cannot read property 'name' of undefined console.log(window.name); // bottle 複製代碼
class Bottle { constructor(name) { this.name = name; } sayHello() { console.log('Hello, ' + this.name); } } // + new let bottle = new Bottle('bottle'); bottle.sayHello(); // ✅ 依然有效,打印:Hello, bottle // 不加 new let bottle1 = Bottle('bottle'); // 🔴 當即失敗 // Uncaught TypeError: Class constructor Bottle cannot be invoked without 'new' 複製代碼
let fun = new Fun(); // ✅ 若是 Fun 是個函數:有效 // ✅ 若是 Fun 是個類:依然有效 let fun1 = Fun(); // 咱們忘記使用 `new` // 😳 若是 Fun 是個長得像構造函數的方法:使人困惑的行爲 // 🔴 若是 Fun 是個類:當即失敗 複製代碼
即
new Fun() | Fun | |
---|---|---|
class | ✅ this 是一個 Fun 實例 |
🔴 TypeError |
function | ✅ this 是一個 Fun 實例 |
😳 this 是 window 或 undefined |
function Bottle() { return 'Hello, AnGe'; } Bottle(); // ✅ 'Hello, AnGe' new Bottle(); // 😳 Bottle {} 複製代碼
對於箭頭函數,使用 new
會報錯🔴
const Bottle = () => {console.log('Hello, AnGe')}; new Bottle(); // Uncaught TypeError: Bottle is not a constructor 複製代碼
這個行爲是遵循箭頭函數的設計而刻意爲之的。箭頭函數的一個附帶做用是它沒有本身的 this
值 —— this
解析自離得最近的常規函數:
function AnGe() { this.name = 'AnGe' return () => {console.log('Hello, ' + this.name)}; } let anGe = new AnGe(); console.log(anGe()); // Hello, AnGe 複製代碼
因此**箭頭函數沒有本身的 this。**但這意味着它做爲構造函數是徹底無用的!
總結:箭頭函數
new
調用的函數返回另外一個對象以 覆蓋 new
的返回值先看一個例子:
function Vector(x, y) { this.x = x; this.y = y; } var v1 = new Vector(0, 0); var v2 = new Vector(0, 0); console.log(v1 === v2); // false v1.x = 1; console.log(v2); // Vector {x: 0, y: 0} 複製代碼
對於這個例子,一目瞭然,沒什麼可說的。
那麼再看下面一個例子,思考一下爲何 b === c
爲 true
喃😲:
let zeroVector = null; // 建立了一個懶變量 zeroVector = null; function Vector(x, y) { if (zeroVector !== null) { // 複用同一個實例 return zeroVector; } zeroVector = this; this.x = x; this.y = y; } var v1 = new Vector(0, 0); var v2 = new Vector(0, 0); console.log(v1 === v2); // true v1.x = 1; console.log(v2); // Vector {x: 1, y: 0} 複製代碼
這是由於,JavaScript 容許一個使用 new
調用的函數返回另外一個對象以 覆蓋 new
的返回值。這在咱們利用諸如「對象池模式」來對組件進行復用時多是有用的。
TypeScript Variable Declarations
想看更過系列文章,點擊前往 github 博客主頁
走在最後,歡迎關注:前端瓶子君,每日更新
、