您好,登錄后才能下訂單哦!
這期內容當中小編將會給大家帶來有關JavaScript函數的特性有哪些,文章內容豐富且以專業的角度為大家分析和敘述,閱讀完這篇文章希望大家可以有所收獲。
1 函數對象
JavaScript 函數就是對象。對象是名值對的集合,它還擁有一個連接到原型對象的鏈接。對象字面量產生的對象連接到 Object.prototype
,而函數對象連接到 Function.prototype(這個對象本身連接到 Object.prototype
)。每個函數在創建時會附加兩個隱藏屬性:函數的上下文以及實現函數的代碼。
函數對象在創建后會有一個 prototype 屬性,它的值是一個擁有 constructor 屬性、且值既是該函數的對象。
因為函數是對象,所以可以被當做參數傳遞給其他函數。它也可以再返回函數。
函數可以通過字面量進行創建:
var add = function (a, b) { return a + b; }
這里沒有給函數命名,所以稱它為匿名函數。
一個內部函數除了可以訪問自己的參數和變量之外,還可以訪問它的父函數的參數和變量。通過函數字面量創建的函數對象包含一個連接到外部上下文的連接,這被稱為閉包。它是 JavaScript 強大表現力的來源。
調用一個函數會暫停當前函數的執行,它會傳遞控制權和參數給這個被調用的函數。
當函數的實際參數的個數與形式參數的個數不匹配時,不會導致運行時錯誤。如果實際參數的個數過多,那么超出的參數會被忽略;如果實際參數的個數過少,那么缺失的值會是 undefined。不會對參數類型進行檢查,所以任何類型的值都可以被傳遞給任何參數。
當一個函數被保存為對象的一個屬性時,就稱它為方法。當方法被調用時,this 被綁定到這個對象。如果調用表達式包含一個提取屬性的動作(即包含一個”.” 點表達式或 “[]” 下標表達式),那么它就是被當做一個方法被調用。
var myObject = { value: 0,//屬性 increment: function (inc) {//方法 this.value += typeof inc === 'number' ? inc : 1; } }; myObject.increment(); console.log(myObject.value);//1 myObject.increment(2); console.log(myObject.value);//3
這里可以使用 this 來訪問自己所屬的對象。通過 this 可以取得它們所屬對象的上下文的方法被稱為公共方法。
var add = function (a, b) { return a + b; } var sum = add(3, 4);//7;this 被綁定到全局對象
這里的 this 被綁定到全局對象,這其實是語言設計上的失誤!如果設計正確,那么當內部函數被調用時,this 應該被綁定到外部函數的 this 變量才是。可以這樣解決:為這個方法定義一個變量并給它賦值為 this,這樣內部函數就可以通過這個變量訪問到 this 啦,一般把這個變量命名為 that:
myObject.double = function () { var that = this;//讓內部函數可以通過這個變量訪問到 this (myObject) var helper = function () { that.value = add(that.value, that.value); }; helper();//以函數形式調用 helper }; myObject.double();//以方法形式調用 helper console.log(myObject.value);//6
JavaScript 是基于原型繼承的語言,所以對象可以從其他對象繼承它們的屬性。
如果在函數之前加上 new ,那么 JavaScript 就會創建一個連接到該函數的 prototype 屬性的新對象,而 this 會綁定到這個新對象。
/** * 構造器調用模式(不推薦) */ var Quo = function (string) {//定義構造器函數;按照約定,變量名首字母必須大寫 this.status = string;//屬性 }; /** * 為 Quo 的所有實例提供一個名為 get_status 的公共方法 * @returns {*} */ Quo.prototype.get_status = function () { return this.status; }; var myQuo = new Quo("confused");//定義一個 Quo 實例 console.log(myQuo.get_status());//"confused"
按照約定,構造器函數被保存在以大寫字母命名的變量中。因為如果調用構造器函數時沒有加上 new,問題很大,所以才以大寫字母的命名方式讓大家記住調用時要加上 new。
因為 JavaScript 是函數式的面向對象語言,所以函數可以擁有方法。
apply 方法可以構建一個參數數組,然后再傳遞給被調用的函數。這個方法接收兩個參數:要綁定給 this 的值以及參數數組。
//相加 var array = [3, 4]; var sum = add.apply(null, array);//7 console.log(sum); //調用 Quo 的 get_status 方法,給 this 綁定 statusObject 上下文 var statusObject = { status: 'A-OK' }; var status = Quo.prototype.get_status.apply(statusObject); console.log(status);//'A-OK'
當函數被調用時,會有一個 arguments 數組。它是函數被調用時,傳遞給這個函數的參數列表,包含那些傳入的、多出來的參數。可以利用這一點,編寫一個無須指定參數個數的函數:
//構造一個能夠接收大量參數,并相加的函數 var sum = function () { var i, sum = 0; for (i = 0; i < arguments.length; i += 1) { sum += arguments[i]; } return sum; }; console.log(sum(4, 5, 6, 7, 8, 9));//39
arguments 不是一個真正的數組,它只是一個類數組的對象,它擁有 length 屬性,但沒有數組的相關方法。
return 語句可以讓函數提前返回。return 被執行時,函數會立即返回。
一個函數總會返回一個值,如果沒有指定這個值,它就會返回 undefined。
如果使用 new 前綴來調用一個函數,那么它的返回值是:創建的一個連接到該函數的 prototype 屬性的新對象。
異常是干擾程序正常流程的事故。發生事故時,我們要拋出一個異常:
var add = function (a, b) { if (typeof a !== 'number' || typeof b !== 'number') { throw{ name: 'TypeError', message: 'add needs numbers' }; } return a + b; }
throw 語句會中斷函數的執行,它要拋出一個 exception 對象,這個對象包含一個用來識別異常類型的 name 屬性和一個描述性的 message 屬性。也可以根據需要,擴展這個對象。
這個 exception 對象會被傳遞到 try 語句的 catch 從句:
var try_it = function () { try { add("seven"); } catch (e) { console.log(e.name + ": " + e.message); } }; try_it();
一個 try 語句只會有一個捕獲所有異常的 catch 從句。所以如果處理方式取決于異常的類型,那么我們就必須檢查異常對象的 name 屬性,來確定異常的類型。
可以給 Function.prototype 增加方法來使得這個方法對所有的函數都可用:
/** * 為 Function.prototype 新增 method 方法 * @param name 方法名稱 * @param func 函數 * @returns {Function} */ Function.prototype.method = function (name, func) { if (!this.prototype[name])//沒有該方法時,才添加 this.prototype[name] = func; return this; };
通過這個方法,我們給對象新增方法時,就可以省去 prototype 字符啦O(∩_∩)O~
有時候需要提取數字中的整數部分,我們可以為 Number.prototype
新增一個 integer 方法:
Number.method('integer', function () { return Math[this < 0 ? 'ceil' : 'floor'](this); });
它會根據數字的正負來決定是使用 Math.ceiling
還是 Math.floor
。
然后再為 String 添加一個移除字符串首尾空白的方法:
String.method('trim', function () { return this.replace(/^\s+|\s+$/g, ''); });
這里使用了正則表達式。
通過為基本類型增加方法,可以極大地提高 JavaScript 的表現力。因為原型繼承的動態本質,新的方法立刻被賦予所有的對象實例上(甚至包括那些在方法被增加之前的那些對象實例)
基本類型的原型是公用的,所以在使用其他類庫時要小心。一個保險的做法是:只在確定沒有該方法時才添加它。
Function.prototype.method = function (name, func) { if (!this.prototype[name])//沒有該方法時,才添加 this.prototype[name] = func; return this; };
遞歸函數是會直接或間接地調用自身的函數。它會把一個問題分解為一組相似的子問題,而每一個子問題都會用一個尋常的解來解決。
漢諾塔的游戲規則是:塔上有 3 根柱子和一套直徑不相同的空心圓盤。開始時,源柱子上的所有圓盤都是按照從小到大的順序堆疊的。每次可以移動一個圓盤到另一個柱子,但不允許把較大的圓盤放置在嬌小的圓盤之上。最終的目標是把一堆圓盤移動到目標柱子上。我們可以用遞歸解決這個問題:
/** * 漢若塔 * @param disc 圓盤編號 * @param src 源柱子 * @param aux 輔助用的柱子 * @param dst 目的柱子 */ var hanoi = function (disc, src, aux, dst) { if (disc > 0) { hanoi(disc - 1, src, dst, aux); console.log('Move disc ' + disc + ' from ' + src + ' to ' + dst); hanoi(disc - 1, aux, src, dst); } }; hanoi(3, 'Src', 'Aux', 'Dst');
這里演示了如果圓盤數為 3 的解法:
這個問題可以分解為 3 個子問題。首先移動一對圓盤中較小的圓盤到輔助的柱子上,從而露出下面較大的圓盤;然后再移動下面的圓盤到目標柱子。最后再將較小的圓盤從輔助柱子再移動到目標柱子上。通過遞歸調用自身來處理圓盤的移動,就可以解決這些子問題。
上面這個函數最終會以一個不存在的圓盤編號被調用,但它不執行任何操作,所以不會導致死循環。
遞歸函數可以非常高效地操作樹型結構。比如文檔對象模型(DOM),我們可以在每次遞歸調用時處理指定樹的一小段:
/** * 從某個節點開始,按照 HTML 源碼中的順序,訪問該樹的每一個節點 * @param node 開始的節點 * @param func 被訪問到的每一個節點,會作為參數傳入這個函數,然后這個函數被調用 */ var walk_the_DOM = function walk(node, func) { func(node); node = node.firstChild; while (node) { walk(node, func); node = node.nextSibling; } };
/** * 查找擁有某個屬性的元素 * @param att 屬性名稱字符串 * @param value 匹配值(可選) * @return 匹配的元素數組 */ var getElementsByAttributes = function (att, value) { var results = []; walk_the_DOM(document.body, function (node) { var actual = node.nodeType === 1 && node.getAttribute(attr); if (typeof actual === 'string' && (actual === value) || typeof value != 'string') { results.push(node); } }); return results; };
注意: 深度遞歸的函數會因為堆棧溢出而運行失敗。比如一個會返回自身調用函數結果的函數,它被稱為尾遞歸函數。
/** * 求階乘(帶尾遞歸的函數) * * JavaScript 當前沒有對尾遞歸進行優化,所以如果遞歸過深會導致堆棧溢出 * @param i * @param a * @returns {*} 返回自身調用的結果 */ var factorial = function factorial(i, a) { a = a || 1; if (i < 2) { return a; } return factorial(i - 1, a * i); }; console.log(factorial(4));
作用域控制著變量和參數的可見性以及生命周期,它減少了名稱沖突,而且提供自動內存管理機制。
var foo = function () { var a = 3, b = 5; var bar = function () { var b = 7, c = 11; console.log("a:" + a + ";b:" + b + ";c:" + c);//a:3;b:7;c:11 a += b + c; console.log("a:" + a + ";b:" + b + ";c:" + c);//a:21;b:7;c:11 }; console.log("a:" + a + ";b:" + b);//a:3;b:5 bar(); console.log("a:" + a + ";b:" + b);//a:21;b:5 }; foo();
JavaScript 支持函數作用域,但要注意一點,就是在一個函數內部的任何位置定義的變量,都這個函數的任何地方都是可見的!
**注意:**JavaScript 不支持塊級作用域。所以最好的做法是在函數體的頂部,聲明函數中可能會用到的所有變量。
作用域的好處是:內部函數可以訪問定義它們外部函數的參數和變量(除了 this 和 arguments)。
注意:內部函數擁有比它的外部函數更長的生命周期。
var myObject = (function () { var value = 0;//只對 increment 與 getValue 可見 return { increment: function (inc) { value += typeof inc === 'number' ? inc : 1; }, getValue: function () { return value; } } }());
通過一個函數的形式來初始化 myObject 對象,它會返回一個對象字面量。函數定義了一個 value 變量,這個變量對 increment 和 getValue 方法總是可見的,但它的函數作用域卻使得這個變量對其他的代碼來說是不可見的!
/** * quo 構造函數 * @param status 私有屬性 * @returns {{get_status: Function}} */ var quo = function (status) { return { get_status: function () {//方法 return status; } }; }; var myQuo = quo("amazed"); console.log(myQuo.get_status());//amazed
當我們調用 quo 時,它會返回一個包含 get_status 方法的新對象,它的引用保存在 myQuo 中。所以即使 quo 函數已經返回了,但 get_status 方法仍然享有訪問 quo 對象的 status 屬性的特權。get_status 方法訪問的可是 status 屬性本身,這就是閉包哦O(∩_∩)O~
再看一個例子:
/** * 設置一個 DOM 節點為黃色,然后漸變為白色 * @param node */ var fade = function (node) { var level = 1; var step = function () { var hex = level.toString(16);//轉換為 16 位字符 node.style.backgroundColor = '#FFFF' + hex + hex; if (level < 15) { level += 1; setTimeout(step, 100); } }; setTimeout(step, 100); }; fade(document.body);
fade 函數在最后一行被調用后已經返回,但只要 fade 的內部函數又需要,它的變量就會持續保留。
注意:內部函數能夠訪問外部函數的實際變量:
var add_the_handlers_error = function (nodes) { var i; for (i = 0; i < nodes.length; i += 1) { nodes[i].onclick = function (e) { alert(i);//綁定的是變量 i 本身,而不是函數在構造時的變量 i 的值!!! } } };
add_the_handlers_error 函數的本意是:想傳遞給每個事件處理器一個唯一的 i 值,但因為事件處理器函數綁定了變量 i 本身,而不是它的值!
/** * 給數組中的節點設置事件處理程序(點擊節點,會彈出一個顯示節點序號的對話框) * @param nodes */ var add_the_handlers = function (nodes) { var helper = function (i) {//輔助函數,綁定了當前的 i 值 return function (e) { alert(i); }; }; var i; for (i = 0; i < nodes.length; i += 1) { nodes[i].onclick = helper(i); } };
我們在循環之外向構造一個輔助函數,讓這個函數返回一個綁定了當前 i 值的函數,這樣就可以解決問題啦O(∩_∩)O~
假設用戶觸發了一個請求,瀏覽器向服務器發送這個請求,然后最終顯示服務器的響應結果:
request = prepare_the_request(); response = send_request_synchronously(request); display(response);
這種方式的問題在于,網絡上的同步請求可能會導致客戶端進入假死狀態。
所以建議使用異步請求,并為服務端的響應創建一個回調函數。這個異步請求函數會立即返回,這樣我們的客戶端就不會被阻塞啦:
request = prepare_the_request(); send_request_asynchronously(request, function(response){ display(response); )};
一旦接收到服務端的響應,傳給 send_request_asynchronously 的匿名函數就會被調用啦O(∩_∩)O~
模塊是一個提供接口但卻隱藏狀態與實現的函數。可以使用函數和閉包來構建模塊。通過函數來生成模塊,就可以不用全局變量啦。
假設我們想給 String 增加一個 deentityify 方法。它會尋找字符串中的 HTML 字符,并把它們替換為對應的字符。這就需要在對象中保存字符實體的名字和它對應的字符。不能用全局變量,因為它是魔鬼!如果定義在函數內部,那么就會帶來運行時的損耗,因為每次執行函數時,這個字面量就會被求值一次。所以理想的方式是把它放入閉包:
String.method('deentityify', function () { //字符實體表:映射字符實體的名字到對應的字符 var entity = { quot: '"', lt: '<', gt: '>' }; //返回 deentityify 方法 return function () { /** * 返回以'&'開頭 和 以';'結尾的子字符串 */ return this.replace(/&([^$;]+);/g, function (a, b) { var r = entity[b];//b:映射字符實體名字 return typeof r === 'string' ? r : a;//a:原始字符串 }); }; }());
注意:最后一行是 (),所以我們是立即調用剛剛創建的函數!
模塊模式利用函數的作用域和閉包來創建被綁定對象和私有成員的關聯。這個例子中只有 deentityify 方法才有權訪問 entity (字符實體表)。
模塊模式是一個定義了私有變量和函數的函數。先利用閉包創建一個可以訪問私有變量和函數的特權函數,最后返回這個函數,或者把它保存到一個可以被訪問到的地方。
模塊模式做到了信息隱藏,是一種優秀的設計實踐,特別適合于封裝應用程序或者構造單例哦O(∩_∩)O~
模塊模式還可以產生安全對象。比如我們想構造一個能夠產生序列號的對象:
/** * 產生唯一字符串的對象(安全的對象) * 唯一字符串由 (前綴 + 序列號) 組成 * @returns {{set_prefix: Function, set_seq: Function, gensym: Function}} */ var serial_number = function () { var prefix = ''; var seq = 0; return { /** * 設置前綴 * @param p */ set_prefix: function (p) { prefix = String(p); }, /** * 設置序列號 * @param s */ set_seq: function (s) { seq = s; }, /** * 產生唯一的字符串 * @returns {string} */ gensym: function () { var result = prefix + seq; seq += 1; return result; } }; }; var seqer = serial_number(); seqer.set_prefix('Q'); seqer.set_seq(1000); var unique = seqer.gensym(); console.log(unique);//Q1000
sequer 包含的方法沒有用到 this 或 that,所以很安全。sequer 是一組函數的集合,只有那些特權函數才能夠獲取或修改私有屬性哦O(∩_∩)O~
如果把 sequer.gensym
作為值傳遞給第三方函數,那么那個函數可以使用它產生唯一的字符串,但卻不能通過這個函數改變 prefix 或 seq 的值。因為這個函數的功能只是“產生唯一的字符串”呀!
如果我們讓某些方法返回 this,那么就會啟動級聯。在級聯中,我們可以在單條語句中依次調用同一個對象的多個方法,最著名的例子就是 jQuery 哦O(∩_∩)O~
形如:
getElement('myDiv') .move(350,150) .width(100);
級聯可以產生出極富表現力的接口。
柯里化指的是:把函數與傳遞給它的參數結合,產生出新的函數:
Function.method('curry', function () { var slice = Array.prototype.slice, args = slice.apply(arguments),//創建一個真正的數組 that = this; return function () { return that.apply(null, args.concat(slice.apply(arguments))); }; }); var add1 = add.curry(1); console.log(add1(6));//7
因為 arguments 不是真正的數組,所以沒有 concat 方法,因此我們使用 slice 創建出了真正的數組。
有時候可以把先前計算過的結果記錄在某個對象中,避免重復計算。
假設我們想通過遞歸來計算 Fibonacci 數列。一個 Fibonacci 數字是之前的兩個 Fibonacci 數字之和,最前面的兩個數字是 0 和 1:
var fiboncci = function () { var memo = [0, 1];//存儲結果(已經計算過的值) var fib = function (n) { var result = memo[n]; if (typeof result !== 'number') { result = fib(n - 1) + fib(n - 2); memo[n] = result; } return result; }; return fib; }(); for (var i = 0; i <= 10; i += 1) { console.log(i + ": " + fiboncci(i)); }
我們把計算結果存儲在 memo 數組中,它被隱藏在閉包里。函數被調用時,它會先檢查計算結果是否已存在,如果存在就立即返回。
我們對這個例子進行擴展,構造一個帶記憶功能的函數:
/** * 帶記憶功能的函數 * @param memo 初始的 memo 數組 * @param formula 公式函數 * @returns {Function} */ var memoizer = function (memo, formula) { var recur = function (n) { var result = memo[n]; if (typeof result != 'number') { result = formula(recur, n); memo[n] = result; } return result; }; return recur; };
這個函數會返回一個可以管理 meno 存儲參數和在需要時調用 formula 函數的 recur 函數。recur 函數和它的參數會被傳遞給 formula 函數(就是公式)。是不是感覺有點繞,我們來看看實例就會清楚啦。
現在我們使用 memoizer 函數來重新定義 fibonacci 函數:
/** * 斐波那契 * @type {Function} */ var fibonacci = memoizer([0, 1], function (recur, n) { return recur(n - 1) + recur(n - 2); }); console.log(fibonacci(10));//55
這樣清楚了吧,使用這個函數可以極大地減少我們的工作量哦,比如下面的這個階乘函數:
/** * 階乘 * @type {Function} */ var factorial = memoizer([1, 1], function (recur, n) { return n * recur(n - 1); }); console.log(factorial(10));//3628800
上述就是小編為大家分享的JavaScript函數的特性有哪些了,如果剛好有類似的疑惑,不妨參照上述分析進行理解。如果想知道更多相關知識,歡迎關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。