0%

函式 function

函式

什麼是函式

「函式」指的是將一或多段程式指令包裝起來,可以重複使用,也方便維護。
函式是物件的一種

一個函式會有以下特性:

  • 函式是可被呼叫的。
  • 函式的名稱 (也可能沒有名稱)。
  • 在括號 ( ) 中的部分,稱為「參數 (arguments) 」,參數與參數之間會用逗號 , 隔開。
  • 在大括號 { } 內的部分,內含需要重複執行的內容,是函式功能的主要區塊。

宣告函式的方法

  • 具名函式
    使用 function 名稱(參數) { ... }; 的方式
function functionA() {
console.log('具名函式', '函式陳述式');
console.log(functionA)
}
functionA()

ƒ functionA() {
console.log(‘具名函式’, ‘函式陳述式’);
console.log(functionA)
}

  • 匿名函式
    透過 變數名稱 = function(參數){ ... }; 的方式,將一個函式透過 = 指定給某個變數。

    var functionB = function () {
    console.log('匿名函式', '函式表達式');
    console.log(functionB)
    }
    functionB()

    ƒ () {
    console.log(‘匿名函式’, ‘函式表達式’);
    console.log(functionB)
    }

    在匿名函式中,如果想要在 function 後面加上一個名字也是可以的,但是要注意的是,這個名字只在「自己函式的區塊內」有效。
    脫離了函式自身區塊後,變數 functionD 就不存在了。

    var functionC = function functionD() {
    console.log(functionC, functionD)
    //具名函式能在函式內被調用
    }
    functionC()
    console.log(functionC)
    console.log(functionD) //functionD is not defined

    ƒ functionD() {
    console.log(functionC, functionD)
    } ƒ functionD() {
    console.log(functionC, functionD)
    }

    var num = 1 ;
    var giveMeMoney = function giveMoreMoney(coin) {
    num += 1;
    console.log('執行 giveMeMoney', num, coin);
    return coin > 100 ? coin : giveMoreMoney(num*coin);
    }
    console.log(giveMeMoney(30))

    執行 giveMeMoney 2 30
    執行 giveMeMoney 3 60
    執行 giveMeMoney 4 180
    180

    function callSomeOne(fn) {
    fn()
    }
    callSomeOne(function() {console.log('執行函式')})

    執行函式

  • 透過 new Function 關鍵字建立函式
    使用 new Function (注意 F 大寫) 這個關鍵字來建立函式物件。
    使用時將參數與函式的內容依序傳入 Function

// 透過 new 來建立 Function "物件"
var square = new Function('number', 'return number * number');

透過 new Function 所建立的函式物件,每次執行時都會進行解析「字串」(如 'return number * number' ) 的動作,運作效能較差,所以通常實務上也較少會這樣做。

  • 箭頭函式 The arrow function expression (=>)
    ES6新增的箭頭函式,使程式碼更為精簡。
    function 改成 => ,並移至 () 的後方。
Arrow() => {
console.log('箭頭函式');
};
Arrow

立即函式(IIFE)

IIFE

IIFE (Immediately Invoked Function Expression) 是一個定義完馬上就執行的 JavaScript function,是一種常見的設計模式。
又可稱為 Self-Executing Anonymous Function。

IIFE包含兩部分

  1. 使用 Grouping Operator () 包含的匿名函式。
  2. 馬上執行 function 的 expression (),JavaScript 引擎看到它就會立刻轉譯該 function。
    (function(參數){ ... })()

特性:

  • 定義完立刻就執行
  • 無法於函式外再次被執行
    (function IFFE() {
    console.log('立即函式', IFFE)
    })();
    console.log(IFFE)

    立即函式 ƒ IFFE() {
    console.log(‘立即函式’, IFFE)
    }
    IFFE is not defined

  • 限制變數的作用域:function scope 內的變數在外面是無法存取的
    (function () {
    var Name = 'IFFE';
    console.log(Name) //IFFE
    })();
    console.log(Name) //Name is not defined
  • 可以傳遞參數
    (function (para) {
    console.log('IFFE' + para) //IFFE可以傳遞參數
    })('可以傳遞參數');
    // 把 IIFE 設定變數並儲存它的結果
    var result = (function (parameter) {
    return parameter
    })('return IFFE 結果')
    console.log(result) //return IFFE 結果
  • 須符合ASI規則:因立即函示不包含在ASI的規則內,故於2個函式間須插入 ; ,否則會出現錯誤。
(function () {
})()
(function () {
})()
// (intermediate value)(...) is not a function
(function () {
})();
(function () {
})()
  • function間傳遞參數

利用物件傳參考的特性,讓不同的function可以互相傳遞。

var a = {};
(function (b) {
b.parameter = '這是傳遞的變數'
})(a);
(function (c) {
console.log(c.parameter) //這是傳遞的變數
})(a)

透過全域物件傳遞變數,常使用在大型框架上,如 Vue.js,此方式可以確保框架可以正確的掛載於全域物件上。

(function (global) {
global.parameter = '透過window傳遞變數'
})(window);
(function () {
console.log(parameter) //透過window傳遞變數
})()

其它形式

  1. JSLint:符合 JSLint 的版本,須將 expression () 包進外層的 grouping operator () 內。
(function () {
}());
  1. Arrow function:ES6新增的箭頭函式,使程式碼更為精簡,但行為一致。
(() => {
})();
  1. Async function:ES7新增的 async ,目前主要為了 top level await 而使用。
(async function () {
})();
//搭配箭頭函式
(async () => {
})();

參數

什麼是參數(parameter)?
參數其實就是我們會帶入函式的變數。

var globalVariable = '全域變數';
var obj = {
aFunction: function(para) {
var localVariable = '區域變數';
console.log(para, localVariable, arguments, this, globalVariable)
}
}
obj.aFunction('一段描述', 2, 3);

一段描述 區域變數 >Arguments(3) [“一段描述”, 2, 3, callee: ƒ, Symbol(Symbol.iterator): ƒ*]* {aFunction: ƒ} 全域變數


  1. 一段描述:外層所傳入的參數,雖然外層傳入3個參數,但是 function 只定義一個,而參數在給值的過程是由左至右的,所以只顯示一個。
  2. 區域變數:function內所定義的區域變數。
  3. Arguments(3):arguments 其實就是 parameters 的意思,也就是說,arguments 會包含所有你放入 function 中的參數值。
    arguments回傳的值是斜體的 [ ] ,它並不是真的陣列而是一個 類陣列
  4. this:執行函式時會自動帶入的變數。

補充:執行物件內函式的this與一般函式有所不同。

  1. 全域變數:來自於外層的變數,可參考範圍鍊章節。

Hoisting與參數的關係

  • function的變數在傳入前已經定義完成,故function內無法重複宣告變數。
  • Hoistion並無會影響傳入的參數
    • 範例一:
function callFunction(a) {
console.log(a); //傳入的參數
var a; //無法重複宣告變數,無作用
console.log(a); //傳入的參數
a = '修改的參數';
console.log(a); //修改的參數
};
callFunction('傳入的參數');
  • 範例二:
function callFunction(a) {
console.log(a); //ƒ a() {}
function a() {};
};
callFunction('傳入的參數');

參數與傳入值之間的關聯性

function callMore (d, c, b, a) {
console.log(d, c, b, a)
}
var a = 'a';
var b = 'b';
var c = 'c';
callMore(a, b, c)

a b c undefined


  1. 參數的名稱是可以自定義的,與傳入的值並沒有關聯性。
  2. 傳入的參數與 function 內的變數數量不一致時,多出的變數會顯示 undefined

註:因為當 JavaScript 在執行這個 function 的時候,它會先為我們的參數建立好記憶體位置,並且賦予它的值是 undefined 。(Javascript 中的 Hoisting 特性)

  1. 參數在給值的過程是由左至右的。

callback function

function helloWorld(para) {
console.log(para);
}
function functionA(fn) {
fn('HelloWorld');
}
functionA(helloWorld)

arguments

arguments會包含所有你放入function中的參數值。
arguments回傳的值是斜體的 [ ] ,它並不是真的陣列而是一個 類陣列

註:類陣列是指以數值索引的值所成的群集,它可能是串列但並非真正的陣列。可使用 for 但不能使用 indexOfconcatforEach 等操作。

function callArg(a) {
console.log(a, arguments) //1 >Arguments(4) [1, 2, 3, "4", callee: ƒ, Symbol(Symbol.iterator): ƒ]
//for 迴圈
for(let i = 0; i <arguments.length; i++ ) {
console.log(arguments[i])
}
}
callArg(1, 2, 3, '4')

展開運算子spread(…)

JavaScript於ES6中另外提供了一個展開運算子(spread),由3個 . 組成 「」。
可以把函數中許多的參數(arguments)或陣列中許多的元素(elements)展開(expand)成個別數值的速寫語法,簡單來說,就是把陣列裡面的值,拆解成一個一個。

function newFunction(a, b, ...other) {
console.log(other) //["hello", "world"]
}
newFunction(1, 2, 'hello', 'world')

參考:展開運算子spread MDN

閉包 Closure

閉包(Closure)是函式以及該函式被宣告時所在的作用域環境(lexical environment)的組合。

  1. 運作原理:呼叫 Function 內的 Function。
  2. 內層 Function 作用域變數只會存在內層。
  3. 內層 Function 變數可以不被釋放,重複使用。
  4. 理解作用域。

大多數人會利用一個全域變數來儲存變數的資訊。但是,當程式碼開始變多了,過多的全域變數會造成不可預期的錯誤,像是變數名稱衝突、沒用到的變數無法回收等等的。
這時候改用閉包的做法就可以避免這些問題。

function storeMoney() {
var money = 1000;
return function(price) {
money = money + price
return money
}
}
var myMoney = storeMoney();
console.log(myMoney(1000)) //2000
var yourMoney = storeMoney();
console.log(yourMoney(10000)) //11000
function counter() {
var count = 0;
function innerCounter() {
return count += 1;
}
return innerCounter;
}
var countFunc = counter();

console.log( countFunc() ) //1
console.log( countFunc() ) //2
console.log( countFunc() ) //3
function makeAdder(x) {
return function(y) {
return x + y
}
}
var add5 = makeAdder(5)
var add10 = makeAdder(10)

console.log( add5(7) ) //12
console.log( add10(7) ) //17

參考:閉包 MDN
參考:閉包 重新認識 JavaScript
參考:閉包 邁向 JavaScript 勇者之路

延伸

因為作用域的關係,因為 var 會直接將 i 宣告成全域變數,不斷透過 for 迴圈累加,只會得到 3 這個數值。

function arrFun() {
var arr = [];
for(var i = 0; i < 3; i++) {
arr.push(function() {
console.log(i);
});
}
console.log(i); //3
console.log(arr); //(3) [ƒ, ƒ, ƒ]
return arr;
}
var fn = arrFun();
console.log(fn[0]()) //3
console.log(fn[1]()) //3
console.log(fn[2]()) //3

使用立即函示來限制作用域。

function arrFun() {
var arr = [];
for(var i = 0; i < 3; i++) {
(function(j) {
arr.push(function() {
console.log(j);
});
}(i))
}
console.log(i); //3
return arr;
}
var fn = arrFun();
console.log(fn[0]()) //1
console.log(fn[1]()) //2
console.log(fn[2]()) //3

使用ES6新增變數 let
因為 let 是屬於 block 變數,i 會被緊緊的鎖在 for… 後方的 {} 內。

function arrFun() {
var arr = [];
for(let i = 0; i < 3; i++ ) {
arr.push(function() {
console.log(i);
});
}
return arr;
}
var fn = arrFun();
console.log(fn[0]()) //0
console.log(fn[1]()) //1
console.log(fn[2]()) //3

工廠模式

某些情形下函式的預設值是未知的,此時透過 || 來加入預設值,在沒有輸入任何值的情況下會使用預設值代替。

function storeMoney(initValue) {
var money = initValue || 1000;
return function(price) {
money = money + price
return money
}
}
var myMoney = storeMoney(100)
console.log(myMoney(500)) //600

透過物件的方式對初始值做不同的操作,可大幅增加函式的可用性。

function storeMoney(initValue) {
var money = initValue || 1000;
return {
increase: function(price) {
money += price;
},
decrease: function(price) {
money -= price;
},
value: function(price) {
return money
}
}
}
var myMoney = storeMoney(2000);
myMoney.increase(1500)
myMoney.decrease(489)
myMoney.decrease(369)
console.log(myMoney.value()) //2642

this

