本文首發(fā)在我的個(gè)人博客:http://muyunyun.cn/

《你不知道的JavaScript》系列叢書給出了很多顛覆以往對(duì)JavaScript認(rèn)知的點(diǎn), 讀完上卷,受益匪淺,于是對(duì)其精華的知識(shí)點(diǎn)進(jìn)行了梳理。

回到頂部(go to top)

什么是作用域

作用域是一套規(guī)則,用于確定在何處以及如何查找變量。

編譯原理

JavaScript是一門編譯語(yǔ)言。在傳統(tǒng)編譯語(yǔ)言的流程中,程序中一段源代碼在執(zhí)行之前會(huì)經(jīng)歷三個(gè)步驟,統(tǒng)稱為“編譯”。

  • 分詞/詞法分析
    將字符串分解成有意義的代碼塊,代碼塊又稱詞法單元。比如程序var a = 2;會(huì)被分解為var、a、=、2、;

  • 解析/語(yǔ)法分析
    將詞法單元流轉(zhuǎn)換成一個(gè)由元素逐級(jí)嵌套所組成的代表了程序語(yǔ)法接口的書,又稱“抽象語(yǔ)法樹”。

  • 代碼生成
    將抽象語(yǔ)法樹轉(zhuǎn)換為機(jī)器能夠識(shí)別的指令。

理解作用域

作用域 分別與編譯器、引擎進(jìn)行配合完成代碼的解析

  • 引擎執(zhí)行時(shí)會(huì)與作用域進(jìn)行交流,確定RHS與LHS查找具體變量,如果查找不到會(huì)拋出異常。

  • 編譯器負(fù)責(zé)語(yǔ)法分析以及生成代碼。

  • 作用域負(fù)責(zé)收集并維護(hù)所有變量組成的一系列查詢,并確定當(dāng)前執(zhí)行的代碼對(duì)這些變量的訪問權(quán)限。

對(duì)于 var a = 2 這條語(yǔ)句,首先編譯器會(huì)將其分為兩部分,一部分是 var a,一部分是 a = 2。編譯器會(huì)在編譯期間執(zhí)行 var a,然后到作用域中去查找 a 變量,如果 a 變量在作用域中還沒有聲明,那么就在作用域中聲明 a 變量,如果 a 變量已經(jīng)存在,那就忽略 var a 語(yǔ)句。然后編譯器會(huì)為 a = 2 這條語(yǔ)句生成執(zhí)行代碼,以供引擎執(zhí)行該賦值操作。所以我們平時(shí)所提到的變量提升,無非就是利用這個(gè)先聲明后賦值的原理而已!

異常

對(duì)于 var a = 10 這條賦值語(yǔ)句,實(shí)際上是為了查找變量 a, 并且將 10 這個(gè)數(shù)值賦予它,這就是 LHS 查詢。 對(duì)于 console.log(a) 這條語(yǔ)句,實(shí)際上是為了查找 a 的值并將其打印出來,這是 RHS 查詢。

為什么區(qū)分 LHS 和 RHS 是一件重要的事情?
在非嚴(yán)格模式下,LHS 調(diào)用查找不到變量時(shí)會(huì)創(chuàng)建一個(gè)全局變量,RHS 查找不到變量時(shí)會(huì)拋出 ReferenceError。 在嚴(yán)格模式下,LHS 和 RHS 查找不到變量時(shí)都會(huì)拋出 ReferenceError。

回到頂部(go to top)

作用域的工作模式

作用域共有兩種主要的工作模型。第一種是最為普遍的,被大多數(shù)編程語(yǔ)言所采用的詞法作用域( JavaScript 中的作用域就是詞法作用域)。另外一種是動(dòng)態(tài)作用域,仍有一些編程語(yǔ)言在使用(比如Bash腳本、Perl中的一些模式等)。

詞法作用域

詞法作用域是一套關(guān)于引擎如何尋找變量以及會(huì)在何處找到變量的規(guī)則。詞法作用域最重要的特征是它的定義過程發(fā)生在代碼的書寫階段(假設(shè)沒有使用 eval() 或 with )。來看示例代碼:

function foo() { console.log(a); // 2 } function bar() { var a = 3; foo(); } var a = 2; bar()

詞法作用域讓foo()中的a通過RHS引用到了全局作用域中的a,因此會(huì)輸出2。

動(dòng)態(tài)作用域

而動(dòng)態(tài)作用域只關(guān)心它們從何處調(diào)用。換句話說,作用域鏈?zhǔn)腔谡{(diào)用棧的,而不是代碼中的作用域嵌套。因此,如果 JavaScript 具有動(dòng)態(tài)作用域,理論上,下面代碼中的 foo() 在執(zhí)行時(shí)將會(huì)輸出3。

function foo() { console.log(a); // 3 } function bar() { var a = 3; foo(); } var a = 2; bar()

函數(shù)作用域

匿名與具名

對(duì)于函數(shù)表達(dá)式一個(gè)最熟悉的場(chǎng)景可能就是回調(diào)函數(shù)了,比如

setTimeout( function() { console.log("I waited 1 second!") }, 1000 )

這叫作匿名函數(shù)表達(dá)式。函數(shù)表達(dá)式可以匿名,而函數(shù)聲明則不可以省略函數(shù)名。匿名函數(shù)表達(dá)式書寫起來簡(jiǎn)單快捷,很多庫(kù)和工具也傾向鼓勵(lì)使用這種風(fēng)格的代碼。但它也有幾個(gè)缺點(diǎn)需要考慮。

  • 匿名函數(shù)在棧追蹤中不會(huì)顯示出有意義的函數(shù)名,使得調(diào)試很困難。

  • 如果沒有函數(shù)名,當(dāng)函數(shù)需要引用自身時(shí)只能使用已經(jīng)過期的 arguments.callee 引用,比如在遞歸中。另一個(gè)函數(shù)需要引用自身的例子,是在事件觸發(fā)后事件監(jiān)聽器需要解綁自身。

  • 匿名函數(shù)省略了對(duì)于代碼可讀性 / 可理解性很重要的函數(shù)名。一個(gè)描述性的名稱可以讓代碼不言自明。

始終給函數(shù)表達(dá)式命名是一個(gè)最佳實(shí)踐:

setTimeout( function timeoutHandler() { // 我有名字了 console.log("I waited 1 second!") }, 1000 )

回到頂部(go to top)

提升

先有聲明還是先有賦值

考慮以下代碼:

a = 2; var a; console.log(a); // 2

考慮另外一段代碼

console.log(a); // undefined var a = 2;

我們習(xí)慣將 var a = 2; 看作一個(gè)聲明,而實(shí)際上 JavaScript 引擎并不這么認(rèn)為。它將 var a 和 a = 2 當(dāng)作兩個(gè)單獨(dú)的聲明,第一個(gè)是編譯階段的任務(wù),而第二個(gè)是執(zhí)行階段的任務(wù)。
這意味著無論作用域中的聲明出現(xiàn)在什么地方,都將在代碼本身被執(zhí)行前首先進(jìn)行處理。可以將這個(gè)過程形象地想象成所有的聲明(變量和函數(shù))都會(huì)被“移動(dòng)”到各自作用域的最頂端,這個(gè)過程稱為提升。

可以看出,先有聲明后有賦值。

再來看以下代碼:

foo(); // TypeError bar(); // ReferenceError var foo = function bar() { // ... };

這個(gè)代碼片段經(jīng)過提升后,實(shí)際上會(huì)被理解為以下形式:

var foo; foo(); // TypeError bar(); // ReferenceError foo = function() { var bar = ...self... // ... };

這段程序中的變量標(biāo)識(shí)符 foo() 被提升并分配給全局作用域,因此 foo() 不會(huì)導(dǎo)致 ReferenceError。但是 foo 此時(shí)并沒有賦值(如果它是一個(gè)函數(shù)聲明而不是函數(shù)表達(dá)式就會(huì)賦值)。foo()由于對(duì) undefined 值進(jìn)行函數(shù)調(diào)用而導(dǎo)致非法操作,因此拋出 TypeError 異常。另外即時(shí)是具名的函數(shù)表達(dá)式,名稱標(biāo)識(shí)符(這里是 bar )在賦值之前也無法在所在作用域中使用。

回到頂部(go to top)

閉包

之前寫過關(guān)于閉包的一篇文章深入淺出JavaScript之閉包(Closure)

循環(huán)和閉包

要說明閉包,for 循環(huán)是最常見的例子。

for (var i = 1; i <= 5; i++) { setTimeout( function timer() { console.log(i); }, i*1000 ) }

正常情況下,我們對(duì)這段代碼行為的預(yù)期是分別輸出數(shù)字 1~5,每秒一次,每次一個(gè)。但實(shí)際上,這段代碼在運(yùn)行時(shí)會(huì)以每秒一次的頻率輸出五次6。

它的缺陷在于:根據(jù)作用域的工作原理,盡管循環(huán)中的五個(gè)函數(shù)是在各個(gè)迭代中分別定義的,但是它們都被封閉在一個(gè)共享的全局作用域中,因此實(shí)際上只有一個(gè)i。因此我們需要更多的閉包作用域。我們知道IIFE會(huì)通過聲明并立即執(zhí)行一個(gè)函數(shù)來創(chuàng)建作用域,我們來進(jìn)行改進(jìn):

for (var i = 1; i <= 5; i++) { (function() { var j = i; setTimeout( function timer() { console.log(j); }, j*1000 ) })(); }

還可以對(duì)這段代碼進(jìn)行一些改進(jìn):

for (var i = 1; i <= 5; i++) { (function(j) { setTimeout( function timer() { console.log(j); }, j*1000 ) })(i); }

在迭代內(nèi)使用 IIFE 會(huì)為每個(gè)迭代都生成一個(gè)新的作用域,使得延遲函數(shù)的回調(diào)可以將新的作用域封閉在每個(gè)迭代內(nèi)部,每個(gè)迭代中都會(huì)含有一個(gè)具有正確值的變量供我們?cè)L問。

重返塊作用域

我們使用 IIFE 在每次迭代時(shí)都創(chuàng)建一個(gè)新的作用域。換句話說,每次迭代我們都需要一個(gè)塊作用域。我們知道 let 聲明可以用來劫持塊作用域,那我們可以進(jìn)行這樣改:

for (var i = 1; i <= 5; i++) { let j = i; setTimeout( function timer() { console.log(j); }, j*1000 ) }

本質(zhì)上這是將一個(gè)塊轉(zhuǎn)換成一個(gè)可以被關(guān)閉的作用域。

此外,for循環(huán)頭部的 let 聲明還會(huì)有一個(gè)特殊行為。這個(gè)行為指出每個(gè)迭代都會(huì)使用上一個(gè)迭代結(jié)束時(shí)的值來初始化這個(gè)變量。

for (let i = 1; i <= 5; i++) { setTimeout( function timer() { console.log(i); }, i*1000 ) }

回到頂部(go to top)

this全面解析

之前寫過一篇深入淺出JavaScript之this。我們知道this是在運(yùn)行時(shí)進(jìn)行綁定的,并不是在編寫時(shí)綁定,它的上下文取決于函數(shù)調(diào)用時(shí)的各種條件。this的綁定和函數(shù)聲明的位置沒有任何關(guān)系,只取決于函數(shù)的調(diào)用方式。

this詞法

來看下面這段代碼的問題:

var obj = { id: "awesome", cool: function coolFn() { console.log(this.id); } }; var id = "not awesome"; obj.cool(); // awesome setTimeout( obj.cool, 100); // not awesome

obj.cool() 與 setTimeout( obj.cool, 100 ) 輸出結(jié)果不一樣的原因在于 cool() 函數(shù)丟失了同 this 之間的綁定。解決方法最常用的是 var self = this;

var obj = { count: 0, cool: function coolFn() { var self = this; if (self.count < 1) { setTimeout( function timer(){ self.count++; console.log("awesome?"); }, 100) } } } obj.cool(); // awesome?

這里用到的知識(shí)點(diǎn)是我們非常熟悉的詞法作用域。self 只是一個(gè)可以通過詞法作用域和閉包進(jìn)行引用的標(biāo)識(shí)符,不關(guān)心 this 綁定的過程中發(fā)生了什么。

ES6 中的箭頭函數(shù)引人了一個(gè)叫作 this 詞法的行為:

var obj = { count: 0, cool: function coolFn() { if (this.count < 1) { setTimeout( () => { this.count++; console.log("awesome?"); }, 100) } } } obj.cool(); // awesome?

箭頭函數(shù)棄用了所有普通 this 綁定規(guī)則,取而代之的是用當(dāng)前的詞法作用域覆蓋了 this 本來的值。因此,這個(gè)代碼片段中的箭頭函數(shù)只是"繼承"了 cool() 函數(shù)的 this 綁定。

但是箭頭函數(shù)的缺點(diǎn)就是因?yàn)槠涫悄涿?,上文已介紹過具名函數(shù)比匿名函數(shù)更可取的原因。而且箭頭函數(shù)將程序員們經(jīng)常犯的一個(gè)錯(cuò)誤給標(biāo)準(zhǔn)化了:混淆了 this 綁定規(guī)則和詞法作用域規(guī)則。

箭頭函數(shù)不僅僅意味著可以少寫代碼。本書的作者認(rèn)為使用 bind() 是更靠得住的方式。

var obj = { count: 0, cool: function coolFn() { if (this.count < 1) { setTimeout( () => { this.count++; console.log("more awesome"); }.bind( this ), 100) } } } obj.cool(); // more awesome

綁定規(guī)則

函數(shù)在執(zhí)行的過程中,可以根據(jù)下面這4條綁定規(guī)則來判斷 this 綁定到哪。

  • 默認(rèn)綁定

    • 獨(dú)立函數(shù)調(diào)用

  • 隱式綁定

    • 當(dāng)函數(shù)引用有上下文對(duì)象時(shí),隱式綁定規(guī)則會(huì)把函數(shù)調(diào)用中的 this 綁定到這個(gè)上下文對(duì)象

  • 顯示綁定

    • call/apply

    • bind(本質(zhì)是對(duì)call/apply函數(shù)的封裝 fn.apply( obj, arguments )

    • 第三方庫(kù)的許多函數(shù)都提供了一個(gè)可選的參數(shù)(上下文),其作用和 bind() 一樣,確保回調(diào)函數(shù)使用指定的 this

  • new 綁定

    • JavaScript 中的 new 機(jī)制實(shí)際上和面向類的語(yǔ)言完全不同

    • 實(shí)際上并不存在所謂的“構(gòu)造函數(shù)”,只有對(duì)于函數(shù)的“構(gòu)造調(diào)用”

書中對(duì)4條綁定規(guī)則的優(yōu)先級(jí)進(jìn)行了驗(yàn)證,得出以下的順序優(yōu)先級(jí):

  • 函數(shù)是否在 new 中調(diào)用(new 綁定)?如果是的話 this 綁定的是新創(chuàng)建的對(duì)象。

  • 函數(shù)是否通過 call、apply(顯式綁定)或者硬綁定(bind)調(diào)用?如果是的話,this 綁定的是指定對(duì)象。

  • 函數(shù)是否在某個(gè)上下文對(duì)象中調(diào)用(隱式綁定)?如果是的話,this 綁定的是那個(gè)上下文對(duì)象。

  • 如果都不是的話,使用默認(rèn)綁定。在嚴(yán)格模式下,綁定到 undefined,否則綁定到全局對(duì)象。

被忽略的 this

如果你把 null 或者 undefined 作為 this 的綁定對(duì)象傳入 call、apply 或者 bind,這些值在調(diào)用時(shí)會(huì)被忽略,實(shí)際應(yīng)用的是默認(rèn)規(guī)則。

什么時(shí)候會(huì)傳入 null/undefined 呢?一種非常常見的做法是用 apply(..) 來“展開”一個(gè)數(shù)組,并當(dāng)作參數(shù)傳入一個(gè)函數(shù)。類似地,bind(..) 可以對(duì)參數(shù)進(jìn)行柯里化(預(yù)先設(shè)置一些參數(shù)),如下代碼:

function foo(a, b) { console.log( "a:" + a + ", b:" + b ); } // 把數(shù)組"展開"成參數(shù) foo.apply(null, [2, 3]); // a:2, b:3 // 使用 bind(..) 進(jìn)行柯里化 var bar = foo.bind( null, 2); bar(3); // a:2, b:3

其中 ES6 中,可以用 ... 操作符代替 apply(..) 來“展開”數(shù)組,但是 ES6 中沒有柯里化的相關(guān)語(yǔ)法,因此還是需要使用 bind(..)。

使用 null 來忽略 this 綁定可能產(chǎn)生一些副作用。如果某個(gè)函數(shù)(比如第三庫(kù)中的某個(gè)函數(shù))確實(shí)使用了 this ,默認(rèn)綁定規(guī)則會(huì)把 this 綁定到全局對(duì)象,這將導(dǎo)致不可預(yù)計(jì)的后果。更安全的做法是傳入一個(gè)特殊的對(duì)象,一個(gè) “DMZ” 對(duì)象,一個(gè)空的非委托對(duì)象,即 Object.create(null)。

function foo(a, b) { console.log( "a:" + a + ", b:" + b ); } var ? = Object.create(null); // 把數(shù)組"展開"成參數(shù) foo.apply( ?, [2, 3]); // a:2, b:3 // 使用 bind(..) 進(jìn)行柯里化 var bar = foo.bind( ?, 2); bar(3); // a:2, b:3

回到頂部(go to top)

對(duì)象

JavaScript中的對(duì)象有字面形式(比如var a = { .. })和構(gòu)造形式(比如var a = new Array(..))。字面形式更常用,不過有時(shí)候構(gòu)造形式可以提供更多選擇。

作者認(rèn)為“JavaScript中萬(wàn)物都是對(duì)象”的觀點(diǎn)是不對(duì)的。因?yàn)?對(duì)象只是 6 個(gè)基礎(chǔ)類型( string、number、boolean、null、undefined、object )之一。對(duì)象有包括 function 在內(nèi)的子對(duì)象,不同子類型具有不同的行為,比如內(nèi)部標(biāo)簽 [object Array] 表示這是對(duì)象的子類型數(shù)組。

復(fù)制對(duì)象

思考一下這個(gè)對(duì)象:

function anotherFunction() { /*..*/ } var anotherObject = { c: true }; var anotherArray = []; var myObject = { a: 2, b: anotherObject, // 引用,不是復(fù)本! c: anotherArray, // 另一個(gè)引用! d: anotherFunction }; anotherArray.push( myObject )

如何準(zhǔn)確地表示 myObject 的復(fù)制呢?
這里有一個(gè)知識(shí)點(diǎn)。

  • 淺復(fù)制。復(fù)制出的新對(duì)象中 a 的值會(huì)復(fù)制舊對(duì)象中 a 的值,也就是 2,但是新對(duì)象中 b、c、d 三個(gè)屬性其實(shí)只是三個(gè)引用。

  • 深復(fù)制。除了復(fù)制 myObject 以外還會(huì)復(fù)制 anotherArray。這時(shí)問題就來了,anotherArray 引用了 myObject, 所以又需要復(fù)制 myObject,這樣就會(huì)由于循環(huán)引用導(dǎo)致死循環(huán)。

對(duì)于 JSON 安全的對(duì)象(就是能用 JSON.stringify 序列號(hào)的字符串)來說,有一種巧妙的復(fù)制方法:

var newObj = JSON.parse( JSON.stringify(someObj) )

我認(rèn)為這種方法就是深復(fù)制。相比于深復(fù)制,淺復(fù)制非常易懂并且問題要少得多,ES6 定義了 Object.assign(..) 方法來實(shí)現(xiàn)淺復(fù)制。 Object.assign(..) 方法的第一個(gè)參數(shù)是目標(biāo)對(duì)象,之后還可以跟一個(gè)或多個(gè)源對(duì)象。它會(huì)遍歷一個(gè)或多個(gè)源對(duì)象的所有可枚舉的自由鍵并把它們復(fù)制到目標(biāo)對(duì)象,最后返回目標(biāo)對(duì)象,就像這樣:

var newObj = Object.assign( {}, myObject ); newObj.a; // 2 newObj.b === anotherObject; // true newObj.c === anotherArray; // true newObj.d === anotherFunction; // true

回到頂部(go to top)

JavaScript 有一些近似類的語(yǔ)法元素(比如 new 和 instanceof), 后來的 ES6 中新增了一些如 class 的關(guān)鍵字。但是 JavaScript 實(shí)際上并沒有類。類是一種設(shè)計(jì)模式,JavaScript 的機(jī)制其實(shí)和類完全不同。

  • 類的繼承(委托)其實(shí)就是復(fù)制,但和其他語(yǔ)言中類的表現(xiàn)不同(其他語(yǔ)言類表現(xiàn)出來的都是復(fù)制行為),JavaScript 中的多態(tài)(在繼承鏈中不同層次名稱相同,但是功能不同的函數(shù))并不表示子類和父類有關(guān)聯(lián),子類得到的只是父類的一份復(fù)本。

  • JavaScript 通過顯示混入和隱式混入 call() 來模擬其他語(yǔ)言類的表現(xiàn)。此外,顯示混入實(shí)際上無法完全模擬類的復(fù)制行為,因?yàn)閷?duì)象(和函數(shù)!別忘了函數(shù)也是對(duì)象)只能復(fù)制引用,無法復(fù)制被引用的對(duì)象或者函數(shù)本身。

檢查“類”關(guān)系

思考下面的代碼:

function Foo() { // ... } Foo.prototype.blah = ...; var a = new Foo();

我們?nèi)绾握页? a 的“祖先”(委托關(guān)系)呢?

  • 方法一:a instanceof Foo; // true (對(duì)象 instanceof 函數(shù))

  • 方法二: Foo.prototype.isPrototypeOf(a); // true (對(duì)象 isPrototypeOf 對(duì)象)

  • 方法三: Object.getPrototypeOf(a) === Foo.prototype; // true (Object.getPrototypeOf() 可以獲取一個(gè)對(duì)象的 [[Prototype]]) 鏈;

  • 方法四: a.__proto__ == Foo.prototype; // true

構(gòu)造函數(shù)
  • 函數(shù)不是構(gòu)造函數(shù),而是當(dāng)且僅當(dāng)使用 new 時(shí),函數(shù)調(diào)用會(huì)變成“構(gòu)造函數(shù)調(diào)用”。

  • 使用 new 會(huì)在 prototype 生成一個(gè) constructor 屬性,指向構(gòu)造調(diào)用的函數(shù)。

  • constructor 并不表示被構(gòu)造,而且 constructor 屬性并不是一個(gè)不可變屬性,它是不可枚舉的,但它是可以被修改的。

對(duì)象關(guān)聯(lián)

來看下面的代碼:

var foo = { something: function() { console.log("Tell me something good..."); } }; var bar = Object.create(foo); bar.something(); // Tell me something good...

Object.create(..)會(huì)創(chuàng)建一個(gè)新對(duì)象 (bar) 并把它關(guān)聯(lián)到我們指定的對(duì)象 (foo),這樣我們就可以充分發(fā)揮 [[Prototype]] 機(jī)制的為例(委托)并且避免不必要的麻煩 (比如使用 new 的構(gòu)造函數(shù)調(diào)用會(huì)生成 .prototype 和 .constructor 引用)。

Object.create(null) 會(huì)創(chuàng)建一個(gè)擁有空鏈接的對(duì)象,這個(gè)對(duì)象無法進(jìn)行委托。由于這個(gè)對(duì)象沒有原型鏈,所以 instanceof 操作符無法進(jìn)行判斷,因此總是會(huì)返回 false 。這些特殊的空對(duì)象通常被稱作“字典”,它們完全不會(huì)受到原型鏈的干擾,因此非常適合用來存儲(chǔ)數(shù)據(jù)。

我們并不需要類來創(chuàng)建兩個(gè)對(duì)象之間的關(guān)系,只需要通過委托來關(guān)聯(lián)對(duì)象就足夠了。而Object.create(..)不包含任何“類的詭計(jì)”,所以它可以完美地創(chuàng)建我們想要的關(guān)聯(lián)關(guān)系。

此書的第二章第6部分就把面對(duì)類和繼承行為委托兩種設(shè)計(jì)模式進(jìn)行了對(duì)比,我們可以看到行為委托是一種更加簡(jiǎn)潔的設(shè)計(jì)模式,在這種設(shè)計(jì)模式中能感受到Object.create()的強(qiáng)大。

ES6中的Class

來看一段 ES6中Class 的例子

class Widget { constructor(width, height) { this.width = width || 50; this.height = height || 50; this.$elem = null; } render($where){ if (this.$elem) { this.$elem.css({ width: this.width + "px", height: this.height + "px" }).appendTo($where); } } } class Button extends Widget { constructor(width, height, label) { super(width, height); this.label = label || "Default"; this.$elem = $("<button>").text(this.label) } render($where) { super($where); this.$elem.click(this.onClick.bind(this)); } onClick(evt) {

出處:http://www.cnblogs.com/MuYunyun/" 
本文版權(quán)歸作者和博客園所有,歡迎轉(zhuǎn)載,轉(zhuǎn)載請(qǐng)標(biāo)明出處。 
如果您覺得本篇博文對(duì)您有所收獲,請(qǐng)點(diǎn)擊右下角的 [推薦],謝謝!

http://www.cnblogs.com/MuYunyun/p/6954155.html