分享一篇我在2015年末作的總結筆記。本文整理了我對 JavaScript 的一些理解,試將零散的知識歸總。此文非語法整理,內容偏中高級,若有紕漏或錯誤,請予以指正。編程
在 JavaScript 的語法層面,除了 undefined
和 null
一切皆對象,字面量也是對象,而 null
的類型也是對象:數組
'foo'.substring(1); 3.1415926.toFixed(2); typeof null; // 'object'
JavaScript 語言中內置了一些對象用來輔助用戶編程,它們均是 函數對象
,如:數據結構
解析引擎中建立了諸多內建類型,它們是實現 JavaScript 各種型的數據結構。閉包
基本類型的字面量建立方式會直接調用解析引擎來建立 JavaScript 對象,它不是內置函數對象的實例:ecmascript
var foo = 'foo'; console.log(foo instanceof String); // false foo = new String('foo'); console.log(foo instanceof String); // true
對象(這裏指語法層面的對象)、正則、數組等的字面量建立方式會調用內置函數對象來建立實例:函數
var foo = {}; console.log(foo instanceof Object); // true foo = new Object(); console.log(foo instanceof Object); // true
概括以下:優化
任何JS對象均須要由函數對象建立。函數對象是在普通對象的基礎上增長了內建的屬性 [[Call]]
和 [[Construct]]
,這一過程由解釋器完成,兩個屬性均指向解釋器的內建函數:[[Call]] 用於函數調用,使用操做符 ()
時執行;[[Construct]] 用於構造對象,使用操做符 new
時執行。this
語法層面上,函數對象也是由其它函數對象(或本身)建立的,使用 function
關鍵字能夠建立用戶自定義函數對象。最上游的對象是 Function
。spa
當對象被建立後,解釋器爲對象增長 constructor
屬性指向建立它的函數對象。prototype
原型對象一般由內置函數對象 Object
建立,它一般是一個普通對象,但也多是函數對象。
任何對象都有內建屬性 [[Prototype]]
用來指向其原型對象,有些解釋器(如V8)會將其開放爲 __proto__
屬性供用戶代碼調用。函數對象有開放屬性 prototype
,用來表示經過函數對象構建的對象的原型。
如下條件老是爲 true :
函數對象.prototype === 該函數對象建立的對象.__proto__
示例以下代碼的原型關係:
function Foo(){ this.foo = 'foo'; }; Foo.prototype.bar = 'bar'; var f1 = new Foo(); var f2 = new Foo();
對象指向原型對象的層層鏈條構成原型鏈,對象查找屬性時沿着原型鏈向上遊找。
一般狀況下,Function.prototype
爲解析引擎建立的空函數,Object.prototype
爲解析引擎建立的空對象。
示例以下代碼:
function Foo(){}; var foo = new Foo();
再加上內置函數對象 String,其關係以下:
有以下規律:
函數生命週期包括:
執行上下文(Execution Context)
是對可執行代碼的抽象,某特定時刻下它們是等價的。發生函數調用的時候,正在執行的上下文被中斷並將新的執行上下文壓入執行上下文棧中,調用結束後(return 或 throw Error)新的上下文從棧中彈出並繼續執行以前的上下文。棧底老是全局執行上下文
:
變量對象(Variable Object)是執行上下文中的一種數據結構,用來存儲:
變量對象爲抽象概念,其實現分兩種狀況:
1、全局執行上下文中的變量對象使用全局對象自身實現,所以全局變量能夠經過相應的變量對象訪問到:
var foo = 'foo' alert(window.foo);
2、函數執行上下文中的變量對象爲活動對象(Activation Object),用戶代碼沒法直接訪問它。
函數執行前會先爲其建立執行環境:
示例如下代碼的執行過程:
function foo(foo1, foo2) { var foo3 = 'foo3'; var foo4 = function () {}; this.foo5 = 'foo5'; function foo6() {}; foo6(); } foo('foo1', 'foo2', 'more');
1) 建立執行環境
該過程重點是建立 活動對象
的命名屬性:
2) 依次執行代碼
理解了函數執行過程即可以解釋局部變量的初始化時機問題:
var foo = 'global'; function bar() { alert(foo); // undefined var foo = 'local'; } bar();
同時也解釋了兩種函數聲明方式的區別:
foo(); // foo bar(); // TypeError: bar is not a function. function foo() { console.log('foo'); } var bar = function () { console.log('bar'); };
根據活動對象的屬性填充順序,也能夠解釋:
alert(x); // function var x = 10; alert(x); // 10 x = 20; function x() {}; alert(x); // 20
示例代碼以下:
var x = 1; function foo() { var y = 2; function bar() { var z = 3; alert(x + y + z); } bar(); } foo(); // 6
其做用域相關的屬性建立過程以下:
其中函數對象的內部屬性 [[Scope]]
在某些解釋器中實現爲 __parent__
並開放給用戶代碼。執行上下文中的 Scope
屬性構成 做用域鏈,其實現未必像圖中所示使用數組,也可使用鏈表等數據結構,ECMAScript 規範對解釋器的實現機制未作規定。
變量查找時沿着做用域鏈向上遊查找。例如在函數 bar 中查找 x 時,會依次查找:1)bar的活動對象;2)foo的活動對象;3)全局對象,最終在全局對象中找到。
ECMAScript 使用靜態詞法做用域:當函數對象建立時,其上層上下文數據(變量對象)保存在內部屬性 [[Scope]] 中,即函數在建立的時候就保存了上層上下文的做用域鏈,無論函數會否被調用。所以全部的函數都是一個閉包(除了 Function 構造器建立的函數)。不過,出於優化目的,當函數不使用自由變量時,引擎實現可能並不保存上層做用域鏈。
自由變量是在函數內使用的一種變量:它既不是函數的參數,也不是其局部變量。
[[Scope]] 屬性是指向變量對象的引用,同一上下文建立的多個閉包共用該變量對象。所以,某個閉包對其變量的修改會影響到其餘閉包對其變量的讀取:
var fooClosure; var barClosure; function foo() { var x = 1; fooClosure = function () { return ++x; }; barClosure = function () { return --x; }; } foo(); alert(fooClosure()); // 2 alert(barClosure()); // 1
函數執行時,變量對象的屬性變化以下:
能夠解釋此常犯錯的狀況:
var data = []; for (var k = 0; k < 3; k++) { data[k] = function () { alert(k); }; } data[0](); // 3, 而不是 0 data[1](); // 3, 而不是 1 data[2](); // 3, 而不是 2
經過建立多個變量對象(方式一)或使用函數對象的屬性(方式二)能夠解決此問題:
// 方式一 var data = []; for (var k = 0; k < 3; k++) { data[k] = (function (x) { return function () { alert(x); }; })(k); } // 方式二 var data = []; for (var k = 0; k < 3; k++) { (data[k] = function () { alert(arguments.callee.x); }).x = k; }
從理論角度講,ECMAScript 中全部的函數都是閉包。然而實踐中,如下函數纔算是閉包:
不使用 var 關鍵字建立的只是全局對象的屬性(全局執行上下文中的變量對象使用全局對象自身實現),它並非一個變量。能夠用以下代碼檢測區別:
alert(a); // undefined alert(b); // Can't find variable: b b = 10; var a = 20;
1) 函數聲明在程序級別或另外一函數的函數體:
function foo() { // ... } function globalFD() { function innerFD() {} }
2) 函數表達式在表達式的位置:
var foo = function () { // ... }; (function foo() {}); [function foo() {}]; 1, function foo() {}; var bar = (foo % 2 == 0 ? function () { alert(0); } : function () { alert(1); } ); // bar 爲函數表達式: foo(function bar() { alert('foo.bar'); });
函數表達式的做用是避免對變量對象形成污染。
3)Function構造器的 [[Scope]] 屬性中只包含全局對象:
var x = 10; function foo() { var x = 20; var y = 30; var bar = new Function('alert(x); alert(y);'); bar(); // 10, "y" is not defined }
參考資料: