如何編寫高品質JS程式碼_基礎知識
想寫出高效的javascript類別庫卻無從下手;
嘗試閱讀別人的類別庫,卻理解得似懂給懂;
打算好好鑽研js高階函數,但權威書上的內容太零散,
即使記住“用法”,但到要“用”的時候卻沒有想“法”。
也許你和我一樣,好像有一顧無形的力量約束著我們的計劃,讓我們一再認為知識面的局限性,致使我們原地踏步,難以向前跨越。
這段時間,各種作業、課程設計、實驗報告,壓力倍增。難得擠出一點點時間,絕不睡懶覺,整理總結往日所看的書,只為了可以離寫自己的類庫近一點。
本文參考自《javascript語言精粹》和《Effective JavaScript》。例子都被調試過,理解過後,我想把一些「深奧」的道理說得淺顯一點點。
1.變數作用域
作用域對程式設計師來說就像氧氣。它無所不在,甚至,你往往不會去想他。但當它被污染時(例如使用全局物件),你會感覺到窒息(例如應用回應變慢)。 javascript核心作用域規則很簡單,精心設計,而且很強大。有效地使用javascript需要掌握變數作用域的一些基本概念,並了解一些可能導致難以捉摸的、令人討厭的問題的極端情況。
1.1盡量少用全域變數
javascript很容易在全域命名空間中建立變數。創建全域變數毫不費力,因為它不需要任何形式的聲明,而且能被整個程式的所有程式碼自動存取。
對於我們這些初學者,遇到某些需求(例如,傳輸的資料被記錄下來、等待某時機某函數呼叫時使用;或者是某函數被經常使用)時,好不猶豫想到全局函數,甚至大一學到的C語言面向過程思想太根深蒂固,系統整齊地都是滿滿函數。定義全域變數會污染共享的公共命名空間,並可能導致意外的命名衝突。全域變數也不利於模組化,因為它會導致程式中獨立組件間的不必要耦合。嚴重地說,過多的全局(包括樣式表,直接定義div或a的樣式),整合到多人開發過稱將會成為災難性錯誤。這就是為什麼jQuery的所有程式碼都被包裹在一個立即執行的匿名表達式中——自呼叫匿名函數。當瀏覽器載入完jQuery檔案後,自呼叫匿名函數立即開始執行,初始化jQuery的各個模組,避免破壞和污染全域變數以至於影響到其他程式碼。
(function(window,undefined){
var jQuery = ...
//...
window.jQuery = window.$ = jQuery;
})(window);
另外,你或許會認為,「先怎麼怎麼寫,日後再整理」比較方便,但優秀的程式設計師會不斷地留意程式的結構、持續地歸類相關的功能以及分離不相關的組件,並這些行為作為程式設計過稱的一部分。
由於全域命名空間是javascript程式中獨立的元件經行互動的唯一途徑,因此,利用全域命名控制項的情況是不可避免的。元件或程式庫不得不定義一些全域變數。以便程式中的其他部分使用。否則最好使用局部變數。
this.foo ;//undefined
foo = " global foo";
this.foo ;//"global foo"
var foo = "global foo";
this.foo = "changed";
foo ;//changed
javascript的全域命名空間也被暴露在程式全域作用域中可以存取的全域對象,該物件作為this關鍵字的初始值。在web瀏覽器中,全域物件被綁定在全域window變數。這意味著你創建全域變數有兩種方法:在全域作用域內使用var聲明他,或將其加入到全域物件中。使用var聲明的好處是能清楚表達全域變數在程式範圍中的影響。
鑑於引用為綁定的全域變數會導致執行階段錯誤,因此,保存作用域清晰和簡潔會使程式碼的使用者更容易理解程式聲明了那些全域變數。
由於全域物件提供了全域環境的動態反應機制,所以可以使用它來查詢一個運行環境,檢測在這個平台下哪些特性可用。
eg.ES5引入了一個全域的JSON物件來讀寫JSON格式的資料。
if(!this.JSON){
this.JSON = {
parse : ..,
stringify : ...
}
}
如果你提供了JSON的實現,你當然可以簡單無條件地使用自己的實作。但是由宿主環境提供的內建實作幾乎更適合的,因為它們是用C語言寫入瀏覽器的。因為它們按照一定的標準對正確性和一致性進行了嚴格檢查,並且普遍來說比第三方實現提供更好的性能。
當初資料結構課程設計模擬串的基本操作,要求不能使用語言本身提供的方法。 javascript對數組的基本操作實現得很好,如果只是出於一般的學習需要,模擬語言本身提供的方法的想法很好,但是如果真正投入開發,無需考慮第一時間選擇使用javascript內置方法。
1.2避免使用with
with語句提供任何「便利」,讓你的應用變得不可靠和低效率。我們需要對單一物件依序呼叫一系列方法。使用with語句可以很方便地避免對物件的重複引用:
function status(info){
var widget = new Widget();
with(widget){
setBackground("blue");
setForeground("white");
setText("Status : " info);
show();
}
}
使用with語句從模組物件中」導入「(import)變數也是很誘人的。
function f(x,y){
with(Math){
return min(round(x),sqrt(y));//抽象引用
}
}
事實上,javascript對待所有的變數都是相同的。 javascript從最內層的作用域開始向外尋找變數。 with語言對待一個物件猶如該物件代表一個變數作用域,因此,在with程式碼區塊的內部,變數尋找從搜尋給定的變數名稱的屬性開始。如果在這個物件中沒有找到該屬性,則繼續在外部作用域中搜尋。 with區塊中的每個外部變數的參考都隱含地假設在with物件(以及它的任何原型物件)中沒有同名的屬性。而在程式的其他地方創建或修改with物件或其原型物件不一定會遵循這樣的假設。 javascript引擎當然不會讀取局部程式碼來取得你使用了那些局部變數。 javascript作用域可被表示為高效率的內部資料結構,變數查找會非常快速。但由於with程式碼區塊需要搜尋物件的原型鏈來尋找with程式碼裡的所有變量,因此,其運行速度遠低於一般的程式碼區塊。
替代with語言,簡單的做法,是將物件綁定在一個簡短的變數名稱上。
function status(info){
var w = new Widget();
w.setBackground("blue");
w.setForeground("white");
w.setText("Status : " info);
w.show();
}
其他情況下,最好的方法是將局部變數明確地綁定到相關的屬性上。
function f(x,y){
var min = Math.min,
round = Math.round,
sqrt = Math.sqrt;
return min(round(x),sqrt(y));
}
1.3熟練閉包
理解閉包有單一概念:
a)javascript允許你引用在目前函數以外定義的變數。
function makeSandwich(){
var magicIngredient = "peanut butter";
function make(filling){
return magicIngredient " and " filling;
}
return make("jelly");
}
makeSandwich();// "peanut butter and jelly"
b)即使外部函數已經返回,當前函數仍然可以引用在外部函數所定義的變數
function makeSandwich(){
var magicIngredient = "peanut butter";
function make(filling){
return magicIngredient " and " filling;
}
return make;
}
var f = sandwichMaker();
f("jelly"); // "peanut butter and jelly"
f("bananas"); // "peanut butter and bananas"
f("mallows"); // "peanut butter and mallows"
javascriptd的函數值包含了比呼叫它們時所執行所需的程式碼還要多的資訊。而且,javascript函數值還在內部儲存它們可能會引用的定義在其封閉作用域的變數。那些在其所涵蓋的作用域內追蹤變數的函數被稱為閉包。
make函數就是一個閉包,其程式碼引用了兩個外在變數:magicIngredient和filling。每當make函數被呼叫時,其程式碼都能引用這兩個變量,因為閉包儲存了這兩個變數。
函數可以引用在其作用域內的任何變量,包括參數和外部函數變數。我們可以利用這一點來編寫更通用的sandwichMaker函數。
function makeSandwich(magicIngredient){
function make(filling){
return magicIngredient " and " filling;
}
return make;
}
var f = sandwichMaker(”ham“);
f("cheese"); // "ham and cheese"
f("mustard"); // "ham and mustard"
閉包是javascript最優雅、最具表現力的特性之一,也是許多習慣用法的核心。
c)閉包可以更新外部變數的值。 事實上,閉包儲存的是外部變數的引用,而不是它們的值的副本。因此,對於任何具有存取這些外部變數的閉包,都可以進行更新。
function box(){
var val = undefined;
return {
set : function(newval) {val = newval;},
get : function (){return val;},
type : function(){return typeof val;}
};
}
var b = box();
b.type(); //undefined
b.set(98.6);
b.get();//98.6
b.type();//number
此範例產生一個包含三個閉包的物件。這三個閉包是set,type和get屬性,它們都共用存取val變量,set閉包更新val的值。隨後呼叫get和type查看更新的結果。
1.4理解變數宣告提升
javascript支援此法作用域(對變數foo的引用會被綁定到宣告foo變數最近的作用域中),但不支援區塊層級作用域(變數定義的作用域並不是離其最近的封閉語句或程式碼區塊)。
不明白這個特性將會導致一些微妙的bug:
function isWinner(player,others){
var highest = 0;
for(var i = 0,n = others.length ;i
if(player.score > highest){
highest = player.score;
}
}
return player.score > highest;
}
1.5 當心命名函數表達式笨拙的作用域
function double(x){ return x*2; }
var f = function(x){ return x*2; }
同一段函數程式碼也可以當作一個表達式,卻有截然不同的意義。匿名函數和命名函數表達式的官方區別在於後者會綁定到與其函數名稱相同的變數上,該變數作為該函數的局部變數。這可以用來寫遞歸函數表達式。
var f = function find(tree,key){
//....
return find(tree.left , key) ||
find(tree.right,key);
}
值得注意的是,變數find的作用域只在其自身函數中,不像函數聲明,命名函數表達式不能透過其內部的函數名稱在外部被引用。
find(myTree,"foo");//error : find is not defined;
var constructor = function(){ return null; }
var f= function(){
return constructor();
};
f();//{}(in ES3 environments)
程式看起來會產生null,但其實會產生一個新的物件。
因為命名函數變數作用域內繼承了Object.prototype.constructor(即Oject的建構子),就像with語句一樣,這個作用域會因Object.prototype的動態改變而受到影響。在系統中避免物件污染函數表達式作用域的辦法是避免任何時候在Object.prototype中加入屬性,以避免使用任何與標準Object.prototype屬性同名的局部變數。
在流行的javascript引擎中另一個缺點是將命名函數表達式的宣告提升。
var f = function g(){return 17;}
g(); //17 (in nonconformat environment)
有些javascript環境甚至把f和g這兩個函數當作不同的對象,從而導致不必要的記憶體分配。
1.6 當心局部塊函數宣告笨拙的作用域
function f() {return "global" ; }
function test(x){
function f(){return "local";}
var result = [];
if(x){
result.push(f());
}
result.push(f());
result result;
}
test(true); //["local","local"]
test(false); //["local"]
function f() {return "global" ; }
function test(x){
var result = [];
if(x){
function f(){return "local";}
result.push(f());
}
result.push(f());
result result;
}
test(true); //["local","local"]
test(false); //["local"]
javascript沒有區塊級作用域,所以內部函數f的作用域應該是整個test函數。一些javascript環境確實如此,但並不是所有javascript環境都這樣,javascript實現在嚴格模式下將這類函數報告為錯誤(具有局部塊函數聲明的處於嚴格模式下的程序將報告成一個語法錯誤),有助於檢測不可移植程式碼,為未來的標準版本在給局部區塊函數聲明給更明智和可以的語義。針對這種情況,可以考慮在test函數內宣告一局部變數指向全域函數f。

熱AI工具

Undresser.AI Undress
人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover
用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool
免費脫衣圖片

Clothoff.io
AI脫衣器

AI Hentai Generator
免費產生 AI 無盡。

熱門文章

熱工具

記事本++7.3.1
好用且免費的程式碼編輯器

SublimeText3漢化版
中文版,非常好用

禪工作室 13.0.1
強大的PHP整合開發環境

Dreamweaver CS6
視覺化網頁開發工具

SublimeText3 Mac版
神級程式碼編輯軟體(SublimeText3)

熱門話題

如何使用C#編寫布林過濾器演算法布隆過濾器(BloomFilter)是一種空間效率非常高的資料結構,可以用來判斷一個元素是否屬於集合。它的基本思想是透過多個獨立的雜湊函數將元素映射到一個位數組中,並將對應位數組的位元標記為1。當判斷一個元素是否屬於集合時,只需要判斷對應位數組的位是否都為1,如果有任何一位為0,則可以判定元素不在集合中。布隆過濾器具有快速查詢和

如何在C語言中編寫乘方函數乘方(exponentiation)是數學中常用的運算,表示將一個數自乘若干次的操作。在C語言中,我們可以透過寫一個乘方函數來實現這個函數。以下將詳細介紹如何在C語言中編寫乘方函數,並給出具體的程式碼範例。確定函數的輸入和輸出乘方函數的輸入通常包含兩個參數:底數(base)和指數(exponent),輸出為計算得到的結果。因此,我們

如何使用C#撰寫動態規劃演算法摘要:動態規劃是求解最最佳化問題的常用演算法,適用於多種場景。本文將介紹如何使用C#編寫動態規劃演算法,並提供具體的程式碼範例。一、什麼是動態規劃演算法動態規劃(DynamicProgramming,簡稱DP)是一種用來求解具有重疊子問題和最優子結構性質的問題的演算法想法。動態規劃將問題分解成若干個子問題來求解,透過記錄每個子問題的解,

飯店預訂系統是一種重要的資訊管理系統,它可以幫助飯店實現更有效率的管理和更良好的服務。如果你想學習如何使用C++來編寫一個簡單的飯店預訂系統,那麼這篇文章將為您提供一個基本的框架和詳細的實作步驟。飯店預訂系統的功能需求在開發飯店預訂系統之前,我們需要確定其實現的功能需求。一個基本的飯店預訂系統至少需要實現以下幾個功能:(1)客房資訊管理:包括客房類型、房間號碼、房

如何使用C++寫一個簡單的學生選課系統?隨著科技的不斷發展,電腦程式設計成為了一種必備的技能。而在學習程式設計的過程中,一個簡單的學生選課系統可以幫助我們更好地理解和應用程式語言。在本文中,我們將介紹如何使用C++來寫一個簡單的學生選課系統。首先,我們需要先明確這個選課系統的功能和需求。一個基本的學生選課系統通常包含以下幾個部分:學生資訊管理、課程資訊管理、選

如何透過C++寫一個簡單的掃雷遊戲?掃雷遊戲是一款經典的益智類遊戲,它要求玩家根據已知的雷區佈局,在沒有踩到地雷的情況下,揭示所有的方塊。在這篇文章中,我們將介紹如何使用C++來寫一個簡單的掃雷遊戲。首先,我們需要定義一個二維陣列來表示掃雷遊戲的地圖。數組中的每個元素可以是一個結構體,用於儲存方塊的狀態,例如是否揭示、是否有雷等資訊。另外,我們還需要定義

如何用Python寫KNN演算法? KNN(K-NearestNeighbors,K近鄰演算法)是一種簡單而常用的分類演算法。它的思想是透過測量不同樣本之間的距離,將測試樣本分類到最近的K個鄰居。本文將介紹如何使用Python編寫並實作KNN演算法,並提供具體的程式碼範例。首先,我們需要準備一些數據。假設我們有一組二維的資料集,每個樣本都有兩個特徵。我們將資料集分

如何使用C#編寫二分查找演算法二分查找演算法是一種高效率的查找演算法,它在有序數組中尋找特定元素的位置,時間複雜度為O(logN)。在C#中,我們可以透過以下幾個步驟來編寫二分查找演算法。步驟一:準備資料首先,我們需要準備一個已經排好序的陣列作為尋找的目標資料。假設我們要在陣列中尋找特定元素的位置。 int[]data={1,3,5,7,9,11,13
