JavaScript Weird Part (11) - 範圍、ES6 與 let

執行環境、變數環境與詞彙環境定義了所謂的範圍,特別是在範圍鏈當中可看到每個函數都有一個外部環境。範圍就是變數可以被取用的區域。如果有兩個相同變數,如果呼叫相同函數 2 次,它會各有一個自己的執行環境,這兩個看起來相同的變數,在記憶體中其實是兩個不同的變數。

使用 let 及 const 宣告區塊變數

在 ES6 當中,引進一種新的宣告變數方法 letconst。雖然它的使用方法與 var 相同,但並沒有取代後者。但是 let 讓 Javascript 引擎使用「區塊範圍」(block scoping)。

區塊={}。
1
2
3
if (a > b) {
let c = true;
}

它會在執行階段被創造,變數仍會被放入記憶體中,設值為 undefined,然而直到執行階段執行到那一行時,真的宣告變數後,你才能使用 let。

在這個例子如果試著在 let c = true 之前取用 c,會得出一個錯誤,儘管它還在記憶體中,但引擎不讓你取用。

另一個重點是,這是在區塊中被宣告,也就是被定義在大括號中,例如 if 條件句或 for 迴圈裡,當變數被宣告在區塊裡面,它就只能在裡面被取用。

let 及 const 的用法區分

const 是用來宣告一個常數,不能修改的唯讀變數,不能被變更的變數,例如 url 網址。 但是如果const 宣告的變數是一個陣列或物件,則裡面的內容就能夠被修改。為了避免這種狀況,可以利用以下組合式語法:

1
2
3
4
5
const obj = {
url: "http://xx.com"
};

obj.freeze(obj);

需注意 let 及 const 的特性

  1. 不具備向上提升 (Hoisting) 特性

var 有向上提昇的特性

1
2
3
console.log(a);
var a = 3;
console.log(a);

但是 constlet 不具備這種特性

1
2
3
console.log(a);
let a = 3;
console.log(a);

  1. 同個區塊上不能重複命名
1
2
var a = 3;
var a = 4;

1
2
let a = 3;
let a = 4;

  1. 無法繼承到全域變數

let 與 const 在 ES6 就是為了避免 var 會干擾全域變數的特性,因此不能使用在全域變數。

1
let a = 3;

1
var b = 3;

延伸閱讀
https://ithelp.ithome.com.tw/articles/10185142

  1. 作用域不同

var 作用域是 function scope

1
2
3
4
5
6
7
8
9
10
function varMing() {
var ming = "小明";
if (true) {
var ming = "杰哥";
// 這裡的 ming 依然是外層的小明,所以小明即將被取代
}
console.log(ming); // '杰哥'
}

varMing();

if 的大括號裡重新定義變數的值,它一樣在同一個作用域,因此會影響到外層的 ming 所以最終結果得出

let 作用域是 block scope,所以內層的變數不會影響到外面的變數

1
2
3
4
5
6
7
8
9
10
function varMing() {
var ming = "小明";
if (true) {
var ming = "杰哥";
// 這裡的 ming 依然是外層的小明,所以小明即將被取代
}
console.log(ming); // '杰哥'
}

varMing();

關於 let 的特性 可以再參考以下例子 :

1
2
3
4
5
6
for (var i = 0; i < 10; i++) {
console.log(i);
setTimeout(function() {
console.log("這執行第" + i + "次");
}, 10);
}

預期的結果是執行 9 次,但結果卻是 10 次

這是因為這裡的 setTimeout執行的是全域變數

改成使用 let 之後就會出現符合期待的結果,同時也清楚看到這裡的i沒有被宣告在全域範圍

在不能使用 ES6 的時候,以立即函式解決上述 var受到全域變數污染的問題

1
2
3
4
5
6
7
8
for (var i = 0; i < 10; i++) {
console.log(i);
(function(i) {
setTimeout(function() {
console.log("這執行第" + i + "次");
}, 10);
})(i);
}

展開與其餘參數

傳統合併陣列方法

1
2
3
4
let groupA = ["小明", "杰倫", "阿姨"];
let groupB = ["老媽", "老爸"];
let groupAll = groupA.concat(groupB);
console.log(groupAll);

