你不知道的js——做用域javascript
1 做用域java
1.1編譯原理node
在傳統編譯語言的流程中,程序中的一段源代碼在執行以前會經歷三個步驟,統稱爲「編 譯」jquery
1.2理解做用域程序員
1.3 做用域嵌套ajax
當前做用域沒法找到某個變量時,引擎會在外層嵌套的做用域中查找,直至抵達最外層(全局)時仍未找到,拋出錯誤。編程
1.4 異常api
變量沒法找到時,拋出ReferenceError(參考錯誤),與做用域判別失敗相關。 對變量的值進行不合理操做,拋出TypeError,與操做是否合法相關。數組
當引擎執行 LHS 查詢時,若是在頂層(全局做用域)中也沒法找到目標變量, 全局做用域中就會建立一個具備該名稱的變量,並將其返還給引擎,前提是程序運行在非 「嚴格模式」下。瀏覽器
(嚴格模式禁止自動或隱式地建立全局變量。)
(若是 RHS 查詢找到了一個變量,可是你嘗試對這個變量的值進行不合理的操做, 好比試圖對一個非函數類型的值進行函數調用,或着引用 null 或 undefined 類型的值中的 屬性,那麼引擎會拋出另一種類型的異常,叫做 TypeError。
ReferenceError 同做用域判別失敗相關,而 TypeError 則表明做用域判別成功了,可是對 結果的操做是非法或不合理的。)
2 詞法做用域
詞法分析階段 基本可以知道所有標識符在哪裏以及是如何聲明的,從而可以預測在執行過程當中如何對它 們進行查找。
做用域共有兩種主要的工做模型。第一種是最爲廣泛的,被大多數編程語言所採用的詞法 做用域,咱們會對這種做用域進行深刻討論。另一種叫做動態做用域,仍有一些編程語 言在使用(好比 Bash 腳本、Perl 中的一些模式等)。
2.1 詞法階段
詞法做用域即定義在詞法階段的做用域。
查找
做用域氣泡的結構和互相之間的位置關係給引擎提供了足夠的位置信息,引擎用這些信息 來查找標識符的位置。
做用域查找會在找到第一個匹配的標識符時中止。。在多層的嵌套做用域中能夠定義同名的 標識符,這叫做「遮蔽效應」(內部的標識符「遮蔽」了外部的標識符)。
全局變量會自動成爲全局對象(好比瀏覽器中的 window 對象)的屬性,所以 能夠間接地經過對全局對象屬性的引 用來對其進行訪問。——→ window.a
經過這種技術能夠訪問那些被同名變量所遮蔽的全局變量。但非全局的變量 若是被遮蔽了,不管如何都沒法被訪問到。
2.2 欺騙詞法
在運行時來「修 改」(也能夠說欺騙)詞法做用域,:欺騙詞法做用域會致使性能 降低(緣由主要是沒法對代碼的詞法進行靜態分析,預先確認變量和函數的位置,從而快速尋找。)
【另一個不推薦使用 eval(..) 和 with 的緣由是會被嚴格模式所影響(限 制)。with 被徹底禁止,而在保留核心功能的前提下,間接或非安全地使用 eval(..) 也被禁止了。】
2.2.1 eval
接受一個字符串爲參數,並將其中的內容視爲好像在書 寫時就存在於程序中這個位置的代碼
eval(..) 一般被用來執行動態建立的代碼
eval(..) 調用中的 "var b = 3;" 這段代碼會被看成原本就在那裏同樣來處理。因爲那段代 碼聲明瞭一個新的變量 b,所以它對已經存在的 foo(..) 的詞法做用域進行了修改。事實 上,和前面提到的原理同樣,這段代碼實際上在 foo(..) 內部建立了一個變量 b,並遮蔽 了外部(全局)做用域中的同名變量。
在嚴格模式的程序中,eval(..) 在運行時有其本身的詞法做用域,意味着其中的聲明沒法修改所在的做用域。
與之類似的有setTimeout,setInterval,new Function(),均可以接收字符串。
2.2.2 with
with一般被看成重複引用同一個對象中的多個屬性的快捷方式,能夠不須要重複引用對象自己。with會建立一個全新的詞法做用域。 在嚴格模式下,with被禁止。
例子:
function foo(obj) { with (obj) { a = 2; } } var o2 = { b: 3 }; foo( o2 ); console.log( o2.a ); // undefined console.log( a ); // 2——很差,a 被泄漏到全局做用域上了! (但當咱們將 o2 做爲做用域時,其中並無 a 標識符, 所以進行了正常的 LHS 標識符查找; o2 的做用域、foo(..) 的做用域和全局做用域中都沒有找到標識符 a,所以當 a=2 執行 時,自動建立了一個全局變量(由於是非嚴格模式)。)
3 函數做用域和塊做用域
3.1 函數中的做用域
函數做用域的含義是指,屬於這個函數的所有變量均可以在整個函數的範圍內使用及復 用(事實上在嵌套的做用域中也可使用)。JS具備基於函數的做用域。內部能向外訪問,外部不能向內訪問。
3.2 隱藏內部實現
把變量和函數包裹在一個函數的做用域中,而後用這個做用域 來「隱藏」它們。
最小受權或最小暴露原則:在軟件設計中,應該最小限度地暴露必要的內容,而將其餘內容都隱藏起來。 隱藏做用域可以避免同名標識符之間的衝突。
規避衝突:「隱藏」做用域中的變量和函數所帶來的另外一個好處,是能夠避免同名標識符之間的衝突, 兩個標識符可能具備相同的名字但用途卻不同,無心間可能形成命名衝突。衝突會致使 變量的值被意外覆蓋。
3.2.1 全局命名空間
引入第三方庫時,沒有妥善地將內部私有的函數或變量隱藏起來,就會很容易引起衝突
3.2.2 模塊管理
另一種避免衝突的辦法和現代的模塊機制很接近,就是從衆多模塊管理器中挑選一個來 使用。
無需將標識符加入到全局做用域中,而是經過依賴管理器 的機制將庫的標識符顯式地導入到另一個特定的做用域中
3.3 函數做用域
若是function是聲明中第一個詞(前面沒有其餘詞,甚至是括號),那麼就是一個函數聲明,不然就是一個函數表達式。函數表達式能夠是匿名的, 而函數聲明則不能夠省略函數名
在任意代碼片斷外部添加包裝函數,能夠將內部的變量和函數定義「隱 藏」起來,外部做用域沒法訪問包裝函數內部的任何內容。可是它並不理想,由於會致使一些額外的問題。首先, 必須聲明一個具名函數 foo(),意味着 foo 這個名稱自己「污染」了所在做用域(在這個 例子中是全局做用域)。
函數不須要函數名(或者至少函數名能夠不污染所在做用域),而且可以自動運行, 這將會更加理想。
☆☆☆☆☆包裝函數的聲明以 (function... 而不只是以 function... 開始。,函數會被看成函數表達式而不是一 個標準的函數聲明來處理。
(function foo(){ .. }) 做爲函數表達式意味着foo 被綁定在函數表達式自身的函數中,外部做用域則不行。foo 變量名被隱藏在自身中意味着不會非必要地污染外部做 用域。
3.3.1 匿名和具名
於函數表達式你最熟悉的場景可能就是回調參數
函數表達式能夠匿名,函數聲明必須具名。 匿名函數也有缺點:
選擇性地給函數表達式具名,能夠解決以上問題。
行內函數表達式很是強大且有用——匿名和具名之間的區別並不會對這點有任何影響,始終給函數表達式命名是一個最佳實踐。
3.3.2 當即執行函數表達式IIFE(Immediately Invoked Function Expression)
用圓括號()將函數包裹,而後緊跟圓括號調用。 另外一種能夠將括號放在內部。 (function ( ){ …… } ( )); UMD標準中,IIFE也被普遍運用,好比:
var a = 2; (function IIFE( def ) { def( window ) })(function def ( global ) { var a = 3; console.log( a ); //3 console.log( global.a ); //2 }); //函數表達式 def 定義在片斷的第二部分,而後看成參數(這個參數也叫做 def)被傳遞進 IIFE 函數定義的第一部分中。最後,參數 def(也就是傳遞進去的函數)被調用,並將 window 傳入看成 global 參數的值
3.4 塊做用域
在 for 循環的頭部直接定義了變量 i,一般是由於只想在 for 循環內部的上下文中使 用 i,而忽略了 i 會被綁定在外部做用域(函數或全局)中的事實。這就是塊做用域的用處。變量的聲明應該距離使用的地方越近越好,並最大限度地本地 化。
塊做用域是一個用來對以前的最小受權原則進行擴展的工具,將代碼從在函數中隱藏信息 擴展爲在塊中隱藏信息。
(因屢次多處訪問而提早作緩存除外) 塊級做用域在ES6中獲得普遍應用。在此以前須要注意,var聲明的變量並不屬於塊級做用域。 能夠生成塊級做用域的有:
try/catch: catch 分句會建立一個塊做 用域,其中聲明的變量僅在 catch 內部有效
try {
undefined(); // 執行一個非法操做來強制製造一個異常
} catch (err) {
console.log( err ); // 可以正常執行!
}
console.log( err ); // ReferenceError: err not found
const(ES6新特性),能夠造成暫時性鎖區。一樣能夠用來建立塊做用域變量,但其值是固定的 (常量)。以後任何試圖修改值的操做都會引發錯誤。
*塊級做用域的用處:**有利於垃圾收集。程序塊在執行後,其中的變量若是不被後續須要(閉包等),就能夠將內存回收 . 減小變量空間污染。
1 垃圾收集
2 let循環
4 提高
4.1 先有雞仍是先有蛋
function和var聲明,會被提高到頂部。 其餘的操做不會提高,處於自身本來的位置。
能夠看到,函數聲明會被提高,可是函數表達式卻不會被提高。 foo(); // 不是 ReferenceError, 而是 TypeError! var foo = function bar() { // ... }; //這段程序中的變量標識符 foo() 被提高並分配給所在做用域(在這裏是全局做用域),所以 foo() 不會致使 ReferenceError。可是 foo 此時並無賦值(若是它是一個函數聲明而不 是函數表達式,那麼就會賦值)。foo() 因爲對 undefined 值進行函數調用而致使非法操做, 所以拋出 TypeError 異常。
4.2 編譯器再度來襲
當你看到 var a = 2; 時,可能會認爲這是一個聲明。但 JavaScript 實際上會將其當作兩個聲明:var a; 和 a = 2; 第一個定義聲明是在編譯階段進行的。第二個賦值聲明會被留在 原地等待執行階段。
每一個做用域都會進行提高操做,其頂部爲其自身做用域的頂部(注意不會跨越做用域)。 函數表達式不會提高,而是處於正常的位置。 具名的函數表達式也不存在提高
foo(); // TypeError,由於此時foo被初始化爲undefined,尚未賦值爲函數表達式 bar(); // ReferenceError,由於函數表達式不提高,此時bar尚未聲明 var foo = function bar(){ // do sth }
等效於
var foo; foo(); // TypeError,由於此時foo被初始化爲undefined,尚未賦值爲函數表達式 bar(); // ReferenceError,由於函數表達式不提高,此時bar尚未聲明 foo = function (){ var bar = self; // do sth }
4.3 函數優先
函數首先被提高,而後是變量。函數與變量同名時,變量的聲明會被省略,可是依舊能夠進行賦值操做。 後出現的函數聲明會覆蓋前面的同名函數,因此在同一做用域內,千萬不要聲明同名的函數 在普通塊內部的聲明,也會提高到做用域頂部,而不是在塊內。
//一個普通塊內部的函數聲明一般會被提高到所在做用域的頂部,這個過程不會像下面的代 碼暗示的那樣能夠被條件判斷所控制: foo() // 'b' var a = true; if(a){ function foo(){ console.log('a') } // 被提高到做用域頂部。先聲明,被後面覆蓋了 } else { function foo(){ console.log('b') } // 被提高到第二個。後聲明,覆蓋了前面 }
所以應該 儘量避免在塊內部聲明函數。
5 做用域閉包
5.1 啓示
JS中閉包無處不在。 閉包是基於詞法做用域書寫代碼時產生的天然結果。 閉包特徵就是將函數表達式,連帶其詞法做用域,進行傳值。
5.2 實質問題
一個閉包的例子:
function foo(){ var a = 2; function bar(){ console.log(a); } return bar; //也能夠直接返回函數表達式,function(){ console.log(a); } } var baz = foo(); baz(); // 2,讀取到了foo中定義的變量a
本質上是,函數bar的詞法做用域可以訪問foo的內部做用域,由於它的做用域嵌套在了foo的做用域內。 也就是說,最終return的結果是帶有scope(做用域)信息的,這個是可以找到須要調用變量的根本依據。 當bar在使用時,閉包會阻止垃圾回收器回收foo的做用域(有好有壞)。
5.3 如今我懂了
引擎在調用函數的同時,其詞法做用域會保持完整。 若是將(訪問他們各自詞法做用域的)函數看成第一級的值類型並處處傳遞,你就會看到閉包在這些函數中的應用。 也就是說,只要使用了回調函數,實際上就是在使用閉包。(在定時器、事件監聽器、 Ajax 請求、跨窗口通訊、Web Workers 或者任何其餘的異步(或者同步)任務中,只要使 用了回調函數,實際上就是在使用閉包!)
5.4 循環和閉包
異步時候,函數進行回調時,調用變量值的是回調發生時候的值,因此要考慮運行時的狀態。
拓展解析:任務隊列、進階
for(var i=0; i<5; i++){ setTimeout( function(){ console.log(i); //輸出都是5 }, 1000*i); }
解決方法有IIFE
for(var i=0; i<5; i++){ // 建立一個新的詞法做用域,把它的值在這個做用域裏面記錄下來 (function(j){ setTimeout(function(){ console.log(j); //這樣輸出就是0,1,2,3,4了 }, 1000*j); })(i); }
或者利用做用塊
// let能夠做用於做用塊 for(let i=0; i<5; i++){ setTimeout( function(){ console.log(i); //這樣輸出就是0,1,2,3,4了 }, 1000*i); }
這裏面本質就是
for(var i=0; i<5; i++){ // 使用僅存在於該做用塊的變量 let _i = i; setTimeout( function(){ console.log(_i); //這樣輸出就是0,1,2,3,4了 }, 1000*_i); }
5.5 模塊
實現模塊的模式稱爲模塊暴露。
function MyModule(){
var something = 'cool';
var another = [1,2,3];
function doSomething(){ console.log(something); } function doAnother(){ console.log(another.join(',')); } //ES5的寫法 return { doSomething: doSomething, doAnother: doAnother, }; // ES6的寫法 /* return { doSomething, doAnother, }; */
}
// 獲得了這個閉包
var foo = MyModule();
foo.doSomething(); // cool
foo.doAnother(); // 1,2,3
模塊模式另外一個簡單但強大的變化用法是,命名將要做爲公共 API 返回的對象:
var foo = (function CoolModule(id) {
function change() {
// 修改公共 API
publicAPI.identify = identify2;
}
function identify1() { console.log( id ); } function identify2() { console.log( id.toUpperCase() ); } var publicAPI = { change: change, identify: identify1 }; return publicAPI; })( "foo module" ); foo.identify(); // foo module foo.change(); foo.identify(); // FOO MODULE
經過在模塊實例的內部保留對公共 API 對象的內部引用,能夠從內部對模塊實例進行修 改,包括添加或刪除方法和屬性,以及修改它們的值。
經過在模塊實例的內部保留對公共 API 對象的內部引用,能夠從內部對模塊實例進行修 改,包括添加或刪除方法和屬性,以及修改它們的值。
5.5.1 現代的模塊機制
一個典型的模塊管理器能夠定義爲(應該是參考了Require.js)
var MyModules = (function Manager(){ var modules = {}; function define(name, deps, impl) { // 抽取依賴 for(var i=0; i<deps.length; i++){ deps[i] = modules[deps[i]]; } // 綁定依賴,得到模塊 modules[name] = impl.apply(impl,deps); } function get(name){ return modules[name]; } // 把方法暴露出來 return { define: define, get: get, }; });
其使用方式爲
// 名稱爲bar的模塊,沒有依賴 MyModules.define('bar', [], function(){ function hello(who){ return 'Let me introduce: ' + who; } return { hello: hello, }; }); // 名稱爲foo,依賴了bar。 // 注意這裏函數表達式能拿到bar,是由於define方法去modules中抽取了bar,而後傳入給它。 MyModules.define('foo', ['bar'], function(bar){ var hungry = 'hippo'; function awesome(){ // 由於外層參數已經傳入了bar,因此這裏也就能拿到bar的hello方法 console.log(bar.hello(hungry).toUpperCase()); } return { awesome: awesome, }; }); var foo = MyModules.get('foo'); foo.awesome(); // LET ME INTRODUCE HIPPO
5.5.2 將來的模塊機制
ES6 的模塊沒有「行內」格式,必須被定義在獨立的文件中(一個文件一個模塊)。瀏覽 器或引擎有一個默認的「模塊加載器」(能夠被重載,但這遠超出了咱們的討論範圍)可 以在導入模塊時異步地加載模塊文件。
bar.js
export function hello(who){ return 'Let me introduce: ' + who; }
foo.js
// 直接得到了hello import {hello} from 'bar'; var hungry = 'hippo'; export function awesome(){ console.log(hello(hungry).toUpperCase()); }
baz.js
// 導入完整的foo模塊 module foo from 'bar'; foo.awesome(); // LET ME INTRODUCE HIPPO
5.6 小結
閉包相似於一個標準,關於如何在函數做爲值按需傳遞的詞法環境中書寫代碼的
當函數能夠記住並訪問所在的詞法做用域,即便函數是在當前詞法做用域以外執行,這時 就產生了閉包。
時閉包也是一個很是強大的工具,能夠用多種形式來實現模塊等模式。
模塊有兩個特徵:
補充:模塊機制、AMD、require.js
當即執行函數寫法
使用"當即執行函數"(Immediately-Invoked Function Expression,IIFE),能夠達到不暴露私有成員的目的。
var module1 = (function(){ var _count = 0; var m1 = function(){ //... }; var m2 = function(){ //... }; return { m1 : m1, m2 : m2 }; })();
使用上面的寫法,外部代碼沒法讀取內部的_count變量。
console.info(module1._count); //undefined
module1就是Javascript模塊的基本寫法。下面,再對這種寫法進行加工。
放大模式:,必須分紅幾個部分,或者一個模塊須要繼承另外一個模塊,這時就有必要採用"放大模式"(augmentation)。
var module1 = (function (mod){ mod.m3 = function () { //... }; return mod; })(module1);
上面的代碼爲module1模塊添加了一個新方法m3(),而後返回新的module1模塊。
寬放大模式(Loose augmentation) :在瀏覽器環境中,模塊的各個部分一般都是從網上獲取的,有時沒法知道哪一個部分會先加載。若是採用上一節的寫法,第一個執行的部分有可能加載一個不存在空對象,這時就要採用"寬放大模式"。
var module1 = ( function (mod){ //... return mod; })(window.module1 || {});
與"放大模式"相比,"寬放大模式"就是"當即執行函數"的參數能夠是空對象。
輸入全局變量
獨立性是模塊的重要特色,模塊內部最好不與程序的其餘部分直接交互。
爲了在模塊內部調用全局變量,必須顯式地將其餘變量輸入模塊。
var module1 = (function ($, YAHOO) { //... })(jQuery, YAHOO);
上面的module1模塊須要使用jQuery庫和YUI庫,就把這兩個庫(實際上是兩個模塊)看成參數輸入module1。這樣作除了保證模塊的獨立性,還使得模塊之間的依賴關係變得明顯。
AMD規範
通行的Javascript模塊規範共有兩種:CommonJS和AMD。
CommonJS
2009年,美國程序員Ryan Dahl創造了node.js項目,將javascript語言用於服務器端編程。
node.js的模塊系統,就是參照CommonJS規範實現的。在CommonJS中,有一個全局性方法require(),用於加載模塊。假定有一個數學模塊math.js,就能夠像下面這樣加載。
var math = require('math');
而後,就能夠調用模塊提供的方法:
var math = require('math');
math.add(2,3); // 5
由於這個系列主要針對瀏覽器編程,不涉及node.js,因此對CommonJS就很少作介紹了。咱們在這裏只要知道,require()用於加載模塊就好了。
瀏覽器環境 對於瀏覽器,模塊都放在服務器端,等待時間取決於網速的快慢,可能要等很長時間,瀏覽器處於"假死"狀態。
所以,瀏覽器端的模塊,不能採用"同步加載"(synchronous),只能採用"異步加載"(asynchronous)。這就是AMD規範誕生的背景。
AMD是"Asynchronous Module Definition"的縮寫,意思就是"異步模塊定義"。
AMD也採用require()語句加載模塊,可是不一樣於CommonJS,它要求兩個參數:
require([module], callback);
第一個參數[module],是一個數組,裏面的成員就是要加載的模塊;第二個參數callback,則是加載成功以後的回調函數。
目前,主要有兩個Javascript庫實現了AMD規範:require.js和curl.js。
require.js的用法
依次加載多個js文件會有很大的弊端。
首先,加載的時候,瀏覽器會中止網頁渲染,加載文件越多,網頁失去響應的時間就會越長;其次,因爲js文件之間存在依賴關係,所以必須嚴格保證加載順序(好比上例的1.js要在2.js的前面),依賴性最大的模塊必定要放到最後加載,當依賴關係很複雜的時候,代碼的編寫和維護都會變得困難。
require.js的誕生解決了兩個問題
require.js的加載
使用require.js的第一步,是先去官方網站下載最新版本。
下載後,假定把它放在js子目錄下面,就能夠加載了。
<script src="js/require.js"></script> //加載這個文件,也可能形成網頁失去響應。解決辦法有兩個,一個是把它放在網頁底部加載,另外一個是寫成下面這樣: <script src="js/require.js" defer async="true" ></script> //async屬性代表這個文件須要異步加載,避免網頁失去響應。IE不支持這個屬性,只支持defer,因此把defer也寫上。 /*加載require.js之後,下一步就要加載咱們本身的代碼了。假定咱們本身的代碼文件是main.js,也放在js目錄下面。那麼,只須要寫成下面這樣就好了:*/ <script src="js/require.js" data-main="js/main"></script> //data-main屬性的做用是,指定網頁程序的主模塊。在上例中,就是js目錄下面的main.js,這個文件會第一個被require.js加載。因爲require.js默認的文件後綴名是js,因此能夠把main.js簡寫成main。
主模塊的寫法
這時就要使用AMD規範定義的的require()函數。
require()異步加載moduleA,moduleB和moduleC,瀏覽器不會失去響應;它指定的回調函數,只有前面的模塊都加載成功後,纔會運行,解決了依賴性的問題。
一個實際的例子:假定主模塊依賴jquery、underscore和backbone這三個模塊,main.js就能夠這樣寫:
require(['jquery', 'underscore', 'backbone'], function ($, _, Backbone){ // some code here }); //require.js會先加載jQuery、underscore和backbone,而後再運行回調函數。主模塊的代碼就寫在回調函數中。
模塊的加載
默認狀況下,require.js假定這三個模塊與main.js在同一個目錄,文件名分別爲jquery.js,underscore.js和backbone.js,而後自動加載。
使用require.config()方法,咱們能夠對模塊的加載行爲進行自定義。require.config()就寫在主模塊(main.js)的頭部。參數就是一個對象,這個對象的paths屬性指定各個模塊的加載路徑。
require.config({ paths: { "jquery": "jquery.min", "underscore": "underscore.min", "backbone": "backbone.min" } });
若是這些模塊在其餘目錄,好比js/lib目錄,則有兩種寫法。一種是逐一指定路徑;另外一種則是直接改變基目錄(baseUrl) 。
require.config({//1 paths: { "jquery": "**lib/**jquery.min", "underscore": "**lib/**underscore.min", "backbone": "**lib/**backbone.min" } }); //2 require.config({ baseUrl: "js/lib", paths: { "jquery": "jquery.min", "underscore": "underscore.min", "backbone": "backbone.min" } }); //也能夠直接指定它的網址 require.config({ paths: { "jquery": "https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min" } });
AMD模塊的寫法
模塊必須採用特定的define()函數來定義。若是一個模塊不依賴其餘模塊,那麼能夠直接定義在define()函數之中。
假定如今有一個math.js文件,它定義了一個math模塊。那麼,math.js就要這樣寫:
define(function (){ var add = function (x,y){ return x+y; }; return { add: add }; }); //加載方法以下: require(['math'], function (math){ alert(math.add(1,1)); });
若是這個模塊還依賴其餘模塊,那麼define()函數的第一個參數,必須是一個數組,指明該模塊的依賴性。
define(['myLib'], function(myLib){ function foo(){ myLib.doSomething(); } return { foo : foo }; });//當require()函數加載上面這個模塊的時候,就會先加載myLib.js文件。