[課程筆記]克服JS的奇怪部分-CH3 物件與函數(下)

Udemy課程連結
課程總目錄

本篇筆記目錄:

U20 立即呼叫的函數表示式(IIFEs)
框架小叮嚀: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

為什麼呢?我們可以觀察執行過程:

  1. 全域執行環境建立,包含 buildFunctions(),fs
  2. buildFunctions()執行環境建立,包含 i 和 arr
  3. for 迴圈執行,直到 i=3 時停止,所以回傳時 i=3
  4. buildFunctions()離開執行堆
  5. fs0執行環境建立,找不到 i 變數,到外部環境找,輸出 3
  6. 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();

執行過程:

  1. 執行sayHiLater(),執行完畢
  2. setTimeOut在瀏覽器外等待,引擎看有沒有函數在等待
  3. 發現setTimeOut,執行,找不到greeting這個變數
  4. 在閉包裡有 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唯一的差別,陣列在數學運算比較強大,依據使用函數的情況,我們有兩種選擇callapply

我們也可以使用 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的資源庫,幫助你處理陣列和物件的集合

  1. [underscore.js]https://underscorejs.org/
  2. 到 annoted source 看程式碼和註解分離的版本或下載 development version

所有code 都包在 IIFE 裡面

lodash.js 資源庫

他和underscore很像,但使用了一些其他功能,讓執行速度更快

看他們的原始碼,我們會學到很多,開始想像你可以應用這些概念到自己的程式,
使用一級函數和它的優點來寫出,乾淨的程式碼,讓其他程式設計師也能使用。