[課程筆記]克服JS的奇怪部分-CH3 物件與函數(下)
本篇筆記目錄:
框架小叮嚀:IIFEs 與安全程式碼
U21 瞭解閉包(一)
U22 瞭解閉包(二)
框架小叮嚀:Function Factories
U23 閉包與回呼
U24 call()、apply() 與 bind()
U25 函數程式設計(一)
U26 函數程式設計(二)
U20 立即呼叫的函數表示式(IIFEs)
IIFE 立即呼叫的函數表示式
在創造函數時呼叫函數
函數表示式 with IIFE:
var greeting=function(name){
console.log('Hello'+name);
}("Mavis");
另外,這些是有效的表示式
3;
'hi a mavis'
不過如果這樣寫我們會得到錯誤
function(name) {
console.log('Hi' + name)
}
//Uncaught SyntaxError
因為當語法解析器解析看到 function 這個字在一行的最前面,會認為應該會有一個名稱,結果我們並沒有給予函數名稱
我們要如何讓語法解析器知道我們不想要它成為陳述句,而是表示式?我們要確保 function 不是這行程式碼的第一個字:
(function(name) {
console.log('Hi' + name)
});
與其他表示式不同,函數我們可以用()呼叫他,所以我們可以讓上面的例子變成IIFE
(function(name) {
console.log('Hi' + name)
}("mavis"));
JS 知道在括號的東西一定是表示式,它會假設你寫的函數是函數表示式
另外,把括號放在外面,寫成這樣也是可以的
(function(name) {
console.log('Hi' + name)
})("mavis");
框架小叮嚀:IIFEs 與安全程式碼
我們先創建兩個檔案
//greet.js
var greeting='Hola';
//all.js
(function(name) {
var greeting='Hello';
console.log(greeting + name)
})("mavis");
console.log(greeting)
//hello mavis
//Hola
為什麼不會有衝突呢?
'Hello'
和'Hola'
在不同的執行環境中,所以我們可以將程式碼包在IIFE裡,保證他不會和其他東西衝突
在很多資源庫或函數,如果你打開他們的原始碼,第一個看到的東西就是括號和函數,當我們創造一些可重複利用的東西時 可以學著這樣做來確保程式不會和其他程式相衝突,我們不會不小心把東西放進全域物件。
但如果我們想要取用全域物件呢?我們只要傳入參數就好
//瀏覽器中window是全域物件
(function(global,name) {
global.greeting='Hello';
console.log(greeting + name)
})(window,"mavis");
//Hola mavis
U21 瞭解閉包(一)
Closuere 閉包
包住所有可以取用的變數的現象
function greet(whattosay){
return function(name){
console.log(whattosay+''+name);
}
}
var sayHi=greet('Hi');
sayHi('Tony')
為什麼 sayHi()
仍然知道 whattosay
是什麼?因為它是閉包
當greet()
創造環境時,whattosay
被傳入到它的變數環境裡,回傳一個函數後,greet 執行環境離開執行堆,當執行環境沒了之後記憶體空間會怎麼樣?一般情況JS會清除它(垃圾回收 garbage collection),但當執行環境結束時,記憶體空間還是存在
JS 找不到 whattosay 這個變數後,到外部環境去找,JavaScript 為了避免我們找不到變數,所以會形成一個框框,將這一整個包住關起來,確保我們可以找的到東西,這個行為就是所謂的閉包。

閉包只是一個功能,確保執行函數時它會正常運作,可以取用外部函數
U22 瞭解閉包(二)
如果我們到網路上搜尋閉包, 我們一定會看到這個例子
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]();
我們預期會得到:0,1,2
但實際上我們會得到:3,3,3
為什麼呢?我們可以觀察執行過程:
- 全域執行環境建立,包含 buildFunctions(),fs
- buildFunctions()執行環境建立,包含 i 和 arr
- for 迴圈執行,直到 i=3 時停止,所以回傳時 i=3
- buildFunctions()離開執行堆
- fs0執行環境建立,找不到 i 變數,到外部環境找,輸出 3
- fs[1]…fs[2]…

補充:在函數外面,但可取用到的,叫做自由變數(free variables)
如果我們不希望輸出都是3呢,而是輸出0,1,2呢?有兩個方法:
1.改使用let變數
function buildFunctions2(){
var arr=[];
for(var i=0;i<3;i++){
arr.push(
function(){
console.log(j)
}
)
}
return arr;
}
var fs2=buildFunctions2();
fs2[0]();
fs2[1]();
fs2[2]();
let的變數的範圍在buildFunctions2()
的大括號裡面,每次for 迴圈執行時,j 會是記憶體中的一個新的變數,每次都會在不同的記憶體位置
2.在ES5中可以使用IIFE
function buildFunctions2(){
var arr=[];
for(var i=0;i<3;i++){
arr.push(
(function(j){
console.log(j)
})(i))
)
}
return arr;
}
var fs2=buildFunctions2();
fs2[0]();
fs2[1]();
fs2[2]();
框架小叮嚀:Function Factories
我們看下面這個範例:
function makeGreeting(language){
return function(firstnmae,lastname){
if(language='en'){
console.log('Hello'+firstname+' '+lastname)
};
if(language='es'){
console.log('Hola'+firstname+' '+lastname)
};
}
}
var greetEnglish = makeGreeting('en');
var greetSpanish = makeGreeting('es');
greetingEnglish('John','Doe');
greetingSpanish('John','Doe');
雖然是一樣的函數(makeGreeting),但每次執行都會創造新的執行環境,他們擁有自己的變數環境,所以閉包會指向不同的記憶體位置

