javascript學習心得
javascript學習心得
javascript是目前web領域中實用最為廣泛的語言,不管是在前端還是在後端都能看到它的影子,可以說web從業者不論怎樣都繞不開它。在前端領域,各種框架層出不窮,最火的時候幾乎每個月都有新的框架誕生,如angularjs,vuejs等。在後端領域,nodejs可謂如火如荼,打破了人們對javascript只能作為前端語言的認知。按照此勢頭下去,javascript會越來越流行,會隨著web的發展越來越重要。現在基本沒有第二種語言可以挑戰js在web前端中的地位,至少10年以內不可能。
所以不論你是想學各種前端框架還是nodejs,都需要深入理解javascript的工作原理以及特性,只有這樣才能以不變應萬變。最近看了一些js的教學視訊與NC的《JS的高階程式設計》這本書,在這裡總結一下js的一些特性,以防自己忘記,也可以方便各位朋友學習與交流。
javascript的實現原理
javascript是Netscape(網景)公司推出的瀏覽器端語言,與java沒有半點關係,可能想接著java炒火吧。然後js越來越火,網景公司想將其標準化,這樣更加利於網路的發展,遂將其提交給了ECMA(歐洲計算機制造商協會)管理,負責對其的標準化。ECMA機構以JS為原型,推出了一個ECMAScript的腳步語言,規定各大瀏覽器廠商都必須依照ECMAScript標準實現各種的JS,保證JS具有良好的跨平臺性。所以可以將ECMAScript看成是標準化的JS,一個意思。
ECMAScript本質上是一種語言規範,其與平臺沒有關係,比如瀏覽器等。web瀏覽器只是ES的宿主環境之一,負責實現ES以及提供ES與環境互動的手段。宿主環境還有Node以及Flash等。
JS的實現要比ES規定的要複雜的多,ES只規定了基本語言特性。瀏覽器下的JS實現可以由下面三部分組成:
- 語言部分(ES)
- 文件物件模型(DOM)
- 瀏覽器物件模型(BOM)
DOM是負責操作由XML編寫的應用程式的API,負責將整個頁面對映成多層節點結構。如下所示:
DOM可以提供JS對節點結構的任何操作,增刪改查等。根據DOM提供的功能多樣性,將DOM分為DOM1,DOM2,DOM3這幾個級別。DOM1由DOM Core與DOM HTML組成。DOM2在DOM1的基礎上提供了更多的操作與功能。DOM3則更進一步的擴充套件了DOM。
BOM是瀏覽器物件模型,負責提供瀏覽器與JS的互動介面,提供JS操作瀏覽器的視窗與框架等。這個沒啥好說的!
總之要實現一個完整的瀏覽器端JS,這個三個部分缺一不可。
javascript與java,C/C++的區別和聯絡
javascript是一門動態語言,即在編寫好程式碼後不用編譯,由js直譯器解釋執行,同時變數不用顯式的寫出型別,統一用var型別表示,具體的變數型別由JS直譯器推測,與python和ruby一樣。說到js,大家經常聽到面向函數語言程式設計,這是js的一大設計特性。強大的function。其實在js中,函式本質上也是物件,也繼承自Object類,也有屬性等。js中也很多地方需要我們注意,它與java和C++很不一樣。
1. js中沒有類繼承關鍵字,和java與C++不一樣。js的類繼承需要自己動手實現,這也衍生出了多種類繼承的編寫正規化。
2. 同時js中沒有函式過載特性,這個需要特別注意。因為在js中函式只是普通物件,沒有函式簽名(函式名+引數)。而在java和C++中,用函式簽名唯一標示一個函式。不過在js中我們也可以有多種方式模擬出函式過載的效果。
3. js中的作用域與java也不一樣,js中有作用域鏈,在函式執行中,直譯器會根據執行函式的作用域鏈一層層的往上尋找變數,一直找到位於末端的window作用域中。
4. js中沒有塊級作用域。
for (var i = 0; i < 10; i++) {
}
alert(i);
上述程式碼是一個簡單的for迴圈,在java等語言中,因為有塊級作用域,所以i變數會在for迴圈執行完後消失。但是在js中,i變數會保持在執行環境中,因為沒有塊級作用域。所以alert出的結果是10。
5. js中有原型的概念,每個類都有對於的原型,包括函式等。類物件中有引用指向原型物件,所以同一類的原型物件被所有類物件共享。由此衍生出很多有意思的特性。
6. js中有閉包,這個閉包特性是由作用域鏈的設計衍生出來的,特別值得注意。根據閉包特性,結合匿名函式,我們可以模擬塊級作用域效果,甚至可以模擬出單例模式以及私有變數等。
7. js中的繼承與多型,需要程式設計師自己實現,與java和C++不一樣。利用js的原型鏈,可以寫出很多不同的繼承效果,各有特點。寫js中的繼承遠比java中有技術含量,哈哈!
8. js有垃圾回收機制,但是比較簡單,沒有jvm中的有意思。
作用域鏈
執行環境是js中一個重要的概念。執行環境定義了物件或函式可以訪問到的資料。每一個執行環境都有一個與之關聯的變數物件,環境定義的所以變數和函式都儲存在這個物件中。程式編寫者無法正常訪問該物件,但是後臺的解析器會訪問到它。
全域性執行環境是最外圍的一個執行環境,在web瀏覽器中是window物件。當某個執行環境執行完後,環境會被銷燬,與之相關聯的變數物件中的所有變數與函式也可能會被銷燬。為什麼用可能呢?有一個值得注意的地方,該變數物件銷不銷燬最本質的是看有沒有其他引用指向它,如果有別的引用指向該變數物件,那麼該變數物件不會被銷燬,比如在閉包中(函式中的函式情況)。
當代碼在一個環境中執行時,解析器會建立變數物件的一個作用域鏈。作用域鏈的最頂端始終是該執行環境的變數物件。對於函式而言,變數物件是其活動物件!當在執行環境中遇到一個變數時,解析器會從作用域鏈的最頂端變數物件中找相應的變數,沒有找到,則會順著作用域鏈一直找下去,最後找到全域性執行環境的變數物件!最後還是沒有,則會報錯!
看下來一個函式:
function compare(value1, value2) {
return value2 - value1;
}
上面的函式執行在全域性環境中,呼叫compare()時,會建立一個包含arguments、value1以及value2的物件,this特殊的變數不能在變數物件中找到。所以,全域性執行環境中的變數物件則處在compare函式的作用域鏈中的第二位。如下圖所示:
全域性變數物件始終存在,像compare函式這樣的區域性環境的變數物件,只有在函式執行時才存在。首先建立compare函式時,會建立一個預先包含全域性變數物件的作用域鏈,這個作用域鏈被儲存在Scope屬性中,Scope屬性儲存在compare函式物件中。當呼叫compare函式時,會建立一個執行物件,然後複製scope中的作用域鏈,將本地函式的活動物件放入作用域鏈的頂部。注意作用域鏈是多個引用的陣列,本身不保持物件,所以複製作用域鏈不在記憶體且非常快速。
原型鏈
原型鏈是JS中實現繼承的主要方式。每一個JS類中都有一個指向該類原型物件的引用。該原型物件有一個constructor屬性,指向建構函式,如圖所示:
由該類生成的物件中,也有個隱含的prototype屬性,指向該類的原型物件!設想一下,我們如何將另一個類的物件例項作為某個類的原型物件會怎麼樣呢?如下所示:
可以看到對於instance物件而言,prototype指向的是SubType類的原型,而SubType類的原型是SuperType的類例項,其中的prototype指向SuperType類的原型物件,這樣就形成了一個3層的原型鏈(包含Object原型)。當解析器在instance中尋找變數時,它會先在例項物件中尋找,然後沿著原型鏈一直向上尋找,直到找到為止!最終如果在Object類的原型中都沒有找到,那麼會產生錯誤!
如圖所示:
根據原型鏈,我們可以寫出JS中的繼承程式碼,一般推薦用混合方式實現,即建構函式與原型鏈混合方式:
function SuperType(name) {
this.name = name;
this.colors = ["red", "blue"];
}
SuperType.prototype.sayName = function() {
alert(this.name);
}
function SubType(name, age) {
this.age = age;
SuperType.call(this, name);
}
//繼承方法
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function() {
alert(this.Age);
}
SuperType A = new SuperType("hh");
SubType B = new SubType("MM", 26);
這樣就完成了繼承的編寫,混合方式的好處,可以避免只用原型鏈方式的一些缺點,比如不能向建構函式中傳遞引數,或者對於引用型別的值,不能做到物件獨有一份!但是上式方式依然有缺點,可以發現,每次建立SubType類時,會呼叫SuperType的建構函式,建立兩個變數,name與colors。但是我們會發現這兩個變數其實在SubType的原型中已經存在了,只是SubType的物件例項中的變數遮蔽了其原型物件中的兩個變數!這樣一來,造成空間浪費,同時也耗費了在形成SubType原型物件中呼叫SuperType的建構函式的時間。
那麼如何解決上述問題呢,我們可以用寄生混合式繼承方法。程式碼如下:
function inheritPrototype(subType, superType) {
var prototype = Object(superType.prototype);
prototype.constructor = subType;
subType.prototype = prototype;
}
function SuperType(name) {
this.name = name;
this.colors = ["red", "blue"];
}
SuperType.prototype.sayName = function() {
alert(this.name);
}
function SubType(name, age) {
this.age = age;
SuperType.call(this, name);
}
SubType.prototype = inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function() {
alert(this.Age);
}
SuperType A = new SuperType("hh");
SubType B = new SubType("MM", 26);
採用這種寄生混合式的繼承方法,使用寄生式繼承父類的prototype物件,將結果做子類的prototype,這樣就可以避免呼叫父類的建構函式,同時需要將prototype物件的constructor屬性指向子類建構函式即可。
這種方法是目前公認的最理想的繼承正規化,能正常使用instanceof和isPrototypeOf()。
思考:這種方式就沒有缺點嗎?如果後續我們在SuperType的原型物件中增加一個方法,但是SubType的原型是複製品,所以後續的SubType物件例項中不能得到該方法。但是如果採用原型式繼承+混合式繼承呢?能不能得到更好的效果呢?思考下面這段程式碼:
function SuperType(name) {
this.name = name;
this.colors = ["red", "blue"];
}
SuperType.prototype.sayName = function() {
alert(this.name);
}
function SubType(name, age) {
this.age = age;
SuperType.call(this, name);
}
//原型繼承SuperType的原型物件
SubType.prototype = Object.create(SuperType.prototype);
SubType.prototype.sayAge = function() {
alert(this.Age);
}
SuperType A = new SuperType("hh");
SubType B = new SubType("MM", 26);
//增加這段程式碼
SuperType.prototype.sayColors = function() {
alert(this.colors);
}
B.sayColors();
閉包
閉包是JS中的一個非常重要的概念。閉包是指有權訪問另一個函式作用域中的變數的函式。建立閉包常見的方式是在一個函式內部建立另一個函式,例如:
function createCompare(propertyName) {
return function(object1, object2) {
var value1 = object1[propertyName];
var value2 = object2[propertyName];
return value2 - value1;
}
}
var compare = createCompare("name");
var result = compare({name: "haha"}, {name: "hehe"});
該函式返回一個匿名函式,在該匿名函式中可以訪問外面函式的活動變數propertyName。該原理是:在匿名函式返回以後,匿名函式的作用域鏈被初始化為包含createCompare()函式的活動物件以及全域性變數物件。這樣,在匿名函式執行時就可以訪問createCompare函式的活動變量了。值得注意的是,createCompare函式在執行完畢後,它的變數物件並沒有被銷燬,因為有匿名函式的作用域鏈依然在引用這個活動物件。換句話說,createCompare函式執行完後,它的作用域鏈會被銷燬,但是它的活動變數卻保留在了記憶體中,直到匿名函式被銷燬後,它的活動物件才會被銷燬。用下圖表示:
匿名函式
java中有匿名物件,js中有匿名函式,其實本質都差不多。js中函式物件都有一個name屬性。對於name屬性,其實是指向函式宣告時跟在function後面的名字。但是在匿名函式中name為空字串。這裡需要正確理解函式宣告與函式表示式。
函式宣告:
functionName(a, b); //由於函式宣告提升,所以可以執行
function functionName(arg0, arg1) {
}
函式表示式:
a(); //函式表示式,,沒有函式宣告提升,所以不能執行,報錯!
var a = function(arg0, arg1) {
};
在遞迴情況下,可以用匿名函式很好的書寫,即使在嚴格模式下,依然可以使用:
var factorial = (function f(num) {
if (num <= 1)
return 1;
else
return num * f(num-1);
});
用命名函式表示式,可以將f()函式賦值給factorial變數,但是函式的名字依然是f,可以測試factorial.name依然是f。
有了匿名函式,可以模擬塊級作用域:
(function(){
//模擬的塊級作用域
})();
在括號內用函式宣告,表示這個是函式表示式,後面緊接括號,表示立刻呼叫這個匿名函式。
匿名函式配合閉包特性,可以實現單例模式:
var singleton = (function() {
//設定私有變數與私有函式
var privateVariable = 10;
function privateFunction(){
alert("hello world");
}
//建立物件,可以是任意型別的物件
var object = new Object();
//新增特權/公有屬性與方法
object.publicProperty = true;
object.publicMethod = function(){
privateVariable++;
return privateFunction();
};
return object;
})();