使用展開語法合併陣列

... 的意思就是將值一個一個取出後再 return 回去

1
2
3
4
let groupA = ["小明", "杰倫", "阿姨"];
let groupB = ["老媽", "老爸"];
let groupAll = [...groupA, ...groupB];
console.log(groupAll);

淺複製觀念 (shallow copy)

1
2
3
4
let groupA = ["小明", "杰倫", "阿姨"];
let groupB = groupA;
groupB.push("阿明");
console.log(groupA);

A 與 B 應該是不一樣的東西,但兩個的值卻一樣。這是因為陣列跟物件都是 by reference,當它們的 reference 是一樣的時候,值變動的時候,兩者都會同時變動。

要避免這樣的狀況,一樣用展開方式。因為這樣對 B 來說,只是將值一個一個被取出再塞入一個新陣列,所以與 A 事實上是無關的

1
2
3
4
let groupA = ["小明", "杰倫", "阿姨"];
let groupB = [...groupA]; //新陣列
groupB.push("阿明");
console.log(groupA);

類陣列觀念

Nodelist 不是一個真正的陣列

1
2
let doms = document.querySelectorAll("li");
console.log(doms.concat());

所以它不具有許多陣列的方法,因此跳錯

我們可以再用展開方法,將陣列轉成真正的陣列

類陣列觀念說明

在函式沒有宣告參數的時候,要傳入的參數就會全部用 arguments 取代

1
2
3
4
5
6
7
8
9
10
var originCash = 1000;
function updateEasyCard() {
let arg = arguments;
let sum = arg.reduce(function(accumulator, currentValue) {
return accumulator + currentValue;
}, 0);
console.log("我有 " + sum + " 元");
}
updateEasyCard(0); // 我有 1000 元
updateEasyCard(10, 50, 100, 50, 5, 1, 1, 1, 500);

但是這裡的 arguments 不是一個真正的陣列

一樣用展開方法改善,可以正確執行加總方法

其餘參數

有時傳入給函式的數字量是不一定的,這時候就可以使用其餘參數方法。就是自動把多餘的參數以陣列的方式呈現。傳入的值沒有數量的限制

範例 1 -

1
2
3
4
function moreMoney(ming, ...money) {
console.log(ming, money);
}
moreMoney(100, 100, 100);

範例 2 -

1
2
3
4
function moreMoney(...money) {
console.log(money);
}
moreMoney("小明", 100, 100, 100);

解構賦值

解構賦值也是另一種 ES6 提供的好用新語法,概念如下圖:

陣列解構

  1. 請將 family 的值,一一賦予到變數上
  2. 請將後面三個名字賦予到另一個陣列上

傳統方式會宣告一個變數,再將陣列裡的值取出來。但以解構方式就像鏡射一樣,因此左方我們可以定義一個新變數。

1
2
3
let family = ["小明", "倫", "阿姨", "老媽", "老爸"];
let [ming, alien, auntie, mom, dad] = family;
console.log(ming, alien, auntie, mom, dad);

可是左右的數量不對等會怎樣? 後面兩個就不會賦值

1
2
3
let family = ["小明", "倫", "阿姨", "老媽", "老爸"];
let [ming, alien, auntie] = family;
console.log(ming, alien, auntie);

又如果中間有缺值,也跳過,不會解構賦值

1
2
3
let family = ["小明", "倫", "阿姨", "老媽", "老爸"];
let [ming, alien, , mom, dad] = family;
console.log(ming, alien, mom, dad);

交換 2 個變數

解構賦值在傳遞資料的時候,非常即時,所以不需要透過第三方變數取代,就可以直接交換 2 個變數

1
2
3
4
let Goku = "悟空";
let Ginyu = "基紐";
[Goku, Ginyu] = [Ginyu, Goku];
console.log(Goku, Ginyu);

拆解字元到單一變數上

解構賦值一樣可以用類似陣列的方式,把字串字元一一取出

1
2
3
let str = "基紐特攻隊";
let [q, a, z, w, s] = str;
console.log(q, a, z, w, s);

物件處理

取出物件內的一個值到單一變數上,右方的物件會先被取出一個值到前方的物件屬性上,前方物件的屬性可以重新定義名稱

1
2
3
4
5
6
7
let GinyuTeam = {
Ginyu: "基紐",
Jeice: "吉斯",
burter: "巴特"
};
let { Ginyu: Goku } = GinyuTeam;
console.log(Goku);

以下是另一個看似複雜的解構賦值例子,但其實我們只要比對他的鏡射位置就可以理解

1
2
3
4
5
let {
ming: Goku,
family: [, mom]
} = { ming: "小明", family: ["阿姨", "老媽", "老爸"] };
console.log(Goku, mom);

ming在左方被重新定義名稱, 而 family 則是要重新取出第 2 個值。所以結果

預設值

第一個會被賦值,第二個會用預設

1
2
let [ming = "小明", alien = "倫"] = ["阿明"];
console.log(ming, alien);

如果右方沒有傳值,就會直接使用預設值

1
let { family: ming = "小明" } = {};

縮寫

縮寫是 Vue 裡面經常用到的操作

物件縮寫

將以下兩個合併至一個物件上

  1. 傳統寫法
1
2
3
4
5
6
7
8
9
10
11
const Frieza = "弗利沙";
const GinyuTeam = {
Ginyu: "基紐",
Jeice: "吉斯",
burter: "巴特"
};

const newTeam = {
GinyuTeam: GinyuTeam,
Frieza: Frieza
};
  1. ES6 縮寫

當名稱跟新的屬性一致的時候,就不用重複寫

1
2
3
4
5
6
7
8
9
10
11
12
const Frieza = '弗利沙'
const GinyuTeam = {
Ginyu: '基紐',
Jeice: '吉斯',
burter: '巴特',

}

const newTeam = {
GinyuTeam
Frieza
}

物件函式縮寫

縮寫以下 showPosture Function

1
2
3
4
5
const newTeam = {
showPosture() {
console.log("我們是 基紐特戰隊");
}
};

箭頭函式縮寫

1
2
3
4
5
const newTeam = {
showPosture: () => {
console.log("我們是 基紐特戰隊");
}
};

搭配解構賦值的使用

將以下物件指向賦予到另一個物件上,並且避免 by reference ,第一種寫法會改變原本的結構,不是我們期待的結果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const GinyuTeam = {
Ginyu: {
name: "基紐"
},
Jeice: {
name: "吉斯"
},
burter: {
name: "巴特"
}
// ...
};

const newTeam = { GinyuTeam };
newTeam.ming = "小明";
console.log(newTeam, GinyuTeam);

裡面多包了一層

用解構寫法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const GinyuTeam = {
Ginyu: {
name: "基紐"
},
Jeice: {
name: "吉斯"
},
burter: {
name: "巴特"
}
};

const newTeam = { ...GinyuTeam };
newTeam.ming = "小明";
console.log(newTeam, GinyuTeam);

箭頭函式與傳統函式

改寫傳統函式

將以下改寫成 Arrow Function,並嘗試縮寫形式

1
2
3
4
5
6
7
8
9
// var callSomeone = function (someone) {
// return someone + '吃飯了'
// }
// console.log(callSomeone('小明'))

var callSomeone = someone => {
return someone + "吃飯了";
};
console.log(callSomeone("小明"));

如果函式的內容只有一行還可以更精簡

1
2
3
var callSomeone = someone => someone + "吃飯了";

console.log(callSomeone("小明"));

如果沒有傳入參數的時候

1
2
3
var callSomeone = () => someone + "吃飯了";

console.log(callSomeone());

沒有 arguments 參數

傳入一堆數值但是並沒有賦與參數變數名稱,就會用 arguments 取代

1
2
3
4
5
6
7
8
9
10
11
// const updateEasyCard = function () {
// let cash = 0;
// console.log(arguments); // arguments is not defined
// }
// updateEasyCard(10, 50, 100, 50, 5, 1, 1, 1, 500);

const updateEasyCard = (...arg) => {
let cash = 0;
console.log(arg); // arguments is not defined
};
updateEasyCard(10, 50, 100, 50, 5, 1, 1, 1, 500);

This 綁定的差異

this 綁定的差異就是傳統函式與箭頭函式最大的差異。現在要了解宣告的物件 auntie 裡面的 this 是指向誰?

  1. 傳統函式

第一個 this 指向的就是物件本身,可是第二個 this 為什麼指向的是全域?這是因為傳統函式的 this 指向的是函式的呼叫方式。呼叫 callName 的位置在 auntie底下,所以第一個this指向auntie。所以 setTimeout 指向的是 window

1
2
3
4
5
6
7
8
9
10
11
12
var name = "全域阿婆";
var auntie = {
name: "漂亮阿姨",
callName: function() {
console.log("1", this.name, this); // 1 漂亮阿姨
setTimeout(function() {
console.log("2", this.name); // 2 漂亮阿姨
console.log("3", this); // 3 auntie 這個物件
}, 10);
}
};
auntie.callName();

  1. 箭頭函式

在物件內使用箭頭函式,this 所指向的「可能」都是全域,「可能」的意思是說它並不一定是綁定在 window上,可能會綁定在其他物件上,它可能是綁定在它定義時所在的物件

1
2
3
4
5
6
7
8
9
10
11
12
13
var name = "全域阿婆";
var auntie = {
name: "漂亮阿姨",
callName2: () => {
console.log("1 arrow", this.name, this); // 1 漂亮阿姨
setTimeout(() => {
console.log("2 arrow", this.name); // 2 漂亮阿姨
console.log("3 arrow", this); // 3 auntie 這個物件
}, 10);
}
};

auntie.callName2();

在 Vue 裡面使用 arrow function 就可能會出錯,在 Vue 的 methods 裡面,使用傳統函式,並且使用縮寫方式。

1
2
3
4
5
6
7
8
9
10
11
12
var name = "全域阿婆";
var auntie = {
name: "漂亮阿姨",
callName() {
console.log("1", this.name, this); // 1 漂亮阿姨
setTimeout(function() {
console.log("2", this.name); // 2 漂亮阿姨
console.log("3", this); // 3 auntie 這個物件
}, 10);
}
};
auntie.callName();

善用箭頭函式

請將內層的 console.log 能夠指向 auntie 這個物件

1
2
3
4
5
6
7
8
9
10
var auntie = {
name: "漂亮阿姨",
callName() {
var vm = this;
setTimeout(() => {
console.log(vm.vm.name);
}, 10);
}
};
auntie.callName();

字串模版 Templete String

字串模版讓傳統組字串方式更簡化。現在一陣列裡有三個物件,現在要將值取出再放入字串

1
<div id="app"></div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
const people = [
{
name: '小明',
friends: 2
},
{
name: '阿姨',
friends: 999
},
{
name: '杰倫',
friends: 0
}
]

// 傳統寫法
let originString = '我叫做 ' + people[0].name;
let originUl = '<ul>\
<li>我叫做 ' + people[0].name + '</li>\
<li>我叫做 ' + people[1].name + '</li>\
<li>我叫做 ' + people[2].name + '</li>\
</ul>';
console.log(originString, originUl);

// ES6
let newString = `我叫做 ${people[0].name}`;
let newUl= `<ul>
<li>我叫做 ${people[0].name}/li>
<li>我叫做 ${people[1].name}/li>
<li>我叫做 ${people[2].name}/li>
</ul>`
console.log(newString, newUl);

反引號與$字號的寫法內,也可插入 JavaScript 原始碼。下面的 map() 會以迴圈方式處理 people,處理完將內容返回字串模版,返回的內容會成為一個陣列。

1
2
3
4
5
6
7
8
 newUl = `<ul>
${people.map(person=>` <li>我叫做 ${person.name}/li>`).join('')}


</ul>`;

console.log(newUl);
$('#app').html(newUl);// 用 jQuery 的方式把資料放回畫面

Powered by Hexo and Hexo-theme-hiker

Copyright © 2013 - 2020 CYC'S BLOG All Rights Reserved.

UV : | PV :