U23 閉包與回呼
使用 setTimeout 和 jQuery 就是使用閉包
1.setTimeout:
function sayHilater(){
var greeting ='Hi';
setTimeout(function(){
console.log(greeting);
},3000)
}
sayHiLater();
執行過程:
- 執行sayHiLater(),執行完畢
- setTimeOut在瀏覽器外等待,引擎看有沒有函數在等待
- 發現setTimeOut,執行,找不到greeting這個變數
- 在閉包裡有 greeting ,輸出greeting
2.jQuery
$("button").click(function(){
})
click 是一個函數,他接受另一個函數在事件發生時執行,我們使用了一級函數做為參數
回呼函數
當某個函數執行完,回呼你給它的函數
function tellMeWhenDone(callback){
var a=1000;
var b=2000;
callback();
}
tellMeWhenDone(function(){
console.log('I am Done!');
})
tellMeWhenDone(function(){
alert('I am Done!');
})
U24 call()、apply() 與 bind()
函數就是物件,所以他可以擁有屬性和方法
先看下面的範例:
var person={
firstname:'John',
lastname:'Doe',
getFullName:function(){
//this 指向person整個物件
var fullname =this.firstname+''+this.lastname;
return fullname;
}
}
var logName =function(lang1,lang2){
//this 指向全域物件
console.log('Logged :'+this.getFullname);
}
logName()
//undefined is not a funcntion
會出現錯物,因為this 會指向全域物件,而全物物件沒有getFullName
如何要控制this指向哪呢?有三種方法call()
、apply()
、bind()
:
1.bind
:創造拷貝,控制 this 指向誰
var person={
firstname:'John',
lastname:'Doe',
getFullName:function(){
var fullname =this.firstname+''+this.lastname;
return fullname;
}
}
var logname =function(lang1,lang2){
console.log('Logged :'+this.getFullname);
}
//傳入我們想要this變數指向的物件
var logPersonName = logName.bind(person)
logPersonName()
是 logName 而不是logName(),因為在這裡我們把函數當作物件,我們在bind中放入我們想要this變數指向的物件,bind 會回傳新的函數,它會複製logName這個函數,設定新的函數物件,當環境被創造時,this 的指向同時也會改變
我們也可以更簡化的寫成:
var person={...}
var logName =function(lang1,lang2){
console.log('Logged :'+this.getFullname);
}.bind(person)
logName()
2.call
:呼叫函數,控制 this 指向誰
var person={...}
var logname =function(lang1,lang2){
console.log('Logged :'+this.getFullname);
})
//也可寫成這樣:logName.call = logName()
logName.call(person,'en,'es')
方法 call 會呼叫函數,我們會在 call 中放入 this 指向哪裡,剩下放入傳給函數的參數
3.apply
var person={...}
var logname =function(lang1,lang2){
console.log('Logged :'+this.getFullname);
})
logName.apply(person,['en,'es'])
apply方法只接受陣列作為參數,這是call和apply唯一的差別,陣列在數學運算比較強大,依據使用函數的情況,我們有兩種選擇call
和apply
我們也可以使用 apply 搭配 IIFE
(function(lang1,lang2){
console.log('Logged :'+this.getFullname);
}).apply(person,['es','en']);
以下為實際使用改變 this 的方法的例子
1. function borrowing 函數借用
使用 call 或 apply 來實作函數借用
var person={
firstname:'John',
lastname:'Doe',
getFullName:function(){
var fullname =this.firstname+''+this.lastname;
return fullname;
}
var person2={
firstname:'Jane',
lastname:'Doe',
}
console.log(person.getfullName.apply(person2));
//Jane Doe
用 apply 借用 person 的函數給 person2 使用,apply呼叫 getfullName,this 指向person2。
2. function currying
建立一個函數的拷貝,並設定預設的參數
function multiply(a,b){
return a*b;
}
//在這裡this不重要
var multipeByTwo=multiply.bind(this,2)
//第二個參數
multipeByTwo(4)
用 bind 給他的參數會設定為拷貝函數的永久參數值,這和下面是一個的概念
function multiply(b){
var a=2;
return a*b;
}
U25 函數程式設計(一)
我們先有一段這樣的程式碼:
var arr1=[1,2,3];
console.log(arr1);
var arr2=[];
for(var i=0;i<arr1.length;i++){
arr2.push(arr1[i]*2);
}
console.log(arr2);
//[1,2,3]
//[[2,4,6]
我們可以把程式放入函數中來最小化程式,漸少重複的工作,
因為 JS 有一級函數,所以我們可以做到這點:
function mapForEach(arr,fn){
var newArr=[];
for(var i=0;i<arr.length;i++){
newArr.push(
fn(arr[i])
)
};
return newArr;
}
var arr1=[1,2,3];
console.log(arr1);
var arr2= mapForEach(arr1,function(item){
return item*2
})
console.log(arr2)
//[[2,4,6]
var arr3= mapForEach(arr1,function(item){
return item>2
})
console.log(arr3)
//[false,false,true]
我們呼叫函數mapForEach,串入陣列arr1,告訴他該對陣列做什麼function(item)…。只要傳入函數,我們就可以重複利用mapForEach來做不同的任務,
另一個例子:傳入自己的變數
function mapForEach(arr,fn){
var newArr=[];
for(var i=0;i<arr.length;i++){
newArr.push(
fn(arr[i])
)
};
return newArr;
}
var checkPastLimit=function(limiter,item){
return item>limiter;
}
//預設 limiter 為 1
var arr4 = mapForEach(arr1,checkPastLimit.bind(this,1));
console.log(arr4)
//[false,true,true]
每次都要傳入bind很麻煩,所以我們可以建立一個只要傳入限制值的函數
function mapForEach(arr,fn){
var newArr=[];
for(var i=0;i<arr.length;i++){
newArr.push(
fn(arr[i])
)
};
return newArr;
}
var checkPastLimitSimplified=function(limiter){
return function(limiter,item){
return item>limiter;
}.bind(this,limiter);
};
var arr5 = mapForEach(arr1,checkPastLimitSimplified(1));
console.log(arr5)
//[false,true,true]
呼叫 checkPastLimitSimplified 來回傳一個函數物件
functional programming 不只是把所有的程式分割進函數中,可以開始思考要如何讓函數或是,被回傳的函數更簡單寫出來
另一個函數程式設計要注意的點是,當你在移動或傳入小函數時,盡量不要改變data,你可能會遇到一些不好的情況,最好是能夠在層級高的函數改變data 或盡量不要更動他們,而是直接回傳一個新的東西像是這邊的新陣列
和函數程式設計會讓JavaScript非常強大,讓你大幅升級,不要只是把JavaScript像是別的程式語言使用
U26 函數程式設計(二)
underscore.js
很有名JavaScript的資源庫,幫助你處理陣列和物件的集合
- [underscore.js]https://underscorejs.org/
- 到 annoted source 看程式碼和註解分離的版本或下載 development version
所有code 都包在 IIFE 裡面
lodash.js 資源庫
他和underscore很像,但使用了一些其他功能,讓執行速度更快
看他們的原始碼,我們會學到很多,開始想像你可以應用這些概念到自己的程式,
使用一級函數和它的優點來寫出,乾淨的程式碼,讓其他程式設計師也能使用。