伯樂線上注:本文來自文章作者 @freestyle21 的投稿。
前言
在JavaScript中,作用域、上下文、閉包、函式等算是精華中的精華了。對於初級JSer來說,是進階必備。對於前端攻城師來說,只有靜下心來,理解了這些精華,才能寫出優雅的程式碼。
本文旨在總結容易忘記的重要知識,不會講基本的概念。如果對基本知識不太熟悉,就去翻下《Javascript權威指南》吧~
參考文章如下(建議讀者朋友用chrome看這些文章吧,不然的話會錯過很多精彩哦~):
http://dmitrysoshnikov.com/ecmascript/chapter-1-execution-contexts/
http://benalman.com/news/2010/11/immediately-invoked-function-expression/
http://dmitrysoshnikov.com/ecmascript/javascript-the-core/
語言特性
函式表示式
先看程式碼段:
1 2 3 4 5 6 |
var f = function foo(){ return typeof foo; // foo是在內部作用域內有效 }; // foo在外部用於是不可見的 typeof foo; // "undefined" f(); // "function" |
json
很多JavaScript開發人員都錯誤地把JavaScript物件字面量(Object Literals)稱為JSON物件(JSON Objects)。 JSON是設計成描述資料交換格式的,它也有自己的語法,這個語法是JavaScript的一個子集。
{ “prop”: “val” } 這樣的宣告有可能是JavaScript物件字面量,也有可能是JSON字串,取決於什麼上下文使用它。如果是用在string上下文(用單引號或雙引 號引住,或者從text檔案讀取)的話,那它就是JSON字串,如果是用在物件字面量上下文中,那它就是物件字面量。
1 2 3 4 5 |
// 這是JSON字串 var foo = '{ "prop": "val" }'; // 這是物件字面量 var bar = { "prop": "val" }; |
原型
1 2 3 4 5 6 7 8 9 10 11 |
function Animal (){ // ... } function cat (){ // ... } cat.prototype = new Animal();//這種方式會繼承建構函式裡面的。 cat.prototype = Animal.prototype;//這種方式不會繼承建構函式裡面的。 //還有一個重要的細節需要注意的就是一定要維護自己的原型鏈,新手總會忘記這個! cat.prototype.constructor = cat; |
1 2 3 4 5 6 7 8 |
function A() {} A.prototype = { x: 10 }; var a = new A(); alert(a.x); // 10 alert(a.constructor === A); // false! |
1 2 3 4 5 6 7 8 9 |
function A() {} A.prototype = { constructor: A, x: 10 }; var a = new A(); alert(a.x); // 10 alert(a.constructor === A); // true |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
function A() {} A.prototype.x = 10; var a = new A(); alert(a.x); // 10 A.prototype = { constructor: A, x: 20 y: 30 }; // 物件a是通過隱式的[[Prototype]]引用從原油的prototype上獲取的值 alert(a.x); // 10 alert(a.y) // undefined var b = new A(); // 但新物件是從新原型上獲取的值 alert(b.x); // 20 alert(b.y) // 30 |
變數物件
在函式執行上下文中,VO(variable object)是不能直接訪問的,此時由活動物件(activation object)扮演VO的角色。 活動物件是在進入函式上下文時刻被建立的,它通過函式的arguments屬性初始化。arguments屬性的值是Arguments物件:
1 2 3 4 5 6 7 8 9 10 |
function foo(x, y, z) { // 宣告的函式引數數量arguments (x, y, z) alert(foo.length); // 3 // 真正傳進來的引數個數(only x, y) alert(arguments.length); // 2 // 引數的callee是函式自身 alert(arguments.callee === foo); // true } |
- 所有函式宣告(FunctionDeclaration, FD);
- 所有變數宣告(var, VariableDeclaration);
另一個經典例子:
1 2 3 4 5 6 7 8 9 10 |
alert(x); // function var x = 10; alert(x); // 10 x = 20; function x() {}; alert(x); // 20 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
a = 10; alert(window.a); // 10 alert(delete a); // true alert(window.a); // undefined var b = 20; alert(window.b); // 20 alert(delete b); // false alert(window.b); // still 20。b is variable,not property! var a = 10; // 全域性上下文中的變數 (function () { var b = 20; // function上下文中的區域性變數 })(); alert(a); // 10 alert(b); // 全域性變數 "b" 沒有宣告. |
this
在一個函式上下文中,this由呼叫者提供,由呼叫函式的方式來決定。如果呼叫括號()的左邊是引用型別的值,this將設為引用型別值 的base物件(base object),在其他情況下(與引用型別不同的任何其它屬性),這個值為null。不過,實際不存在this的值為null的情況,因為當this的值 為null的時候,其值會被隱式轉換為全域性物件。
1 2 3 |
(function () { alert(this); // null => global })(); |
1 2 3 4 5 6 7 8 9 10 11 12 |
var foo = { bar: function () { alert(this); } }; foo.bar(); // Reference, OK => foo (foo.bar)(); // Reference, OK => foo (foo.bar = foo.bar)(); // global (false || foo.bar)(); // global (foo.bar, foo.bar)(); // global |
- 第一個例子很明顯———明顯的引用型別,結果是,this為base物件,即foo。
- 在第二個例子中,組運算子並不適用,想想上面提到的,從引用型別中獲得一個物件真正的值的方法,如GetValue。相應的,在組運算的返回中———我們得到仍是一個引用型別。這就是this值為什麼再次設為base物件,即foo。
- 第三個例子中,與組運算子不同,賦值運算子呼叫了GetValue方法。返回的結果是函式物件(但不是引用型別),這意味著this設為null,結果是global物件。
- 第四個和第五個也是一樣——逗號運算子和邏輯運算子(OR)呼叫了GetValue 方法,相應地,我們失去了引用而得到了函式。並再次設為global。
正如我們知道的,區域性變數、內部函式、形式引數儲存在給定函式的啟用物件中。
1 2 3 4 5 6 |
function foo() { function bar() { alert(this); // global } bar(); // the same as AO.bar() } |
作用域鏈
通過函建構函式建立的函式的scope屬性總是唯一的全域性物件。
一個重要的例外,它涉及到通過函式建構函式建立的函式。
1 2 3 4 5 6 7 8 9 10 11 12 |
var x = 10; function foo() { var y = 20; function barFD() { // 函式宣告 alert(x); alert(y); } var barFn = Function('alert(x); alert(y);'); barFD(); // 10, 20 barFn(); // 10, "y" is not defined } foo(); |
1 2 3 4 5 6 7 8 9 10 11 12 |
var x = 10, y = 10; with ({x: 20}) { var x = 30, y = 30; //這裡的 x = 30 覆蓋了x = 20; alert(x); // 30 alert(y); // 30 } alert(x); // 10 alert(y); // 30 |
- x = 10, y = 10;
- 物件{x:20}新增到作用域的前端;
- 在with內部,遇到了var宣告,當然什麼也沒建立,因為在進入上下文時,所有變數已被解析新增;
- 在第二步中,僅修改變數“x”,實際上物件中的“x”現在被解析,並新增到作用域鏈的最前端,“x”為20,變為30;
- 同樣也有變數物件“y”的修改,被解析後其值也相應的由10變為30;
- 此外,在with宣告完成後,它的特定物件從作用域鏈中移除(已改變的變數“x”--30也從那個物件中移除),即作用域鏈的結構恢復到with得到加強以前的狀態。
- 在最後兩個alert中,當前變數物件的“x”保持同一,“y”的值現在等於30,在with宣告執行中已發生改變。
函式
關於圓括號的問題
讓我們看下這個問題:‘ 為何在函式建立後的立即呼叫中必須用圓括號來包圍它?’,答案就是:表示式句子的限制就是這樣的。
按照標準,表示式語句不能以一個大括號 { 開始是因為他很難與程式碼塊區分,同樣,他也不能以函式關鍵字開始,因為很難與函式宣告進行區分。即,所以,如果我們定義一個立即執行的函式,在其建立後立即按以下方式呼叫:
1 2 3 4 5 6 7 8 9 |
function () { ... }(); // 即便有名稱 function foo() { ... }(); |
1 2 3 4 5 6 |
// "foo" 是一個函式宣告,在進入上下文的時候建立 alert(foo); // 函式 function foo(x) { alert(x); }(1); // 這只是一個分組操作符,不是函式呼叫! foo(10); // 這才是一個真正的函式呼叫,結果是10 |
1 2 3 |
(function foo(x) { alert(x); })(1); // 這才是呼叫,不是分組操作符 |
1 2 3 4 5 6 7 |
var foo = { bar: function (x) { return x % 2 != 0 ? 'yes' : 'no'; }(1) }; alert(foo.bar); // 'yes' |
1 2 3 |
因此,”關於圓括號”問題完整的答案如下: 當函式不在表示式的位置的時候,分組操作符圓括號是必須的——也就是手工將函式轉化成FE。 如果解析器知道它處理的是FE,就沒必要用圓括號。 |
1 2 3 4 5 6 7 |
function testFn() { var localVar = 10;//對於innerFn函式來說,localVar就屬於自由變數。 function innerFn(innerParam) { alert(innerParam + localVar); } return innerFn; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
var z = 10; function foo() { alert(z); } foo(); // 10 – 使用靜態和動態作用域的時候 (function () { var z = 20; foo(); // 10 – 使用靜態作用域, 20 – 使用動態作用域 })(); // 將foo作為引數的時候是一樣的 (function (funArg) { var z = 30; funArg(); // 10 – 靜態作用域, 30 – 動態作用域 })(foo); |
* 在程式碼中引用了自由變數
最後:
ECMAScript是一種面嚮物件語言,支援基於原型的委託式繼承。