天天看點

javascript 作用域 閉包 對象 原理和示例分析(上)

                                                                                             閱讀、了解、思考、實踐,再實踐、再思考....  深圳小地瓜獻上

javascript進階特性包含:作用域、閉包、對象

-----------------------------------------------作用域----------------------------------------------------------------------------------------    

   作用域(scope)是結構化程式設計語言中的重要概念,它決定了變量的可見範圍和生命周

期,正确使用作用域可以使代碼更清晰、易懂。作用域可以減少命名沖突,而且是垃圾回收

的基本單元。和 c、c++、java 等常見語言不同,javascript 的作用域不是以花括号包圍的塊

級作用域(block scope) ,這個特性經常被大多數人忽視,因而導緻莫名其妙的錯誤。例如

下面代碼, 在大多數類 c 的語言中會出現變量未定義的錯誤, 而在 javascript 中卻完全合法:

if (true) {

var somevar = 'value';

}

console.log(somevar); // 輸出 value

這是因為 javascript 的作用域完全是由函數來決定的, if 、 for  語句中的花括号不是獨

立的作用域。

      作用域(scope)包含函數作用域和全局作用域

      函數作用域:不同于大多數類 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();

上面代碼可能和你預想的不一樣, 沒有輸出  global , 而是 undefined , 這是為什麼呢?

這是 javascript 的一個特性,按照作用域搜尋順序,在 console.log  函數通路  scope 變

量時,javascript 會先搜尋函數 f  的作用域,恰巧在 f 作用域裡面搜尋到 scope 變量,

是以上層作用域中定義的 scope  就被屏蔽了,但執行到  console.log 語句時, scope 還

沒被定義,或者說初始化,是以得到的就是  undefined 值了。

我們還可以從另一個角度來了解:對于開發者來說,在通路未定義的變量或定義了但沒

有初始化的變量時,獲得的值都是  undefined 。于是我們可以認為,無論在函數内什麼地

方定義的變量,在一進入函數時就被定義了,但直到  var 所在的那一行它才被初始化,所

以在這之前引用到的都是  undefined  值。 (事實上,javascript 的内部實作并不是這樣,未

定義變量和值為 undefined 的變量還是有差別的。 )

函數作用域的嵌套

接下來看一個稍微複雜的例子:

var scope = 'f0';

(function() {

       var scope = 'f1';

       (function() {

       console.log(scope); // 輸出 f1

       })();

})();

上面是一個函數作用域嵌套的例子,我們在最内層函數引用了  scope 變量,通過作用

域搜尋,找到了其父作用域中定義的  scope 變量。

有一點需要注意:函數作用域的嵌套關系是定義時決定的,而不是調用時決定的,也就

是說,javascript 的作用域是靜态作用域,又叫詞法作用域,這是因為作用域的嵌套關系可

以在文法分析時确定,而不必等到運作時确定。下面的例子說明了這一切:

var scope = 'top';

 var f1 = function() {

 console.log(scope);

 };

