最近看了一下《nodejs開發指南》發現nodejs在某些特定的領域由他自己的長處,適合密集計算但是業務邏輯比較簡單的場景,如果做網站還是選擇php吧,呵呵,這本書我除了第5章《用nodejs開發web》沒有看,其他章節都大概看完了,了解了nodejs的簡單用法,感覺對我作用最大的還是附錄A《javascript的高級特性》,這里的內容讓我對js的高級特性有了深一步的認識,以下做個記錄:
一、作用域
和C、C++、Java 等常見語言不同,JavaScript 的作用域不是以花括號包圍的塊級作用域(block scope),這個特性經常被大多數人忽視,因而導致莫名其妙的錯誤。例如下面代碼,在大多數類C 的語言中會出現變量未定義的錯誤,而在JavaScript 中卻完全合法:
if(true) { var somevar = 'value'; } console.log(somevar); // 輸出value
這是因為JavaScript 的作用域完全是由函數來決定的,if、for 語句中的花括號不是獨立的作用域。
1、函數作用域
不同於大多數類C 的語言,由一對花括號封閉的代碼塊就是一個作用域,JavaScript 的作用域是通過函數來定義的,在一個函數中定義的變量只對這個函數內部可見,我們稱為函數作用域。在函數中引用一個變量時,JavaScript 會先搜索當前函數作用域,或者稱為“局部作用域”,如果沒有找到則搜索其上層作用域,一直到全局作用域。我們看一個簡單的例子:
var v1 = 'v1';
var f1 = function() { console.log(v1); // 輸出v1 }; f1();
var f2 = function() { var v1 = 'local'; console.log(v1); // 輸出local }; f2();
以上示例十分明了,JavaScript 的函數定義是可以嵌套的,每一層是一個作用域,變量搜索順序是從內到外。下面這個例子可能就有些令人困惑:
var scope = 'global'; var f = function() { console.log(scope); // 輸出undefined var scope = 'f'; } f();
這是JavaScript 的一個特性,按照作用域搜索順序,在console.log 函數訪問 scope 變量時,JavaScript 會先搜索函數f 的作用域,恰巧在f 作用域里面搜索到scope 變量,所以上層作用域中定義的scope 就被屏蔽了,但執行到 console.log 語句時,scope 還沒被定義,或者說初始化,所以得到的就是 undefined 值了。
函數作用域的嵌套關系是定義時決定的,而不是調用時決定的,也就是說,JavaScript 的作用域是靜態作用域,又叫詞法作用域,這是因為作用域的嵌套關系可以在語法分析時確定,而不必等到運行時確定。下面的例子說明了這一切:
var scope = 'top'; var f1 = function() { console.log(scope); };
f1(); // 輸出top
var f2 = function() { var scope = 'f2'; f1(); }; f2(); // 輸出to
這個例子中,通過f2 調用的f1 在查找 scope 定義時,找到的是父作用域中定義的scope 變量,而不是 f2 中定義的 scope 變量。這說明了作用域的嵌套關系不是在調用
時確定的,而是在定義時確定的。
2、全局作用域
在JavaScript 中有一種特殊的對象稱為 全局對象。這個對象在Node.js 對應的是 global對象,在瀏覽器中對應的是 window 對象。由於全局對象的所有屬性在任何地方都是可見的,所以這個對象又稱為 全局作用域。全局作用域中的變量不論在什么函數中都可以被直接引用,而不必通過全局對象。
滿足以下條件的變量屬於全局作用域:
在最外層定義的變量;
全局對象的屬性;
任何地方隱式定義的變量(未定義直接賦值的變量)。
需要格外注意的是第三點,在任何地方隱式定義的變量都會定義在全局作用域中,即不通過var 聲明直接賦值的變量。這一點經常被人遺忘,而模塊化編程的一個重要原則就是避免使用全局變量,所以我們在任何地方都不應該隱式定義變量。
二、閉包
1、啥叫閉包
閉包的嚴格定義是“由函數(環境)及其封閉的自由變量組成的集合體。”,通俗地講,JavaScript 中每個的函數都是一個閉包
var generateClosure = function() { var count = 0; var get = function() { count ++; return count; }; return get; }; var counter = generateClosure(); console.log(counter()); // 輸出1 console.log(counter()); // 輸出2 console.log(counter()); // 輸出3
當一個函數返回它內部定義的一個函數時,就產生了一個閉包,閉包不但包括被返回的函數,還包括這個函數的定義環境。上面例子中,當函數generateClosure() 的內部函數get 被一個外部變量counter 引用時,counter 和generateClosure() 的局部變量就是一個閉包
var generateClosure = function() { var count = 0; var get = function() { count ++; return count; }; return get; }; var counter1 = generateClosure(); var counter2 = generateClosure(); console.log(counter1()); // 輸出1 console.log(counter2()); // 輸出1 console.log(counter1()); // 輸出2 console.log(counter1()); // 輸出3 console.log(counter2()); // 輸出2
counter1 和 counter2 分別調用了 generate-Closure() 函數,生成了兩個閉包的實例,它們內部引用的 count 變量分別屬於各自的運行環境。我們可以理解為,在generateClosure() 返回get 函數時,私下將 get 可能引用到的 generateClosure() 函數的內部變量(也就是 count 變量)也返回了,並在內存中生成了一個副本,之后generateClosure() 返回的函數的兩個實例counter1和counter2 就是相互獨立的了。
2、閉包的用途
1)、嵌套的回調函數
如下代碼是在Node.js 中使用MongoDB 實現一個簡單的增加用戶的功能:
exports.add_user = function(user_info, callback) { varuid = parseInt(user_info['uid']); mongodb.open(function(err, db) { if(err) {callback(err); return;} db.collection('users', function(err, collection) { if(err) {callback(err); return;} collection.ensureIndex("uid", function(err) { if(err) {callback(err); return;} collection.ensureIndex("username", function(err) { if(err) {callback(err); return;} collection.findOne({uid: uid}, function(err) { if(err) {callback(err); return;} if(doc) { callback('occupied'); } else{ varuser = { uid: uid, user: user_info, }; collection.insert(user, function(err) { callback(err); }); } }); }); }); }); }); };
這段代碼中用到了閉包的層層嵌套,每一層的嵌套都是一個回調函數。回調函數不會立即執行,而是等待相應請求處理完后由請求的函數回調。我們可以看到,在嵌套的每一層中都有對 callback 的引用,而且最里層還用到了外層定義的 uid 變量。由於閉包機制的存在,即使外層函數已經執行完畢,其作用域內申請的變量也不會釋放,因為里層的函數還有可能引用到這些變量,這樣就完美地實現了嵌套的異步回調。
2)、實現私有成員
JavaScript 的對象沒有私有屬性,也就是說對象的每一個屬性都是曝露給外部的。這樣可能會有安全隱患,譬如對象的使用者直接修改了某個屬性,導致對象內部數據的一致性受到破壞等。JavaScript通過約定在所有私有屬性前加上下划線(例如_myPrivateProp),表示這個屬性是私有的,外部對象不應該直接讀寫它。但這只是個非正式的約定,假設對象的使用者不這么做,有沒有更嚴格的機制呢?答案是有的,通過閉包可以實現。
var generateClosure = function() { var count = 0; var get = function() { count ++; return count; }; return get; }; var counter = generateClosure(); console.log(counter()); // 輸出1 console.log(counter()); // 輸出2 console.log(counter()); // 輸出3
只有調用counter() 才能訪問到閉包內的 count 變量,並按照規則對其增加1,除此之外決無可能用其他方式找到count 變量。受到這個簡單例子的啟發,我們可以把一個對象用閉包封裝起來,只返回一個“訪問器”的對象,即可實現對細節隱藏
三、對象
1、對象的創建和訪問
JavaScript 中的對象實際上就是一個由屬性組成的關聯數組,屬性由名稱和值組成,值的類型可以是任何數據類型,或者函數和其他對象
在JavaScript 中,你可以用以下方法創建一個簡單的對象:
var foo = {}; //也可以用這種 var foo = new Object() 來顯式地創建一個對象。
foo.prop_1 = 'bar'; foo.prop_2 = false; foo.prop_3 = function() { return'hello world'; } console.log(foo.prop_3());
我們還可以用關聯數組的模式來創建對象,以上代碼修改為:
var foo = {}; foo['prop1'] = 'bar'; foo['prop2'] = false; foo['prop3'] = function() { return'hello world'; }
2、call 和 apply
call 和 apply 的功能是以不同的對象作為上下文來調用某個函數。簡而言之,就是允許一個對象去調用另一個對象的成員函數
call 和 apply 的功能是一致的,兩者細微的差別在於 call 以參數表來接受被調用函數的參數,而 apply 以數組來接受被調用函數的參數。call 和 apply 的語法分別是:
func.call(thisArg[, arg1[, arg2[, ...]]])
func.apply(thisArg[, argsArray])
var someuser = { name: 'byvoid', display: function(words) { console.log(this.name + ' says ' + words); } };
var foo = { name: 'foobar' }; someuser.display.call(foo, 'hello'); // 輸出foobar says hello
3、原型
functionPerson() { }
Person.prototype.name = 'BYVoid'; Person.prototype.showName = function() { console.log(this.name); };
varperson = newPerson(); person.showName();
上面這段代碼使用了原型而不是構造函數初始化對象。這樣做與直接在構造函數內定義屬性有什么不同呢?
構造函數內定義的屬性繼承方式與原型不同,子對象需要顯式調用父對象才能繼承構造函數內定義的屬性。
構造函數內定義的任何屬性,包括函數在內都會被重復創建,同一個構造函數產生的兩個對象不共享實例。
構造函數內定義的函數有運行時閉包的開銷,因為構造函數內的局部變量對其中定義的函數來說也是可見的。
function Foo() { var innerVar = 'hello'; this.prop1 = 'BYVoid'; this.func1 = function(){ innerVar = ''; }; }
Foo.prototype.prop2 = 'Carbo'; Foo.prototype.func2 = function() { console.log(this.prop2); };
var foo1 = newFoo(); var foo2 = newFoo(); console.log(foo1.func1 == foo2.func1); // 輸出false console.log(foo1.func2 == foo2.func2); // 輸出true
那么我們什么時候使用原型,什么時候使用構造函數內定義來創建屬性呢?
除非必須用構造函數閉包,否則盡量用原型定義成員函數,因為這樣可以減少開銷。
盡量在構造函數內定義一般成員,尤其是對象或數組,因為用原型定義的成員是多個實例共享的。
4、原型鏈
JavaScript 中有兩個特殊的對象:Object 與Function,它們都是構造函數,用於生成對象。Object.prototype 是所有對象的祖先,Function.prototype 是所有函數的原
型,包括構造函數,我把JavaScript 中的對象分為三類,一類是用戶創建的對象,一類是構造函數對象,一類是原型對象。
用戶創建的對象,即一般意義上用new 語句顯式構造的對象。構造函數對象指的是普通的構造函數,即通過 new 調用生成普通對象的函數。原型對象特指構造函數prototype 屬性指向的對象。這三類對象中每一類都有一個__proto__ 屬性,它指向該對象的原型,從任何對象沿着它開始遍歷都可以追溯到 Object.prototype。構造函數對象有prototype 屬性,指向一個原型對象,通過該構造函數創建對象時,被創建對象的 __proto__ 屬性將會指向構造函數的 prototype 屬性。原型對象有 constructor屬性,指向它對應的構造函數。
functionFoo() { } Object.prototype.name = 'My Object'; Foo.prototype.name = 'Bar';
var obj = new Object(); var foo = newFoo();
console.log(obj.name); // 輸出My Object console.log(foo.name); // 輸出Bar console.log(foo.__proto__.name); // 輸出Bar console.log(foo.__proto__.__proto__.name); // 輸出My Object console.log(foo. __proto__.constructor.prototype.name); // 輸出Bar
在JavaScript 中,繼承是依靠一套叫做原型鏈(prototype chain)的機制實現的。屬性繼承的本質就是一個對象可以訪問到它的原型鏈上任何一個原型對象的屬性。例如上例的foo 對象,它擁有foo. __proto__ 和 foo. __proto__.__proto__ 所有屬性的淺拷貝(只復制基本數據類型,不復制對象)。所以可以直接訪問foo.constructor(來自foo.
__proto__,即Foo.prototype),foo.toString(來自foo. __proto__.__proto__,即Object.prototype)。
5、對象的復制
JavaScript 和Java 一樣都沒有像C語言中一樣的指針,所有對象類型的變量都是指向對象的引用,兩個變量之間賦值傳遞一個對象並不會對這個對象進行復制,而只是傳遞引用,有些時候我們需要完整地復制一個對象,這該如何做呢?Java 語言中有 clone 方法可以實現對象復制,但JavaScript 中沒有這樣的函數。因此我們需要手動實現這樣一個函數,一個簡單的做法是復制對象的所有屬性:
//自己寫的clone方法
Object.prototype.clone = function() { var newObj = {}; for(var i in this) { newObj[i] = this[i]; } return newObj; }
//定義一個obj對象 var obj = { name: 'byvoid', likes: ['node'] };
//clone一個對象 var newObj = obj.clone(); obj.likes.push('python');
console.log(obj.likes); // 輸出[ 'node', 'python' ] console.log(newObj.likes); // 輸出[ 'node', 'python' ]
上面的代碼是一個對象淺拷貝(shallow copy)的實現,即只復制基本類型的屬性,而共享對象類型的屬性。淺拷貝的問題是兩個對象共享對象類型的屬性,淺拷貝的問題是兩個對象共享對象類型的屬性,例如上例中 likes 屬性指向的是同一個數組。
實現一個完全的復制,或深拷貝(deep copy)並不是一件容易的事,因為除了基本數據類型,還有多種不同的對象,對象內部還有復雜的結構,因此需要用遞歸的方式來實現:
Object.prototype.clone = function() { var newObj = {}; for(var i in this) { if(typeof(this[i]) == 'object' || typeof(this[i]) == 'function') { newObj[i] = this[i].clone(); } else{ newObj[i] = this[i]; } } return newObj; };
Array.prototype.clone = function() { var newArray = []; for(var i = 0; i < this.length; i++) { if(typeof(this[i]) == 'object' || typeof(this[i]) == 'function') { newArray[i] = this[i].clone(); } else{ newArray[i] = this[i]; } } return newArray; };
Function.prototype.clone = function() { var that = this; var newFunc = function() { returnthat.apply(this, arguments); }; for(var i in this) { newFunc[i] = this[i]; } returnnewFunc; };
var obj = { name: 'byvoid', likes: ['node'], display: function() { console.log(this.name); }, };
var newObj = obj.clone(); newObj.likes.push('python'); console.log(obj.likes); // 輸出[ 'node' ] console.log(newObj.likes); // 輸出[ 'node', 'python' ] console.log(newObj.display == obj.display); // 輸出false