What’s this?

  • this 是 JavaScript 的一個關鍵字。
  • this 是 function 執行時,自動生成的一個內部物件。
  • 影響 this 的是在於函式的呼叫方法。
  • 在大多數的情況下, this 代表的就是呼叫 function 的物件 (Owner Object of the function)。

基本觀念

  • 每一個執行環境都有屬於自己的 this
  • this 與函式如何宣告沒有關聯性,僅與呼叫方法有關。
  • 嚴格模式下,簡易呼叫會有很大改變。

this 的調用方式

  • 簡易呼叫(大多數的呼叫方式)
  • 物件方式(最常運用 this 的方法)
  • bindapplycall 方法。
  • new
  • DOM 事件處理器
  • 箭頭函式(Arrow functions)

Global context(全域環境)

In the global execution context (outside of any function), this refers to the global object whether in strict mode or not.
在全域執行環境下, this 指向全域物件,瀏覽器中是 Window

console.log( this === window )    //true
this.a = "this";
console.log(window.a) //this

Function context

Inside a function, the value of this depends on how the function is called.

簡易呼叫 (Simple call)

直接調用函式,此函式的 this 會指向 window。範例一
無論在哪一層,簡易呼叫的方式 this 都會指向 window。範例二
簡易呼叫的本質其實是 function.prototype.call() 代入 undefinedcall()

盡可能不要使用 Simple call 的 this
function f1() {
return this;
}
f1() === window; //true
window.a = '我是window'
function callFunA () {
console.log('call:', this.a);
//this === window
}
callFunA () //call: 我是window
window.b = '我還是window'
function callFunB () {
console.log('call:', this.b); //call: 我還是window
//function 內的 function
function callAgainFunB () {
console.log('call again:', this.b); //call again: 我還是window
}
callAgainFunB()
}
callFunB()

物件的調用(As an object method)

如果一個函式是以物件的方法呼叫,它的 this 會設為該呼叫函式的物件。

  • this 與函式如何宣告沒有關聯性,僅與呼叫方法有關。
  • 物件的調用,僅需要關注是在哪一個物件下呼叫。
var name = '我是window'
var obj = {
name: '我是外層物件',
fn: fn,
obj2: {
name: '我是內層物件',
fn: fn
}
}
function fn() {
return this.name;
}
console.log(obj.fn()); // 我是外層物件
console.log(obj.obj2.fn()); // 我是內層物件
var name = '我是window'
var obj = {
name: '我是外層物件',
fn: fn,
obj2: {
name: '我是內層物件',
fn: fn
}
}
function fn() {
return this.name;
}
var callName = obj.fn;
console.log(callName()); //我是window

bindapplycall 方法

函式未以物件的方法呼叫,它的 this 會指向全域物件 window
此時可使用bindapplycallthis 以特定的物件 or 純值傳入。

差異:

  • call
    • 函式會立刻執行。
    • 語法大致上與 apply() 相同,不同處只有 call() 接受一連串的參數。
  • apply
    • 函式會立刻執行。
    • 語法大致上與 call() 相同,不同處只有 apply() 使用單一的array作為參數。
  • bind
    • 函式不會立刻執行,須另外呼叫。

語法:
fun.call(thisArg[, arg1[, arg2[, ...]]])

  • thisArg


    呼叫fun時提供的this值。可以為物件 or 純值。

    在非嚴苛模式( non-strict mode ), nullundefined 將會被置換成全域變數,而原生型態(純值)的值將會使用建構式方式建立。
  • arg1 arg2


    其他參數。

    call() 接受一連串的參數。
var obj = { name: '我是物件' }
function fn(para1, para2) {
console.log(this, this.name, para1, para2)
}
fn.call(obj, 1, 2) //{name: "我是物件"}name: "我是物件" "我是物件" 1 2

代入純值的情況下,純值(原生型態)的值將會使用建構式方式建立。

var name = '我是字串'
function fn(para1, para2) {
console.log(this, para1, para2)
}
fn.call(name, 1, 2) //String {"我是字串"} 1 2

非嚴苛模式( non-strict mode )下,代入nullundefined 的情況下, this 將會被置換成全域變數。

function fn(para1, para2) {
console.log(this, para1, para2)
}
fn.call(undefined, 1, 2) //window {....} 1 2