f1(); // 輸出 top

 var f2 = function() {

   var scope = 'f2';

   f1();

f2(); // 輸出 top

這個例子中,通過 f2  調用的 f1  在查找  scope  定義時,找到的是父作用域中定義

的 scope 變量,而不是  f2  中定義的  scope 變量。這說明了作用域的嵌套關系不是在調用

時确定的,而是在定義時确定的。

全局作用域:在 javascript 中有一種特殊的對象稱為 全局對象。這個對象在node.js 對應的是  global

對象, 在浏覽器中對應的是  window 對象。 由于全局對象的所有屬性在任何地方都是可見的,

是以這個對象又稱為 全局作用域。全局作用域中的變量不論在什麼函數中都可以被直接引

用,而不必通過全局對象。

滿足以下條件的變量屬于全局作用域:

在最外層定義的變量;

全局對象的屬性;

任何地方隐式定義的變量(未定義直接指派的變量) 。

需要格外注意的是第三點,在任何地方隐式定義的變量都會定義在全局作用域中,即不

通過 var  聲明直接指派的變量。這一點經常被人遺忘,而子產品化程式設計的一個重要原則就是

避免使用全局變量,是以我們在任何地方都不應該隐式定義變量。

-----------------------------------------------------閉包--------------------------------------------------------------------------

閉包(closure)是函數式程式設計中的概念,出現于 20 世紀 60 年代,最早實作閉包的語言

是 scheme,它是 lisp 的一種方言。之後閉包特性被其他語言廣泛吸納。

閉包的嚴格定義是“由函數(環境)及其封閉的自由變量組成的集合體。 ”這個定義對

于大家來說有些晦澀難懂,是以讓我們先通過例子和不那麼嚴格的解釋來說明什麼是閉包,

然後再舉例說明一些閉包的經典用途。

什麼是閉包?

 通俗地講,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()  函數中有一個局部變量 count ,初值為 0。還有一

個叫做 get  的函數, get  将其父作用域,也就是  generateclosure() 函數中的  count 變

量增加 1,并傳回  count 的值。 generateclosure()  的傳回值是  get 函數。在外部我們

通過  counter 變量調用了  generateclosure()  函數并擷取了它的傳回值,也就是  get  函

數,接下來反複調用幾次 counter() ,我們發現每次傳回的值都遞增了 1。

讓我們看看上面的例子有什麼特點,按照通常指令式程式設計思維的了解, count  是

generateclosure  函數内部的變量,它的生命周期就是 generateclosure  被調用的時

期,當  generateclosure 從調用棧中傳回時, count  變量申請的空間也就被釋放。問題

是,在  generateclosure() 調用結束後, counter() 卻引用了“已經釋放了的” count

變量,而且非但沒有出錯,反而每次調用  counter()  時還修改并傳回了 count 。這是怎

麼回事呢?

這正是所謂閉包的特性。 當一個函數傳回它内部定義的一個函數時, 就産生了一個閉包,

閉包不但包括被傳回的函數,還包括這個函數的定義環境。上面例子中,當函數

generateclosure()  的内部函數 get 被一個外部變量 counter 引用時, counter  和

generateclosure() 的局部變量就是一個閉包。如果還不夠清晰,下面這個例子可以幫助

你了解:

count ++;

return count;

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  就是互相獨立的了。

閉包的用途:

   1. 嵌套的回調函數

閉包有兩個主要用途,一是實作嵌套的回調函數,二是隐藏對象的細節。讓我們先看下

面這段代碼示例,了解嵌套的回調函數。如下代碼是在 node.js 中使用 mongodb 實作一個

簡單的增加使用者的功能:

 exports.add_user = function(user_info, callback) {

var uid = parseint(user_info['uid']);

mongodb.open(function(err, db) {

if (err) {

callback(err);

return

db.collection('users',

function(err, collection) {

collection.ensureindex("uid",

function(err) {

collection.ensureindex("username",

collection.findone({

uid: uid

},

if (doc) {

callback('occupied')

} else {

var user = {

uid: uid,

user: user_info,

collection.insert(user,

callback(err)

})

如果你對 node.js 或 mongodb 不熟悉,沒關系,不需要去了解細節,隻要看清楚大概

的邏輯即可。這段代碼中用到了閉包的層層嵌套,每一層的嵌套都是一個回調函數。回調函

數不會立即執行,而是等待相應請求處理完後由請求的函數回調。我們可以看到,在嵌套的

每一層中都有對  callback 的引用,而且最裡層還用到了外層定義的  uid 變量。由于閉包

機制的存在,即使外層函數已經執行完畢,其作用域内申請的變量也不會釋放,因為裡層的

函數還有可能引用到這些變量,這樣就完美地實作了嵌套的異步回調。

2. 實作私有成員

   我們知道,javascript 的對象沒有私有屬性,也就是說對象的每一個屬性都是曝露給外部

的。這樣可能會有安全隐患,譬如對象的使用者直接修改了某個屬性,導緻對象内部資料的一

緻性受到破壞等。javascript通過約定在所有私有屬性前加上下劃線(例如 _myprivateprop ) ,

表示這個屬性是私有的,外部對象不應該直接讀寫它。但這隻是個非正式的約定,假設對象

的使用者不這麼做,有沒有更嚴格的機制呢?答案是有的,通過閉包可以實作。讓我們再看

看前面那個例子:

 var generateclosure = function() {

count++;

我們可以看到,隻有調用 counter()  才能通路到閉包内的  count  變量,并按照規則

對其增加1,除此之外決無可能用其他方式找到 count  變量。受到這個簡單例子的啟發,

我們可以把一個對象用閉包封裝起來, 隻傳回一個 “通路器” 的對象, 即可實作對細節隐藏。

關于實作javascript對象私有成員的更多資訊,請參考http://javascript.crockford.com/private.html。

-------------------------------------------------------------------對象-------------------------------------------------------------

提起面向對象的程式設計語言,立刻讓人想起的是 c++、java 等這類靜态強類型語言,

以及 python、ruby 等腳本語言,它們共有的特點是基于類的面向對象。而說到 javascript,

很少能讓人想到它面向對象的特性,甚至有人說它不是面向對象的語言,因為它沒有類。沒

錯,javascript 真的沒有類,但 javascript 是面向對象的語言。javascript 隻有對象,對象就

是對象,不是類的執行個體。

因為絕大多數面向對象語言中的對象都是基于類的, 是以經常有人混淆類的執行個體與對象

的概念。對象就是類的執行個體,這在大多數語言中都沒錯,但在 javascript 中卻不适用。

javascript 中的對象是基于原型的,是以很多人在初學 javascript 對象時感到無比困惑。通過

這一節,我們将重新認識 javascript 中對象,充分了解基于原型的面向對象的實質。

建立和通路

  javascript 中的對象實際上就是一個由屬性組成的關聯數組,屬性由名稱和值組成,值

的類型可以是任何資料類型,或者函數和其他對象。注意 javascript 具有函數式程式設計的特性,

是以函數也是一種變量,大多數時候不用與一般的資料類型區分。

在 javascript 中,你可以用以下方法建立一個簡單的對象:

var foo = {};

foo.prop_1 = 'bar';

foo.prop_2 = false;

foo.prop_3 = function() {

return 'hello world';

console.log(foo.prop_3());

以上代碼中,我們通過  var foo = {};  建立了一個對象,并将其引用指派給 foo ,

通過  foo.prop1 來擷取它的成員并指派, 其中 {}  是對象字面量的表示方法, 也可以用  var

foo = new object()  來顯式地建立一個對象。

1. 使用關聯數組通路對象成員

我們還可以用關聯數組的模式來建立對象,以上代碼修改為:

foo['prop1'] = 'bar';

foo['prop2'] = false;

foo['prop3'] = function() {

在 javascript 中,使用句點運算符和關聯數組引用是等價的,也就是說任何對象(包括

this 指針)都可以使用這兩種模式。使用關聯數組的好處是,在我們不知道對象的屬性名

稱的時候,可以用變量來作為關聯數組的索引。例如:

var some_prop = 'prop2';

foo[some_prop] = false;

2. 使用對象初始化器建立對象

上述的方法隻是讓你對javascript對象的定義有個了解, 真正在使用的時候, 我們會采用

下面這種更加緊湊明了的方法:

var foo = {

'prop1': 'bar',

prop2: 'false',

prop3: function (){

這種定義的方法稱為對象的初始化器。注意,使用初始化器時,對象屬性名稱是否加引

号是可選的, 除非屬性名稱中有空格或者其他可能造成歧義的字元, 否則沒有必要使用引号。

構造函數

前一小節講述的對象建立方法都有一個弱點,就是建立對象的代碼是一次性的。如果我

們想建立多個規劃好的對象,有若幹個固定的屬性、方法,并能夠初始化,就像 c++ 語言

中的對象一樣,該如何做呢?别擔心,javascript 提供了構造函數,讓我們來看看應該如何

建立複雜的對象。

 function user(name, uri) {

this.name = name;

this.uri = uri;

this.display = function() {

console.log(this.name);

以上是一個簡單的構造函數,接下來用  new  語句來建立對象:

var someuser = new user('byvoid', 'http://www.byvoid.com');

然後就可以通過  someuser  來通路這個對象的屬性和方法了。

上下文對象

在 javascript 中,上下文對象就是  this 指針,即被調用函數所處的環境。上下文對象

的作用是在一個函數内部引用調用它的對象本身,javascript 的任何函數都是被某個對象調

用的,包括全局對象,是以  this 指針是一個非常重要的東西。

在前面使用構造函數的代碼中我們已經看到了  this 的使用方法,下面代碼可以更佳清

楚地說明上下文對象的使用方式: 

var someuser = {

name: 'byvoid',

display: function() {

someuser.display(); // 輸出 byvoid

bar: someuser.display,

name: 'foobar'

foo.bar(); // 輸出 foobar

javascript 的函數式程式設計特性使得函數可以像一般的變量一樣指派、傳遞和計算,我們

看到在上面代碼中, foo  對象的  bar  屬性是  someuser.display 函數,使用  foo.bar()

調用時, bar 和  foo 對象的函數看起來沒有差別,其中的 this  指針不屬于某個函數,而

是函數調用時所屬的對象。

在 javascript 中,本質上,函數類型的變量是指向這個函數實體的一個引用,在引用之

間指派不會對對象産生複制行為。我們可以通過函數的任何一個引用調用這個函數,不同之

處僅僅在于上下文。下面例子可以幫助我們了解:

 var someuser = {

func: function() {

someuser.func(); // 輸出 byvoid

foo.func = someuser.func;

foo.func(); // 輸出 foobar

name = 'global';

func = someuser.func;

func(); // 輸出 global

為防止func 歧義,再複制一份,将得到同樣的結果。

var func1 = someuser.func;

func1(); // 輸出 global

仔細觀察上面的例子,使用不同的引用來調用同一個函數時, this 指針永遠是這個引

用所屬的對象。在前面的章節中我們提到了 javascript 的函數作用域是靜态的,也就是說一

個函數的可見範圍是在預編譯的文法分析中就可以确定的, 而上下文對象則可以看作是靜态

作用域的補充。

1.  call 和  apply

在 javascript 中, call 和  apply 是兩個神奇的方法,但同時也是容易令人迷惑的兩個

方法,乃至許多對 javascript 有經驗的人也不太清楚它們的用法。 call 和  apply 的功能是

以不同的對象作為上下文來調用某個函數。簡而言之,就是允許一個對象去調用另一個對象

的成員函數。乍一看似乎很不可思議,而且容易引起混亂,但其實 javascript 并沒有嚴格的

所謂“成員函數”的概念,函數與對象的所屬關系在調用時才展現出來。靈活使用  call 和

apply 可以節省不少時間,在後面我們可以看到, call 可以用于實作對象的繼承。

call 和  apply 的功能是一緻的,兩者細微的差别在于  call 以參數表來接受被調用函

數的參數,而  apply 以數組來接受被調用函數的參數。 

call 和  apply 的文法分别是:

func.call(thisarg[, arg1[, arg2[, ...]]])

func.apply(thisarg[, argsarray])

其中, func 是函數的引用, thisarg 是  func 被調用時的上下文對象, arg1 、 arg2 或

argsarray 是傳入  func 的參數。我們以下面一段代碼為例介紹  call 的工作機制:

display: function(words) {

console.log(this.name + ' says ' + words);

someuser.display.call(foo, 'hello'); // 輸出 foobar says hello

用 node.js 運作這段代碼, 我們可以看到控制台輸出了  foobar 。 someuser.display  是

被調用的函數,它通過 call 将上下文改變為 foo  對象,是以在函數體内通路 this.name

時,實際上通路的是 foo.name, 因而輸出了 foobar 。

2.  bind

如何改變被調用函數的上下文呢?前面說過,可以用  call 或  apply 方法,但如果重複

使用會不友善,因為每次都要把上下文對象作為參數傳遞,而且還會使代碼變得不直覺。針

對這種情況,我們可以使用  bind 方法來永久地綁定函數的上下文,使其無論被誰調用,上

下文都是固定的。 bind  文法如下:

func.bind(thisarg[, arg1[, arg2[, ...]]])

其中  func  是待綁定函數, thisarg  是改變的上下文對象, arg1 、 arg2 是綁定的參

數表。 bind 方法傳回值是上下文為 thisarg  的  func 。通過下面例子可以幫你了解  bind

的使用方法: 

foo.func1 = someuser.func.bind(someuser);

foo.func1(); // 輸出 byvoid

func = someuser.func.bind(foo);

func(); // 輸出 foobar

func2 = func;

func2(); // 輸出 foobar

上面代碼直接将  foo.func  指派為  someuser.func ,調用  foo.func() 時, this 指

針為 foo ,是以輸出結果是 foobar 。 foo.func1 使用了 bind  方法,将 someuser  作

為 this 指針綁定到 someuser.func ,調用  foo.func1()  時, this 指針為 someuser ,

是以輸出結果是 byvoid 。(未定義直接指派的變量)全局函數 func 同樣使用了 bind  方法,将 foo 作為 this  指

針綁定到  someuser.func ,調用  func()  時, this 指針為  foo ,是以輸出結果是 foobar 。

而  func2  直接将綁定過的 func 指派過來,與 func  行為完全相同。

3. 使用 bind  綁定參數表

bind  方法還有一個重要的功能:綁定參數表,如下例所示。

 var person = {

says: function(act, obj) {

console.log(this.name + ' ' + act + ' ' + obj);

person.says('loves', 'diovyb'); // 輸出 byvoid loves diovyb

byvoidloves = person.says.bind(person, 'loves');

byvoidloves('you'); // 輸出 byvoid loves you

可以看到, byvoidloves  将  this 指針綁定到了 person ,并将第一個參數綁定到

loves ,之後在調用  byvoidloves  的時候,隻需傳入第三個參數。這個特性可以用于建立

一個函數的“捷徑” ,之後我們可以通過這個“捷徑”調用,以便在代碼多處調用時省略重

複輸入相同的參數。

4. 了解  bind

盡管  bind 很優美,還是有一些令人迷惑的地方,例如下面的代碼:

func2 = func.bind(someuser);

全局函數  func  通過 someuser.func.bind 将 this 指針綁定到了 foo ,調用 func() 輸

出了 foobar 。我們試圖将 func2 指派為已綁定的 func 重新通過 bind 将 this 指針綁定到

someuser 的結果, 而調用 func2 時卻發現輸出值仍為 foobar , 即  this 指針還是停留在 foo

對象上,這是為什麼呢?要想解釋這個現象,我們必須了解 bind  方法的原理。

讓我們看一個 bind 方法的簡化版本(不支援綁定參數表) :

someuser.func.bind = function(self) {

return this.call(self);

假設上面函數是 someuser.func 的 bind  方法的實作,函數體内  this 指向的是

someuser.func ,因為函數也是對象,是以  this.call(self) 的作用就是以 self  作為

this 指針調用  someuser.func 。

//将func = someuser.func.bind(foo)展開:

func = function() {

return someuser.func.call(foo);

//再将func2 = func.bind(someuser)展開:

func2 = function() {

return func.call(someuser);

從上面展開過程我們可以看出, func2  實際上是以 someuser 作為 func 的 this 指

針調用了  func ,而  func 根本沒有使用  this 指針,是以兩次 bind  是沒有效果的。