6、執行環境與作用域 (1)執行環境(execution context):所有的JavaScript程式碼都運作在一個執行環境中,當控制權轉移到JavaScript的可執行程式碼時,就進入了一個執行環境。活動的執行環境從邏輯上形成了一個棧,全域執行環境永遠是這個棧的棧底元素,棧頂元素就是目前正在運作的執行環境。每一個函數都有自己的執行環境,當執行流進入函數時,會將這個函數的執行環境壓入棧頂,函數執行完之後再將這個執行環境彈出,控制權回傳給先前的執行環境。
(2)變數物件(variable object):每一個執行環境都有一個與之對應的變數對象,而執行環境中定義的所有變數和函數就是保存在這個變數物件中。這個變數對像是後台實作中的一個對象,我們無法在程式碼中訪問,但這有助於我們理解執行環境和作用域相關概念。
(3)作用域鏈(scope chain):當程式碼在一個執行環境中運作時,會建立一個由變數物件組成的一個作用域鏈。這個鏈的前端,就是目前程式碼所在環境的變數對象,鏈的最末端,就是全域環境的變數對象。在一個執行環境中解析標識符時,會在當前執行環境相應的變量對像中搜索,找到就返回,沒有找到就沿著作用域鏈一級一級往上搜索直至全局環境的變量對象,如果一直未找到,就拋出引用異常。
(4)活動物件(activation object):如果一個執行環境是函數執行環境,也將變數物件稱為活動物件。活動物件在最開始只包含一個變量,即arguments物件(這個物件在全域環境的變數物件中不存在)。
這四個概念雖然有些抽象,但還是比較自然的,可以結合《JavaScript高級程式設計(第3版)》中的一個例子來細細體會一下:
// 進入全域作用域,建立全域變數物件
var color = " blue";
function changeColor(){
// 進入changeColor作用域,建立changeColor對應變數物件
var anotherColor = "red";
function swapors(111 , color2){
// 進入到swapColors作用域,創建swapColors相應變量對象
var tempColor = anotherColor;
anotherColor = color;
color = tempColor;
/*
* swapColors作用域內可以存取的物件有:
* 全域變數物件的color,changeColor
* changeColor函數對應變數物件的anotherColor、swapColors
* swapColors函數對應變數物件的tempColor
*/
}
swapColors('white');
/*
* changeColor作用域內可以存取的物件有:
* 全域變數物件的color,changeColor
* changeColor函數函數變數物件的anotherColor、swapColors
*/
}
changeColor();
/*
* 全域作用域內可存取的物件有:
* 全域變數的color,changeColor
*/
這裡的整個過程是: (1)進入全域環境,建立全域變數對象,將全域環境壓入堆疊頂(這裡也是堆疊底)。根據前面的關於宣告提升的結論,這裡建立全域變數物件可能的一個過程是,先建立全域變數對象,然後處理函數宣告設定屬性changeColor為對應函數,再處理變數宣告設定屬性color為undefined。
(2)執行全域環境中的程式碼。先執行color變數初始化,賦值為'blue',再呼叫changeColor()函數。
(3)呼叫changeColor()函數,進入到changeColor函數執行環境,建立這個環境對應的變數物件(也就是活動物件),將這個環境壓入堆疊頂部。建立活動對象可能的一個過程是,先建立活動對象,處理內部函數宣告設定屬性swapColors為對應函數,處理函數參數建立活動對象的屬性arguments對象,處理內部變數宣告設定屬性anotherColor為undefined。
(4)執行changeColor()函數程式碼。先執行anotherColor初始化為'red',再呼叫swapColors()函數。
(5)呼叫swapColors()函數,進入到swapColors函數執行環境,建立對應的變數物件(活動物件),將swapColors執行環境壓入堆疊頂。這裡創建活動對象可能的一個過程是,先創建活動對象,處理函數參數,將形式參數作為活動對象的屬性並賦值為undefined,創建活動對象的屬性arguments對象,並根據實際參數初始化形式參數和arguments對應的值和屬性(將屬性color1和arguments[0]初始化為'white',由於沒有第二個實際參數,所以color2的值為undefined,而arguments的長度只為1了),處理完函數參數之後,再處理函數內部變數聲明,將tempColor作為活動物件的屬性並賦值為undefined。
(6)執行swapColors()函數程式碼。先將tempColor初始化賦值,然後實作值交換功能(這裡color和anotherColor的值都是沿著作用域鏈才讀取到的)。
(7)swapColors()函數程式碼執行完之後,返回undefined,將對應的執行環境彈出堆疊並銷毀(注意,這裡會銷毀執行環境,但是執行環境對應的活動物件不一定會被銷毀),目前執行環境恢復成changeColor()函數的執行環境。隨著swapColor()函數執行完並返回,changeColor()也就執行完了,同樣返回undefined,並將changeColor()函數的執行環境彈出堆疊並銷毀,當前執行環境恢復成全域環境。整個處理過程結束,全域環境直到頁面退出再銷毀。
作用域鏈也解釋了為什麼函數可以在內部遞歸呼叫自身:函數名稱是函數定義所在執行環境對應變數物件的屬性,然後在函數內部執行環境中,就可以沿著作用域鏈向外上溯一層訪問函數名指向的函數物件了。如果在函數內部將函數名稱指向了一個新函數,遞歸呼叫時就會不正確了:
function fn(num){
if(1 == num){
return 1;
}else{
fn = function(){
return 0;
};
return num * fn(num - 1);
}
}
console.info(fn(5));//0
關於作用域和宣告提升,再看一個例子:
程式碼如下:
程式碼如下:
程式碼如下:
console.info(name);//linjisong
這裡最不直觀的可能是第3行輸出undefined,因為在全域中已經定義過name了,不過按照上面解析的步驟去解析一次,就可以得到正確的結果了。另外強調一下,在ECMAScript中只有全域執行環境和函數執行環境,對應的也只有全域作用域和函數作用域,沒有區塊作用域-雖然有區塊語句。
複製程式碼
程式碼如下:
function fn(){
{
var blockScope = 'b'; blockScope = fnScope; } console.info(blockScope);//沒有區塊作用域,所以可以在整個函數作用域內存取blockScope console.info(fnScope); } fn();//ba,a console.info(blockScope);//ReferenceError,console.info(blockScope);//ReferenceError,console.info(blockScope);//ReferenceError,console.info(blockScope);//ReferenceError,函數作用域外,不能存取內部定義的變數console.info(fnScope);//ReferenceError
對於作用域鏈,也可以使用with、try-catch語句的catch區塊來延長:
•使用with(obj){}語句時,將obj物件加入目前作用域鏈的最前端。
•使用try{}catch(error){}語句時,將error物件加入目前作用域鏈的最前端。
插了一段較為抽象的概念,希望不至於影響整個閱讀的流暢,事實上,我在這裡還悄悄的繞過了一個稱為“閉包”的概念,關於函數與閉包,在下篇文章中再詳細敘述。
7、函數內部物件與this
對於物件導向語言的使用者來說,this實在是再熟悉不過了,不就是指向建構子新創建的對象嗎!不過,在ECMAScript中,且別掉以輕心,事情沒有那麼簡單,雖然在使用new操作符調用函數的情況下,this也的確是指向新創建的對象,但這只是指定this對象值的一種方式而已,還有更多的方式可以指定this物件的值,換句話說,this是動態的,是可以由我們自己自由指定的。
(1)全域環境中的this
在全域環境中,this指向全域物件本身,在瀏覽器中也就是window,這裡也可以把全域環境中的this理解為全域執行環境對應的變數對象,在全域環境定義的變數和函數都是這個變數物件的屬性:
var vo = 'a';
vo2 = 'b';
function fn(){
return 'fn';
}
console.info(this === window);//true
console.info(this.vo);//a
console.info(this.vo2);//b
console .info(this.fn());//fn
如果在自訂函數中要引用全域對象,雖然可以直接使用window,但更好的方式則是將全域物件作為參數傳入函數,這是在JS函式庫中非常通用的一種方式:
(function(global){
console.info(global === window);//在內部可以用global取代window了
})(this);
這種方式相容性更好(ECMAScript的實作中全域物件未必都是window),在壓縮時,也可以將global簡化為g,而不用使用window了。
(2)函數內部屬性this
在函數環境中,this是一個內部屬性對象,可以理解成函數對應的活動對象的一個屬性,而這個內部屬性的值是動態的。那this值是怎麼動態決定的呢?
•使用new呼叫時,函數也稱為建構函數,這個時候函數內部的this被指定為新建立的物件。
function fn(){
function fn(){
varname = ' oulinhai';//函數對應的活動對象的屬性
this.name = 'linjisong';//當使用new呼叫函數時,將this指定為新創建對象,也就是給新創建對象添加屬性
}
var person = new fn();
console.info(person.name);//linjisong
var arr = [fn];
console.info(arr[ 0]());//undefined
需要注意區分函數執行環境中定義的屬性(也即活動物件的屬性)和this物件的屬性,在使用陣列元素方式呼叫函數時,函數內部this指向數組本身,因此上例最後輸出undefined。
•作為一般函數呼叫時,this指向全域物件。
•當物件的方法呼叫時,this指向呼叫這個方法的物件。
看下面的範例: 複製程式碼
程式碼如下:
var🎜> var person = {
name:'linjisong',
getName:function(){
return this.name;
}
};
console. info(person.getName());//linjisong
var getName = person.getName; console.info(getName());//oulinhai
這裡函數物件本身是匿名的,是作為person對象的一個屬性,當作為對象屬性調用時,this指向了對象,當把這個函數賦給另一個函數然後調用時,是作為一般函數調用的,this指向了全域物件。這個例子充分說明了“函數作為對象的方法調用時內部屬性this指向這個調用對象,函數作為一般函數調用時內部屬性this指向全局對象”,也說明了this的指定是動態的,是在調用時指定的,而不管函數是單獨定義的還是作為物件方法定義的。也正是因為函數作為對象的方法呼叫時this指向這個呼叫對象,所以在函數內部返回this時才能夠延續呼叫對象的下一個方法-也就是鍊式操作(jQuery的一大特色)。
•使用apply()、call()或bind()呼叫函數時,this指向第一個參數物件。如果沒有傳入參數或傳入的是null和undefined,this指向全域物件(在ES5的嚴格模式下會設為null)。如果傳入的第一個參數是一個簡單類型,會將this設定為對應的簡單類型包裝物件。
var name = 'function; (){
return this.name;
}
var person = {
name:'oulinhai',
getName:fn
};
var person2 = {name :'hujinxing'};
var person3 = {name:'huanglanxue'};
console.info(fn());//linjisong,一般函數調用,內部屬性this指向全域對象,因此this. name返回linjisong
console.info(person.getName());//oulinhai,作為對象方法調用,this指向這個對象,因此這裡返回person.name
console.info(fn.apply(person2) );//hujinxing,使用apply、call或bind呼叫函數,執行傳入的第一個參數對象,因此返回person2.name
console.info(fn.call(person2));//hujinxing
var newFn = fn.bind(person3);//ES5中新增方法,會建立一個新函數實例返回,內部this值被指定為傳入的參數物件
console.info(newFn()); //huanglanxue
上面範例中列出的都是一些常見情況,沒有列出第一個參數為null或undefined的情況,有興趣的朋友可以自行測試。關於this值的確定,在原文中還有一個例子:
var name = 'The Window';
var object = {
name : 'My Object',
getName:function(){
return this.name;
},
getNameFunc:function(){
return function(){
return this.name;
}
}
};
console.info(object.Name ());//My Object
console.info((object.getName)());//My Object
console.info((object.getName = object.getName)());// The Window
console.info(object.getNameFunc()());//The Window
第1个是正常输出,第2个(object.getName)与object.getName的效果是相同的,而第3个(object.getName=object.getName)最终返回的是函数对象本身,也就是说第3个会作为一般函数来调用,第4个则先是调用getNameFunc这个方法,返回一个函数,然后再调用这个函数,也是作为一般函数来调用。
8、函数属性和方法 函数是一个对象,因此也可以有自己的属性和方法。不过函数属性和方法与函数内部属性很容易混淆,既然容易混淆,就把它们放一起对照着看,就好比一对双胞胎,不对照着看,不熟悉的人是区分不了的。
先从概念上来区分一下:
(1)函数内部属性:可以理解为函数相应的活动对象的属性,是只能从函数体内部访问的属性,函数每一次被调用,都会被重新指定,具有动态性。
(2)函数属性和方法:这是函数作为对象所具有的特性,只要函数一定义,函数对象就被创建,相应的属性和方法就可以访问,并且除非你在代码中明确赋为另一个值,否则它们的值不会改变,因而具有静态性。有一个例外属性caller,表示调用当前函数的函数,也是在函数被调用时动态指定,在《JavaScript高级程序设计(第3版)》中也因此将caller属性和函数内部属性arguments、this一起讲解,事实上,在ES5的严格模式下,不能对具有动态特性的函数属性caller赋值。
光从概念上区分是非常抽象的,也不是那么容易理解,再把这些属性列在一起比较一下(没有列入一些非标准的属性,如name):
类别 |
名称 |
继承性 |
说明 |
备注 |
函数内部属性 |
this |
- |
函数据以执行的环境对象 |
和一般面向对象语言有很大区别 |
arguments |
- |
表示函数实际参数的类数组对象
arguments本身也有自己的属性:length、callee和caller
|
1、length属性表示实际接收到的参数个数
2、callee属性指向函数对象本身,即有:
fn.arguments.callee === fn
3、caller属性主要和函数的caller相区分,值永远都是undefined
|
函数属性 |
caller |
否 |
调用当前函数的函数 |
虽然函数一定义就可访问,但是不在函数体内访问时永远为null,在函数体内访问时返回调用当前函数的函数,在全局作用域中调用函数也会返回null |
length |
否 |
函数形式参数的长度 |
就是定义函数时命名的参数个数 |
prototype |
否 |
函数原型对象 |
原型对象是ECMAScript实现继承的基础 |
constructor |
是 |
继承自Object,表示创建函数实例的函数,也就是Function() |
值永远是Function,也就是内置的函数Function() |
函数方法 |
apply |
否 |
调用函数自身,以(类)数组方式接受参数 |
这三个方法主要作用是动态绑定函数内部属性this
1、apply和call在绑定之后会马上执行
2、bind在绑定之后可以在需要的时候再调用执行
|
call |
否 |
调用函数自身,以列举方式接受参数 |
bind |
否 |
绑定函数作用域,ES5中新增 |
toLocalString |
覆盖 |
覆盖了Object类型中的方法,返回函数体
不同浏览器实现返回可能不同,可能返回原始代码,也可能返回去掉注释后的代码
|
toString |
覆盖 |
valueOf |
覆盖 |
hasOwnProperty |
是 |
直接继承自Object类型的方法,用法同Object |
propertyIsEnumerable |
是 |
isPropertyOf |
是 |
函數屬性和方法,除了從Object繼承而來的屬性和方法,也包括函數本身特有的屬性和方法,用的最多的方法自然就是上一小節說的apply()、call(),這兩個方法都是用來設定函數內部屬性this從而擴展函數作用域的,只不過apply()擴展函數作用域時是以(類)數組方式接受函數的參數,而call()擴展函數作用域時需要將函數參數一一列舉出來傳遞,看下面的範例:
function sum(){
var total = 0,
l = arguments.length ;
for(l) l-- --){ -1];
}
return total;
}
console.info(sum.apply(null,[1,2,3,4]));//10 console.info(sum.call(null,1,2,3,4));//10
不過需要強調的是:apply和call的主要作用還是在於擴展函數作用域。 apply和call在擴展作用域時會馬上呼叫函數,這使得應用中有了很大限制,因此在ES5中新增加了一個bind()函數,這個函數也用於擴展作用域,但是可以不用馬上執行函數,它傳回一個函數實例,將傳入給它的第一個參數作為原函數的作用域。它的一個可能的實作如下:
function bindscope ){
var that = this;
return function(){
that.apply(scope, arguments);
}
}
Function.prototype.bind = bind;
這裡涉及了一個閉包的概念,明天再繼續。