語法
fun.apply(thisArg, [argsArray])

  • thisArg


    呼叫fun時提供的this值。可以為物件 or 純值。

    在非嚴苛模式( non-strict mode ), nullundefined 將會被置換成全域變數,而原生型態(純值)的值將會使用建構式方式建立。
  • argsArray


    一組陣列形式的參數(array-like object)。

    如果沒有需要提供,可以傳入 nullundefined
var obj = { name: '我是物件' }
function fn(para1, para2) {
console.log(this, this.name, para1, para2)
}
fn.apply(obj, [1, 2]) //{name: "我是物件"}name: "我是物件" "我是物件" 1 2

代入純值的情況下,純值(原生型態)的值將會使用建構式方式建立。

var number = 100
function fn(para1, para2) {
console.log(this, para1, para2)
}
fn.apply(number, [1, 2]) //Number {100} 1 2

非嚴苛模式( non-strict mode )下,代入nullundefined 的情況下, this 將會被置換成全域變數。

function fn(para1, para2) {
console.log(this, para1, para2)
}
fn.apply(undefined, [1, 2]) //window {....} 1 2

bind() 方法,會建立一個新函式。當該函式被呼叫時,會將 this 傳遞给 bind() 的第一個參數。
語法
fun.bind(thisArg[, arg1[, arg2[, ...]]])

  • thisArg


    當綁定的函式被調用時,該參數會做為 this 而被指向。

    當使用 new 調用綁定的函式,此參數無效。
  • arg1 arg2


    當綁定的函式被調用時,這些參數加上綁定函數本身的參數會按照順序作為原函數運行時的參數。

    如果傳入了多餘函式設定的參數,也只會取函式本身設定參數的數量。
var obj = { name: '我是物件' }
function fn(a, b, c) {
console.log(this, this.name, a + b + c)
}
var _fn = fn.bind(obj, 20,30,40)
_fn() //{name: "我是物件"} "我是物件" 90

代入純值的情況下,純值(原生型態)的值將會使用建構式方式建立。

var boolean = true
function fn(a, b, c) {
console.log(this, a + b + c)
}
var _fn = fn.bind(boolean, 20,30,40)
_fn() //Boolean {true} 90

非嚴苛模式( non-strict mode )下,代入nullundefined 的情況下, this 將會被置換成全域變數。

window.x = 100
function fn(a, b) {
return this.x+a+b
}
var _fn = fn.bind(undefined, 20,30,40)
_fn() //150 //40不代入

嚴謹模式 (strict mode)

  • 加入 'use strict' 即可運作。
  • 並不會影響不支援嚴謹模式的瀏覽器。
  • 可依據執行環境設定'use strict'
  • 透過拋出錯誤的方式消除一些安靜的錯誤(意指不再靜默地忽略某些錯誤)。
  • 禁止使用一些有可能被未來版本 ECMAScript 定義的語法。

In strict mode, however, if the value of this is not set when entering an execution context, it remains as undefined.
在嚴謹模式下,未定義的 this 不會指向執行環境的 window,而是 undefined

function f2() {
'use strict'; // 嚴謹模式
return this;
}
f2() === undefined; // true

在嚴謹模式下,代入純值不會將純值以建構式方式建立,而是維持原來的型別。

var number = 100
function fn(para1, para2) {
'use strict'
console.log(this, typeof this, para1, para2)
}
fn.call(number, 1, 2) //100 "number" 1 2

代入 undefined 不會將 this 指向 window ,而是維持原來的型別。

function fn(para1, para2) {
'use strict'
console.log(this, typeof this, para1+para2)
}
fn.call(undefined, 1, 2) //undefined "undefined" 3

DOM (Document Object Model)

於空頁面下建立一個清單。

(function(){
var ul = document.createElement('ul');
ul.setAttribute('id','newList');

productList = ['1','2','3','4'];

document.body.appendChild(ul);
productList.forEach(renderProductList);

function renderProductList(element, index, arr) {
var li = document.createElement('li');
li.setAttribute('class','item');

ul.appendChild(li);

li.innerHTML=li.innerHTML + element;
}
})();
於 DOM 下使用 this 會指向目前所觸發的DOM元素。
var fn = function() {
console.dir(this);
this.style.backgroundColor = 'orange'
}
var els = document.querySelectorAll('li')

for (let i=0; i < els.length; i++ ){
els[i].addEventListener('click', fn)
}

Arrow functions

待更新。