var、let、const、解構、展開、new、this、class、函數

引言

JS系列暫定 27 篇,從基礎,到原型,到異步,到設計模式,到架構模式等,此爲第一篇:是對 var、let、const、解構、展開、函數 的總結。html

let在不少方面與 var 是類似的,可是 let 能夠幫助你們避免在 JavaScript 裏常見一些問題。const 是對 let 的一個加強,它能阻止對一個變量再次賦值。前端

1、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 變量。 即便當 gf 已經執行完後才被調用,它仍然能夠訪問及修改 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 循環體裏的代碼。

2、let 聲明

如今你已經知道了 var 存在一些問題,這剛好說明了爲何用 let 語句來聲明變量。 除了名字不一樣外, letvar 的寫法一致。

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個變量 aba 的做用域是 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
複製代碼

3、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 變量的內部狀態是可修改的。

4、let vs. const

如今咱們有兩種做用域類似的聲明方式,咱們天然會問到底應該使用哪一個。 與大多數泛泛的問題同樣,答案是:依狀況而定。

使用最小特權原則,全部變量除了你計劃去修改的都應該使用const。 基本原則就是若是一個變量不須要對它寫入,那麼其它使用這些代碼的人也不可以寫入它們,而且要思考爲何會須要對這些變量從新賦值。 使用 const也可讓咱們更容易的推測數據的流動。

跟據你的本身判斷,若是合適的話,與團隊成員商議一下。

5、解構

解構數組

最簡單的解構莫過於數組的解構賦值了:

let input = [1, 2];
let [first, second] = input;
console.log(first); // 1
console.log(second); // 2
複製代碼

這建立了2個命名變量 firstsecond。 至關於使用了索引,但更爲方便:

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 建立了 ab 。 注意,若是你不須要 c 你能夠忽略它。

就像數組解構,你能夠用沒有聲明的賦值:

({ a, b } = { a: "baz", b: 101 });
複製代碼

注意,咱們須要用括號將它括起來,由於 Javascript 一般會將以 { 起始的語句解析爲一個塊。

你能夠在對象裏使用 ... 語法建立剩餘變量:

let { a, ...passthrough } = o;
let total = passthrough.b + passthrough.c.length;
複製代碼

6、展開

展開操做符正與解構相反。 它容許你將一個數組展開爲另外一個數組,或將一個對象展開爲另外一個對象。 例如:

let first = [1, 2];
let second = [3, 4];
let bothPlus = [0, ...first, ...second, 5];
複製代碼

這會令 bothPlus 的值爲 [0, 1, 2, 3, 4, 5] 。 展開操做建立了 firstsecond 的一份淺拷貝。 它們不會被展開操做所改變。

你還能夠展開對象:

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!
複製代碼

7、new、this、class、函數

this 與 new

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

在函數調用前增長 new,至關於把 SuperType 當成一個構造函數(雖然它僅僅只是個函數),而後建立一個 {} 對象並把 SuperType 中的 this 指向那個對象,以即可以經過相似 this.mouse 的形式去設置一些東西,而後把這個對象返回。

具體來說,只要在函數調用前加上 new 操做符,你就能夠把任何函數當作一個類的構造函數來用。

加 new

在上例中,咱們能夠看到:在構造函數內定義的 私有變量或方法 ,以及類定義的 靜態公有屬性及方法 ,在 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

使用 new 的怪異之處

return 無效
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。**但這意味着它做爲構造函數是徹底無用的!

總結:箭頭函數

  • this 指向定義時的環境。
  • 不可 new 實例化
  • this 不可變。
  • 沒有 arguments 對象
容許一個使用 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 === ctrue 喃😲:

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 博客主頁

走在最後,歡迎關注:前端瓶子君,每日更新

前端瓶子君
相關文章
相關標籤/搜索