顯示廣告
隱藏 ✕
看板 KnucklesNote
作者 Knuckles (站長 那克斯)
標題 [JS] 克服JS的奇怪部分 ch4 物件與函數(下)
時間 2016-11-25 Fri. 03:45:26


Udemy課程: JavaScript全攻略:克服JS的奇怪部分
https://www.udemy.com/javascriptjs/learn/v4/overview
上完第四章後半的心得筆記

章節4 物件與函數(下)


38. 觀念小叮嚀:陣列——任何東西的集合

JS的陣列可以同時存各種資料型態,包含物件和函數

var arr = [ // 用陣列實體語法,建立一個有各種資料的陣列
	
1,  // 數字
	
false, // 布林值
	
{ name: 'Knuckles' }, // 物件
	
function(name){ // 函數
	
	
console.log('Hello '+name);
	
},
	
"hello" //字串
];

//可執行陣列中的函數,可取得陣列中的物件屬性
arr[3](arr[2].name); //顯示 Hello Knuckles


39. arguments and spread

arguments 是在函數的執行環境時自動建立的陣列
會將函數的輸入值存在這個陣列中

spread 是下一版JS的功能,可使用在函數有不確定有多少個輸入值的時候
目前瀏覽器還未支援

function greet(firstname, lastname, language){
	
console.log(firstname + ',' + lastname + ',' + language);
	
console.log(arguments);
}

greet(); // 顯示 undefined, undefined, undefined 與 []
greet('John'); // 顯示 John, undefined, undefined 與 ["John"]
greet('John', 'Doe'); // 顯示 John, Doe, undefined 與 ["John","Doe"]
greet('John', 'Doe', 'es'); // 顯示 John, Doe, es 與 ["John","Doe","es"]


40. 框架小叮嚀:重載函數

JS不像其他程式語言有重載函數的功能
只能在同一個函式裡依輸入值寫判斷式

例如寫一個可以用兩種語言打招呼的函數
function greet(name, language){
	
language = language || 'en'; // 預設值

	
if(language === 'en'){
	
	
console.log('Hello ' + name);
	
}else if(language === 'es'){
	
	
console.log('Hola ' + name);
	
}
}

greet('Knuckles'); // 只給一個輸入值,預設使用'en',顯示 Hello Knuckles
greet('Knuckles', 'es'); // 有輸入語言為'es'時,顯示 Hola Knuckles

// 不想讓人可以輸入第二個參數的話,可以建立不同的函數來呼叫 greet()
function greetEnglish(name){
	
greet(name, 'en');
}
function greetSpanish(name){
	
greet(name, 'es');
}

greetEnglish('Knuckles'); // 顯示 Hello Knuckles
greetSpanish('Knuckles'); // 顯示 Hola Knuckles

42. 危險小叮嚀:自動插入分號

JS在某些情況下看到沒有分號就換行時,會自動加分號上去
例如 return 後直接換行時,會被改為 return;
function getPerson(){
	
return 
	
{
	
	
firstname: 'Knuckles'
	
}
}

console.log(getPerson()); // 顯示 undefined
將要回傳的物件寫在return下一行,卻沒有照預期的回傳出來
因為 return 被改為 return; 了

改成這樣就沒問題了
function getPerson(){
	
return {
	
	
firstname: 'Knuckles'
	
}
}

