寫本文以整理我對 JavaScript 的一些理解,試將零散的知識歸總。此文非語法整理,內容偏中高級,如有紕漏或錯誤,請予以指正。
1. 對象模型
1.1. 數據類型
在 JavaScript 的語法層面,除了 undefined
和 null
一切皆對象,字面量也是對象,而 null
的類型也是對象:
'foo'.substring(1);
3.1415926.toFixed(2);
typeof null; // 'object'
JavaScript 語言中內置了一些對象用來輔助用戶編程,它們均是 函數對象
,如:
- Function
- Object
- String
- Number
解析引擎中創建了諸多內建類型,它們是實現 JavaScript 各類型的數據結構。
基本類型的字面量創建方式會直接調用解析引擎來創建 JavaScript 對象,它不是內置函數對象的實例:
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
歸納如下:
1.2. 函數對象
任何JS對象均需要由函數對象創建。函數對象是在普通對象的基礎上增加了內建的屬性 [[Call]]
和 [[Construct]]
,這一過程由解釋器完成,兩個屬性均指向解釋器的內建函數:[[Call]] 用于函數調用,使用操作符 ()
時執行;[[Construct]] 用于構造對象,使用操作符 new
時執行。
語法層面上,函數對象也是由其它函數對象(或自己)創建的,使用 function
關鍵字可以創建用戶自定義函數對象。最上游的對象是 Function
。
當對象被創建后,解釋器為對象增加 constructor
屬性指向創建它的函數對象。
1.3. 原型對象
原型對象通常由內置函數對象 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
為解析引擎創建的空對象。
1.4. 對象的關系
示例如下代碼:
function Foo(){};
var foo = new Foo();
再加上內置函數對象 String,其關系如下:
有如下規律:
- 所有函數對象的原型最終指向 Function.prototype ;
- 所有普通對象(除 Object.prototype)的原型最終指向 Object.prototype,而 Object.prototype 的原型為 null ;
- 所有 constructor 最終指向 Function ,包括它自己;
- 所有原型對象的 constructor 的 prototype 指向自己,普通對象不具備該特性。
2. 執行模型
函數生命周期包括:
2.1. 執行上下文
執行上下文(Execution Context)
是對可執行代碼的抽象,某特定時刻下它們是等價的。發生函數調用的時候,正在執行的上下文被中斷并將新的執行上下文壓入執行上下文棧中,調用結束后(return 或 throw Error)新的上下文從棧中彈出并繼續執行之前的上下文。棧底總是全局執行上下文
:
變量對象(Variable Object)是執行上下文中的一種數據結構,用來存儲:
- 變量
- 函數聲明
- 形參
變量對象為抽象概念,其實現分兩種情況:
一、全局執行上下文中的變量對象使用全局對象自身實現,因此全局變量可以通過相應的變量對象訪問到:
var foo = 'foo'
alert(window.foo);
二、函數執行上下文中的變量對象為活動對象(Activation Object),用戶代碼無法直接訪問它。
2.2. 函數執行過程
函數執行前會先為其創建執行環境:
示例以下代碼的執行過程:
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
2.2. 作用域
示例代碼如下:
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)全局對象,最終在全局對象中找到。
2.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 中所有的函數都是閉包。然而實踐中,以下函數才算是閉包:
- 即使創建它的上下文已經銷毀,它仍然存在
- 代碼中引用了自由變量
3. 其它
3.1. 不使用var聲明并不能創建全局變量
不使用 var 關鍵字創建的只是全局對象的屬性(全局執行上下文中的變量對象使用全局對象自身實現),它并不是一個變量。可以用如下代碼檢測區別:
alert(a); // undefined
alert(b); // Can't find variable: b
b = 10;
var a = 20;
3.2. 三種函數類型
- 函數聲明在程序級別或另一函數的函數體:
function foo() {
// ...
}
function globalFD() {
function innerFD() {}
}
- 函數表達式在表達式的位置:
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
}
參考資料: