JavaScript進階程式設計(第3版)學習筆記8 js函數(中)

來源:互聯網
上載者:User

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 swapColors(color1, 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

關於範圍和聲明提升,再看一個例子: 複製代碼 代碼如下:var name = 'linjisong';
function fn(){
console.info(name);//undefined
var name = 'oulinhai';
console.info(name);//oulinhai
}
fn();
console.info(name);//linjisong

這裡最不直觀的可能是第3行輸出undefined,因為在全域中已經定義過name了,不過按照上面解析的步驟去解析一次,就可以得出正確的結果了。另外強調一下,在ECMAScript中只有全域執行環境和函數執行環境,相應的也只有全域範圍和函數範圍,沒有塊範圍——雖然有塊語句。 複製代碼 代碼如下:function fn(){
var fnScope = 'a';

{
var blockScope = 'b';
blockScope += fnScope;
}
console.info(blockScope);//沒有塊範圍,所以可以在整個函數範圍內訪問blockScope
console.info(fnScope);
}
fn();//ba,a

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(){
var name = '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 name = 'oulinhai';
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的strict 模式下會設為null)。如果傳入的第一個參數是一個簡單類型,會將this設定為相應的簡單類型封裝對象。 複製代碼 代碼如下:var name = 'linjisong';
function fn(){
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.getName());//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的strict 模式下,不能對具有動態特性的函數屬性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--){
  total += arguments[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 bind(scope){
var that = this;
return function(){
that.apply(scope, arguments);
}
}
Function.prototype.bind = bind;

這裡涉及了一個閉包的概念,明天再繼續。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.