console.log(getPerson()); // 顯示 Object {firstname: "Knuckles"}
return 後有看到 { 就不會自動加 ; 了

所以將物件實體語法的 { 接在上一行的句尾會是比較好的習慣
否則可能會因為被自動加 ; 而造成難以Debug的情況


44. 立即執行的函數表示式(IIFE, Immediately Invoked Function Expressions)

先看一個函數表示式的例子
var greet = function(name){
	
return 'Hello ' + name;
};
var greeting = greet('Knuckles');

console.log(greeting); // 顯示 Hello Knuckles
我們先創造一個匿名函數,存在 greet
然後用 greet 接 ('Knuckles') 來執行這個函數並傳入參數
將函數 return 的值存在 greeting

如果這個函數只有要執行一次,之後不會用到的話
可以簡寫成這樣
var greeting = function(name){
	
return 'Hello ' + name;
}('Knuckles');

console.log(greeting); // 顯示 Hello Knuckles
也就是直接在匿名函數 function(name){...}
後面接上 ('Knuckles'),來執行這個匿名函數並傳入參數
這樣就不用多一個變數 greet 來存這個只用一次的函數了

這種寫法就叫做立即執行的函數表示式(IIFE)

若是這個函數沒有輸出值,只有要執行裡面的程式時
就不用加上 var greeting = 來接收輸出值,變成像這樣
function(name){
	
console.log('Hello ' + name); // 沒有 return 值
}('Knuckles');
結果出現錯誤: Unexpected token (

因為一行的開頭第一個字若是 function 的話
JS會認為這是一個函數陳述句,function後應該要接函數名稱才對

解決方法就是讓一行的第一個字不要是 function 就可以了
普遍的寫法是把整個立即執行函數用()包起來
(function(name){
	
console.log('Hello ' + name); 
}('Knuckles')); // 顯示 Hello Kncukles
這樣就不會顯示錯誤了

或是把('Knuckles')移出整個()也可以
(function(name){
	
console.log('Hello ' + name); 
})('Knuckles'); // 顯示 Hello Kncukles
兩種結果是一模一樣的,選一種喜歡的來用就可以了


45. 框架小叮嚀:IIFEs 與安全程式碼

為了避免在全域環境加上太多變數,造成變數名稱干擾的問題
大部份的框架都會將整個js檔裡的程式,用一個立即執行函數包起來

然後為了存取全域環境的變數,再將全域變數寫在輸入參數傳進來
(function(global){ // global 會接收到全域的 window 物件
	

	
global.greeting = 'hi'; //可以用 global 存取全域變數
 
})(window); // 將全域變數 window 傳入
很多框架的js檔案開頭跟結尾就是長這樣


46. 瞭解閉包(Closure)

閉包是JS的重要觀念,但很難懂所以很多人不喜歡

參考這個例子
function greet(whattosay){
	
return function(name){
	
	
console.log(whattosay + ' ' + name);
	
}
}

var sayHi = greet('Hi');
sayHi('Knuckles'); // 顯示 Hi Knuckles
建立了一個函數 greet,輸入值為 whattosay
輸出值為一個匿名函數,這個匿名函數輸入值為 name
然後會顯示 whattosay 與 name 的值

接著執行 greet('Hi') 然後使用變數 sayHi 儲存輸出的匿名函數

此時用 sayHi('Knuckles'); 來執行這個儲存的函數
執行結果為顯示 Hi Knuckles

奇怪的事情是,whattosay的值被設為Hi,是在執行 greet('Hi') 時
但 greet() 執行完後,whattosay的值應該就不存在了呀
為何在執行 sayHi() 時,還是能取得whattosay的值為'Hi'呢

這就是閉包的特性讓人困惑的地方

當 greet() 執行環境結束時,雖然 whattosay 的生命週期已結束
但因為還有可能被存取到,所以JS會保留住他的值(閉包)
當執行 sayHi() 時,需要到外部環境取用 whattosay
雖然外部環境 greet() 已結束了,還是可以取得保留的'Hi'值


再來看一個經典的閉包例子
function buildFunctions(){
	
var arr = [];
	
for(var i=0; i<3; i++){
	
	
arr.push(function(){
	
	
	
console.log(i);
	
	
});
	
}
	
return arr;
}

var fs = buildFunctions();

fs[0]();
fs[1]();
fs[2]();
建立一個函數 buildFunctions(),
使用迴圈在 i 為 0, 1, 2 時,
分別建立一個匿名函式 function(){ console.log(i); }
存到陣列 arr[] 的 0, 1, 2 的位置
然後輸出這個陣列 arr

執行 buildFunctions() 並將輸出的陣列存在 fs
然後分別執行陣列裡的三個匿名函數
此時會顯示什麼呢?

看起來好像是顯示 0 1 2,但其實是 3 3 3,為什麼呢?

因為迴圈在將三個匿名函數放進陣列時
這三個匿名函數只是創造了,但沒有執行
所以 console.log(i); 並沒有將 i 的值傳進去

直到最後使用 fs[0](); fs[1](); fs[2]();
才是執行了這三個匿名函數,需要到外部環境取得 i 的值來顯示
雖然外部環境,也是就 buildFunctions() 已執行結束了
但因為閉包的關係,i的值還保留在記憶體中讓三個匿名函數存取

此時 i 的值因為迴圈的關係已經從0跑到3了
所以三個匿名函數的執行結果都是顯示 3


如果我們的程式,就是想要輸出的結果為 0 1 2 的話呢
可以將程式改成這樣
function buildFunctions2(){
	
var arr = [];
	
for(var i=0; i<3; i++){
	
	
//arr.push(function(){
	
	
//
	
console.log(i);

	
	
//});
	
	
arr.push(
	
	
	
(function(j){
	
	
	
	
return function(){
	
	
	
	
	
console.log(j);
	
	
	
	
}
	
	
	
})(i)
	
	
)
	
}
	
return arr;
}

var fs2 = buildFunctions2();

fs2[0]();
fs2[1]();
fs2[2]();
儲存匿名函數前先用個立即執行函數包起來
在立即執行函數輸入i的值,存在參數j,輸出原本的匿名函數

因為立即執行函數是有執行的,會分別傳入i為0 1 2的值
並建立了三個執行環境,分別儲存了j為0 1 2的值
當最後匿名函數執行時,他們的外部環境就是那三個不同的立即執行函數
使用console.log(j)顯示的值就會是0 1 2三個不同的值了


48. 框架小叮嚀:Function Factories

利用閉包的特性,可以建立一些看似不可能的模式
例如改寫之前用不同語言打招呼的程式
function makeGreeting(language){

	
return function(name){

	
	
if(language === 'en'){
	
	
	
console.log('Hello ' + name);
	
	
}else if(language === 'es'){
	
	
	
console.log('Hola ' + name);
	
	
}
	
}
}

var greetEnglish = makeGreeting('en');
var greetSpanish = makeGreeting('es');

greetEnglish('Knuckles'); // 顯示 Hello Knuckles
greetSpanish('Knuckles'); // 顯示 Hola Knuckles
建立一個函數 makeGreeting(),依輸入值 language,
輸出一個會顯示不同語言的匿名函數

接著我們就可以利用 makeGreeting() 快速建立不同語言的 greet 函數了

先用 var greetEnglish = makeGreeting('en');
產生一個用英文打招呼的函數 greetEnglish()

再用 var greetSpanish = makeGreeting('es');
產生一個用西班牙文打招呼的函數 greetSpanish()

因為執行了兩次 makeGreeting(),會分別建立不同的執行環境
所以儲存 language 值的記憶體位置也不同,不會蓋掉另一個
而 language 的值會因為閉包的關係被保留下來

當執行 greetEnglish() 時,取得的 language 就是 'en'
當執行 greetSpanish() 時,取得的 language 就是 'es'
可以正確的顯示想要的打招呼語言


49. 閉包(Closures)與回呼(Callbacks)

常用的 setTimeout() 也有用到閉包的特性
function sayHiLater(){

	
var greeting = 'Hi!';

	
setTimeout(function(){ // 設定三秒後執行
	
	
console.log(greeting);
	
}, 3000);
}

sayHiLater(); // 三秒後顯示 Hi!
建立一個函數 sayHiLater(),在裡面建立一個變數 greeting,
使用 setTimeout 在3秒後執行一個匿名函數來顯示 greeting 的值

執行 sayHiLater() 後,過了三秒匿名函數才執行,
此時雖然 sayHiLater() 的執行環境已結束了,
但因為閉包的關係 greeting 的值還是有保留著讓匿名函數可以讀取


Callback Function: 把函數b傳給函數a,讓函數a可以做完某些事後再執行函數b

function tellMeWhenDone(callback){

	
console.log('Do something ...');

	
callback();
}

tellMeWhenDone(function(){
	
console.log('I am done!');
});
建立一個函數 tellMeWhenDone(),輸入值為函數 callback
當做完某些事情後,再執行這個函數 callback

接著在執行 tellMeWhenDone() 時,
將希望 tellMeWhenDone() 執行完某些事後要做的事情,
寫成匿名函數傳進去

執行結果為顯示:
Do something...
I am done!


50. call(), apply(), and bind()

當執行一個函數建立一個新的執行環境時,
會自動產生一個 this 變數,指向這個函數所在的物件

但有時候我們會想指定 this 是指向別的物件,該怎麼做呢?

我們知道函數也是個特殊的物件,函數物件會有三個內建的成員函數
分別為 bind(), call(), apply()
使用這三個成員函數就可以指定函數的 this 要指向哪個物件了

以下舉例說明三種函數的用法
// 建立一個物件 person,有個成員函數 getFullName 會輸出他的兩個屬性值
var person = { 
	
firstname: 'Knuckles',
	
lastname: 'Huang',
	
getFullName: function(){
	
	
var fullname = this.firstname + ' ' + this.lastname;
	
	
return fullname;
	
}
}

// 建立一個函數 logName,會執行 this.getFullName() 取得 fullname
// 將 fullname 與隨便設的兩個輸入值 s1, s2 顯示出來
var logName = function(s1, s2){
	
console.log(this.getFullName() + ' ' + s1 + ' ' + s2);
}

logName('a','b'); // 顯示錯誤,因為 logName() 裡的 this 是指向全域物件 window
                  // 而 window 裡沒有 getFullName 這個成員函數

// 使用 bind() 複製函數 logName,並指定新函數的 this 是指向物件 person
// 將新函數存在 logPersonName
var logPersonName = logName.bind(person);

logPersonName('a', 'b'); // 顯示 Knuckles Huang a b

//也可以直接在建立函數時立即指定 this 要指向 person
var logPersonName2 = function(s1, s2){
	
console.log(this.getFullName() + ' ' + s1 + ' ' + s2);
}.bind(person); // 在建立匿名函數時立即指定 this 指向 person

logPersonName2('a', 'b'); // 顯示 Knuckles Huang a b

// 使用 call() 執行函數 logName,並指定 this 指向 person
// 第一個輸入值為要指向的物件,第二個之後的輸入值為函數原本的輸入值
logName.call(person, 'a', 'b'); // 顯示 Knuckles Huang a b

// 使用 bind() 只會複製函數不會執行,使用 call() 會直接執行

// 使用 apply() 執行函數 logName,並指定 this 指向 person
// 第一個輸入值為要指向的物件,第二個輸入值為函數輸入值陣列
logName.apply(person, ['a', 'b']); // 顯示 Knuckles Huang a b

// apply() 與 call() 的功用相同,只是傳函數輸入值要改為陣列

// call() 和 apply() 也可以使用在立即執行函數
(function(s1, s2){
	
console.log(this.getFullName() + ' ' + s1 + ' ' + s2);
}).apply(person, ['a', 'b']); // 顯示 Knuckles Huang a b

什麼時候會需要用到這些功能呢?

例如函數借用(function borrowing)
// 建立一個物件 person,有個成員函數 getFullName 會輸出他的兩個屬性值
var person = { 
	
firstname: 'Knuckles',
	
lastname: 'Huang',
	
getFullName: function(){
	
	
var fullname = this.firstname + ' ' + this.lastname;
	
	
return fullname;
	
}
}

// 建立另一個物件 person2,只有兩個屬性沒成員函數
var person2 = { 
	
firstname: 'Jane',
	
lastname: 'Doe'
}

// person2 不用再建立一個成員函數 getFullName 了
// 可以利用 apply() 借 person 的 getFullName 給 person2 用
console.log(person.getFullName.apply(person2)); // 顯示 Jane Doe

function currying: 複製一個函數並設定預設輸入值
function multiply(a, b){ //輸出兩數相乘的值
	
return a*b;
}

var multiplyByTwo = multiply.bind(this, 2); // this 在這邊沒有作用
console.log(multipleByTwo(4)); // 顯示 8
利用 bind() 複製函數 multiply 並設定原本第一個輸入值固定為2
只要輸入原本的的第二個參數就好
這樣就可以快速產生一個乘以2的函數,不用再寫一次相乘的程式

function currying 在有許多數學運算的時候很好用


51. 函數程式設計(Functional Programing)

JS因為有一級函數,可以用來實作函數程式設計
可以做一些在其他沒有一級函數的程式語言不能做的事
可以用一些全新的方法來思考及設計程式

從以下例子來看看如何用函數程式設計來簡化程式
var arr1 = [1, 2, 3]; //建立一個簡單的數字陣列 arr1
console.log(arr1); // 顯示 [1, 2, 3]

// 我們想要將 arr1 每個值乘以2,然後存成新的陣列 arr2
var arr2 = [];
for(var i=0; i < arr1.length; i++){

	
arr2.push(arr1[i] * 2);

}
console.log(arr2); // 顯示 [2, 4, 6]

因為JS有一級函數可以用,所以可以改寫一下
將使用迴圈跑一遍陣列的事情寫成函數
將每個值乘以2這個動作切出來放到函數的輸入值
function mapForEach(arr, fn){

	
var newArr = [];
	
for (var i=0; i < arr.length; i++){
	
	
newArr.push(
	
	
	
fn(arr[i]) //原本乘以2的改用輸入的函數
	
	
)
	
};

	
return newArr;
}

var arr1 = [1, 2, 3]; //建立一個數字陣列 arr1
console.log(arr1); // 顯示 [1, 2, 3]

var arr2 = mapForEach(arr1, function(item){
	
return item * 2; // 乘以2的動作在這邊輸入
});

// 這樣程式變得清楚多了,我們輸入一個陣列,然後給他一個乘以2的動作
// 輸出就是一個每個值都乘以2的陣列
console.log(arr2); // 顯示 [2, 4, 6]

// 如果我們再來想求出陣列中哪幾個值大於2,只要這樣
var arr3 = mapForEach(arr1, function(item){
	
return item > 2; // 對每個值做 > 2 的運算
});
console.log(arr3); // 顯示 [false, false, true]

// 重覆利用函數 mapForEach 就可以對陣列做各種運算
// 只要用匿名函式傳入我要他做的運算即可
// 這就是函數程式設計的經典例子

// 但傳入的匿名函式輸入值只有固定一個,如果想加新參數,
// 像是下面這個函數這樣,變成兩個輸入值
var checkPastLimit = function(limiter, item){
	
return item > limiter;
}

// 必需想辦法將兩個參數變成一個才能傳入 mapForEach
// 可以用 bind() 做 function currying
var arr4 = mapForEach(arr1, checkPastLimit.bind(this, 2));
console.log(arr4); // 顯示 [false, false, true]

// 如果不想在輸入值用 bind(),只想傳一個 limiter 的值就好,
// 可以利用閉包的特性,將函數 checkPastLimit 改為
var checkPastLimitSimplified = function(limiter){
	
return function(limiter, item){
	
	
return item > limiter;
	
}.bind(this, limiter);
};

// 這樣就可以讓程式變得更簡潔了
var arr5 = mapForEach(arr1, checkPastLimitSimplified(2));
console.log(arr5); // 顯示 [false, false, true]


參考並使用 Underscore.js 函式庫

Underscore.js 是一個很有名的 JS 資源庫
提供很多處理陣列和物件的函數
[圖]


跟underscore.js相似的資源庫還有 https://lodash.com/

這兩個資源庫不但很有用,他的網站也展示了他如何做到那些功能
所以閱讀他的原始碼可以學到很多東西

在網站下載 underscore.js 在自己的網頁載入後
來試試使用他的 map() 來改寫之前的程式
var arr1 = [1, 2, 3];

// Underscore 使用底線 _ 做為他的全域變數,使用 _ 來呼叫他的函數
var arr6 = _.map(arr1, function(item){
	
return item * 2;
});
console.log(arr6); // 顯示 [2, 4, 6]

//使用 filter() 取得陣列中的雙數值
var arr7 = _.filter([2,3,4,5,6,7], function(item){
	
return item % 2 === 0; 
});
console.log(arr7); // 顯示 [2, 4, 6]





--
※ 作者: Knuckles 時間: 2016-11-25 03:45:26
※ 編輯: Knuckles 時間: 2016-11-26 08:53:39
※ 看板: KnucklesNote 文章推薦值: 0 目前人氣: 0 累積人氣: 1187 
分享網址: 複製 已複製
r)回覆 e)編輯 d)刪除 M)收藏 ^x)轉錄 同主題: =)首篇 [)上篇 ])下篇