函式 及 物件
在 JavaScript 程式裡, Object (物件)是一個容器,Function(函式)就好比是靈魂(程序),主宰及運作整個生命體的就是程式.物件是由特性(property)所形成的個體,特性以 關鍵字:值 (key:value) 的方式配對成雙,相互用逗點隔開, 大括號 { } 框住全部特性形成物件.而物件要取用特性時用名稱加句點語法直接引用.也只有物件及原生體(name of the primitive values:詳見後記)可以用句點語法取用特性.對於函式而言,他的名稱同時也會被賦予成一個物件.而函式是用小寫關鍵字function來定義.我們可以隨便定義一個函式就可明瞭, 如以下的空心函式 :function my( ) { } // 先宣告一段函式, 名稱是 my, 無內容.
my.name // 'my', name:'my' 這是一個 my 物件的一個特性
任何一個函式的最後必定會傳回(return)一個值(value),即使沒有寫在函式裡頭,Javsscript仍然會傳回值叫作undefined,函式的傳回值簡稱函數值(function value).因此當定義完一個函式,裡頭有三個意義: 函式物件, 函數值, 函式, 以 function my( ) { } 這個例子來說 my 是函式物件, my( ) 是函數值,整個敘述 my( ) { } 才是函式, 如果以中文語意來看,函式名稱 my 到底是物件還是函式或是函數值,見仁見智,可以說既是物件也是函式又可能是函數值,很容易讓人混淆.有個新名詞叫作方法(method),他代表著函式的功能,因此以下文章引用'函式方法把它當成函式功能來看,如果是'函式體',那指的就是'函式物件'.單獨稱'函數值'時才是函式的回傳值.一定要區別,否則會搞混.最後要注意的是只有物件才能擔任繼承者或是被繼承的角色,比較特別的是原生值雖非物件但也會繼承到一些特性(後記再加以說明).函式方法一旦被繼承或是自行創造,也在物件中扮演著特性的一部份,實際上函式方法最後所得到的函數值才是物件的特性.因此函式體是可以繼承或被繼承的,而原生體的數值(primitive value)是一個常數(immutable)是無法改變的.
所有函式體(注意是物件)全部都繼承來自Function.prototype,實際上Function.prototype就是所有函式體的始祖簡稱函式祖(注意是物件).函式祖有著prototype,bind,call,apply,name ...等等的特性.因此任何一個函式只要宣告完成,函式體(注意是物件)一定會繼承到prototype,bind,call,apply,name ...等等這些隱藏的特性.定義物件可以簡單用 {key:value, ... }方式直接定義產生新的物件, 而且用中文來描述特性也沒問題:
a={
'住址':'台灣',
'性別':'男生' ,
'姓名':'中文名',
};
console.log(a.住址+a.性別+a.姓名);
當物件指定一個名稱後,它只是該物件的一個別名, 所有利用別名來改變特性都會直接影響到物件實體的特性.例如:
myo = {x:3, y:4} ;
myn = myo; // an alias, different name but same object
myn.x = 1 // myo = { x:1, y:4}
myo.y = 2 // myn = { x:1, y:2}
myn === myo // true
上述 myn 是 myo 的一個別名, 物件實體只有一個. 運算符號 '===' 可以用來確認實體是否相同.透過 Object( ) 函式也可以用來產生新物件,例如:
myo = Object({x:3,y:4})
或是:
myo = new Object({x:3,y:4}) // alias, same object but different name
Object(myo) === new Object(myo) // Identical operation
myo === Object(myo) // using alias, return alias
myo === Object(myo) // using alias, return alias
上面可以看到比較特殊的, 當傳進去的是別名(alias), Object( ) 及 new Object( )並不會另外產生一個實體,而是直接回傳物件的別名, 有點意外!
{x:1} === Object({x:1}) // false. differenct object
{x:1} === new Object({x:1}) // false. different object
Object({x:1}) === new Object({x:1}) // false. different object
由上面可以知道當用常數物件時, Object 及 new Object ( ) 會產生新的物件, 但如果是用變數,他只是一個別名,對別名而言,就直接回傳.
透過Object.create( )除了產生時實體外,同時也繼承了物件的特性, 例如要繼承函式祖(注意是物件)的特性,可以這樣做:
上述物件 notFunction 利用Object.create( )只有繼承到函式體的特性,並沒有賦予函數功(就是說不能用 notFunction( ) 去執行一個命令, 或是呼叫 call 綁定物件,因此notFunction.call( )是不允許的.這就是為何要將函式體與函式方法分開的原因.如果要創造函式,還真可以利用原生函式Function( )來完成,它完全繼承了函式的特性.例如輸入兩個參數相加後回傳的函式:
a= Function('a,b','return a+b');
a(3,5)
創造物件時,可以用關鍵字 new 函式方法(注意是函式功能), 這種特殊的函式方法簡稱建構式(注意它是函式).new 建構式除了可以繼承函式祖(Function.prototype)外,並且用函式本身程序可以創造出新的特性,(再三要強調的一點是new建構式產生出來的是新物件或原生體而不是函式).如以下範例:
a= Function('a,b','return a+b');
a(3,5)
創造物件時,可以用關鍵字 new 函式方法(注意是函式功能), 這種特殊的函式方法簡稱建構式(注意它是函式).new 建構式除了可以繼承函式祖(Function.prototype)外,並且用函式本身程序可以創造出新的特性,(再三要強調的一點是new建構式產生出來的是新物件或原生體而不是函式).如以下範例:
function fobj() { this.x=3; this.y=4;return 3; }
myn = new fobj()
上述的函數值(注意是 fobj 的傳回值 3)就變的沒多大的意義了. myn除了繼承了函式祖外,也用程序創造了 x:3 及 y:4 的特性.上述 'this' 是一個特殊的字眼(詳件後記),他是一個物件,要小心使用,this會因出現的位置不同,而解釋成不同的物件,函式是可以透過綁定(bind, call, apply)一個物件之後再讓程序(program)運作的.當然被綁定的物件必定要有一些規範(protocol), 以上面 fobj 例子來說, 他的運作是可以更改特性 x及y,因此特性 x:, y: 就成了存在物件裡必要的規範(protocol), 所以要先宣告一個物件含有 x和y的特性, 才能用函式 fobj 去綁定(bind)該物件,最後才讓程序去運作, 如果綁定( bind )之後加上命令符號 ( )可以讓綁定函式的程序直接去運行.綁定後 this 就變成是新物件,例如下例 fobj 綁定物件 a 的新程序: fobj.bind(a) ( )
a={x:5,y:6};
fobj.bind(a) ( ) // a = {x:3,y:4} , 'this' is equal to a
可以看到 a 的特性x和y都被更改了, 而 call 的用法是除了綁定物件外,緊接著就執行程序,因此後面不再加上命令符號 ( )
function fobj() { this.x=3; this.y=4; }
b={x:1,y:2};
fobj.call(b) // 'this' is equal to b
如果函式帶有參數要傳遞可以加在 ( ) 裏面, 當建構式利用參數來初始化特性時,它就成了初始化建構式. 這樣子無形中又替這種函式建立了另一套規範(protocol), 就是說除了物件必須有x及y兩個特性外,當呼叫時必須要指定兩個參數:
function iobj(x,y) { this.x=x;this.y=y; }
a={x:5,y:6}
iobj.call(a,7,8) // 'this' is equal to a
而 apply 不同於 call 的用法,在於他要用一個陣列將參數一次傳遞過去,就等於是使用另外一種規範(protocol: 物件要有屬性 x,y, 呼叫時要傳兩個參數, 將他們組合成陣列一次傳過來)的程序而已:
function iobj(x,y) { this.x=x;this.y=y; }
b={x:5,y:6}
iobj.apply(b,[7,8]) // 'this' is equal to b
要特別注意的是只有函式方法(注意是函式功能)才可以呼叫綁定bind( )或call( )或apply( )程序綁定物件讓它有執行程序的能力,就好像是物件去執行新程序一樣.因此所有的函式方法必須定義好規範(protocol)才能正常運作. 否則能運作只是運氣好(lucky)而已.回過頭來看 new 建構式與 Object.create(函式體),同樣看似繼承自函式物件,但產生的物件卻是截然不同的.而物件可以透過 Object.getPrototypeOf( ) 取得上一代的原型加以驗證:
function fobj( ) {this.x=1; return 3;}
a = Object.create( fobj )
// a inherit from fobj, a pure object, no functionality.
b = new fobj( ) // b is instance of fobj
c = new fobj( ) // c is another instance of fobj
Object.getPrototypeOf(Function.prototype) === Object.prototype// true
Object.getPrototypeOf(fobj)===Function.prototype // true
Object.getPrototypeOf(a) === fobj // true, a is inherited from fobj
Object.getPrototypeOf(fobj.prototype) === Object.prototype // ture
Object.getPrototypeOf(b)=== fobj.prototype // true
Object.getPrototypeOf(c)=== fobj.prototype // true
a.name === fobj.name // true
a.prototype === fobj.prototype // true
因為fobj用function宣告,繼承自Function.prototype也就是函式祖,函式祖又從Object.prototype繼承而來,因為Object.prototype是一切物件的始祖(簡稱祖先).由此可見fobj是祖先後二代的物件
函式體的物件繼承路徑圖:祖先->函式祖->函式體(fobj)->a
而a又繼承自fobj,但Object.create( )僅能繼承到函式體而不會繼承函式功能,a單純就是一個祖先後三代沒有執行功能的物件.而a.prototype===fobj.prototype是因繼承而來,即使a.call===fobj.call,但仍不能呼叫a.call( ),因為綁定程序是給物件函式用的.很重要的觀念就是物件不能呼叫bind或call或apply等程序,只有方法可以呼叫bind,call,apply程序來綁定物件讓它看起來像是物件在執行函式.再來看new的操作. new是操作於一個建構式,函式體裏面有一個特性稱為prototype,當函式一宣告完成,就會從祖先繼承一個空物件{},它有個專有名詞叫原型物件(prototype).顯然 b 及 c 繼承了這個原型物件,但要注意的是原型物件裡並不一定有prototype(大部份原型物件並沒有)這個特性存在的,可以想像成是b及c都是用Object.create(fobj.prototype)產生的繼承關係,因為內定原型物件是一個空物件,因此 b與 c 並沒有 prototype這個特性. 再做個實驗, 當建構式傳回物件時:
function fobj( ) { var c=[ ]; c.a='a';c.d=function ( ) { console.log('d'); }; return c; }
a=new fobj( )
a // [ a:'a', d:Function ]
a.a // 'a'
a.d( ) // 'd'
可見他真的傳回該陣列 c, 陣列是一個物件而不是繼承原型的空物件, 有點意外! 建構式return 物件時真的要小心!,當建構式不傳回物件時:
函式體的物件繼承路徑圖:祖先->函式祖->函式體(fobj)->a
而a又繼承自fobj,但Object.create( )僅能繼承到函式體而不會繼承函式功能,a單純就是一個祖先後三代沒有執行功能的物件.而a.prototype===fobj.prototype是因繼承而來,即使a.call===fobj.call,但仍不能呼叫a.call( ),因為綁定程序是給物件函式用的.很重要的觀念就是物件不能呼叫bind或call或apply等程序,只有方法可以呼叫bind,call,apply程序來綁定物件讓它看起來像是物件在執行函式.再來看new的操作. new是操作於一個建構式,函式體裏面有一個特性稱為prototype,當函式一宣告完成,就會從祖先繼承一個空物件{},它有個專有名詞叫原型物件(prototype).顯然 b 及 c 繼承了這個原型物件,但要注意的是原型物件裡並不一定有prototype(大部份原型物件並沒有)這個特性存在的,可以想像成是b及c都是用Object.create(fobj.prototype)產生的繼承關係,因為內定原型物件是一個空物件,因此 b與 c 並沒有 prototype這個特性. 再做個實驗, 當建構式傳回物件時:
function fobj( ) { var c=[ ]; c.a='a';c.d=function ( ) { console.log('d'); }; return c; }
a=new fobj( )
a // [ a:'a', d:Function ]
a.a // 'a'
a.d( ) // 'd'
可見他真的傳回該陣列 c, 陣列是一個物件而不是繼承原型的空物件, 有點意外! 建構式return 物件時真的要小心!,當建構式不傳回物件時:
new 建構式的物件繼承路徑圖:祖先->原型物件->b或c
由此來看new 建構式的操作程序是:
1. 繼承原型物件產生新物件.
2. 建構式程序修改新物件.
3. 如果 return 不是物件,則回傳上述新物件產生實體(instance).
4. 如果 return 後面接的是一個物件, 就直接回傳該物件.
因此 b 與 c 雖有相同的特性,但卻是分別獨立的個體.彼此更改特性並不會受到影響.要注意的是 fobj, 及 fobj.prototype 分別表示兩個不同的實體物件,彼此間並不一定有繼承關係(繼承路徑不同),不需太大著墨.僅須記住fobj是函式體,而fobj.prototype是原型物件.因為所有繼承者都會繼承這個原型物件,而原型就只有一個.這也就是為何當原型物件更動任何特性時,都會反應在繼承者身上.那可不可以將原型物件換掉呢,答案顯而易見是可以的, 但有可能因繼承關係而造成混亂.做個實驗:
function fobj() {} // empty function to construc an new object
fobj.prototype={x:20,y:30,z:50}// use a defined object to be construct.
a=new fobj();
a.x+a.y+a.z // a is empty, but {x:20, y:30, z:50 }did exist
結論: 當建構式傳回的是物件時,new建構式直接傳回該物件,否則繼承函式的原型物件後回傳,有點像是Object.create(fobj.prototype),而原型物件在函式宣告後,只要在使用 new 建構式之前都是可以被替換掉的,原型物件所有特性也將會被繼承.而Object.create(fobj)則只是繼承函式體罷了,繼承者並不俱備執行功能. fobj及fobj.prototype各自描述著不同的實體物件,fobj是函式體,記載著所有函數的特性,而fobj.prototype只是函式體內其中的一個特性(稱為原型物件),視需要可以自行替換,千萬不要搞混了.
由此來看new 建構式的操作程序是:
1. 繼承原型物件產生新物件.
2. 建構式程序修改新物件.
3. 如果 return 不是物件,則回傳上述新物件產生實體(instance).
4. 如果 return 後面接的是一個物件, 就直接回傳該物件.
因此 b 與 c 雖有相同的特性,但卻是分別獨立的個體.彼此更改特性並不會受到影響.要注意的是 fobj, 及 fobj.prototype 分別表示兩個不同的實體物件,彼此間並不一定有繼承關係(繼承路徑不同),不需太大著墨.僅須記住fobj是函式體,而fobj.prototype是原型物件.因為所有繼承者都會繼承這個原型物件,而原型就只有一個.這也就是為何當原型物件更動任何特性時,都會反應在繼承者身上.那可不可以將原型物件換掉呢,答案顯而易見是可以的, 但有可能因繼承關係而造成混亂.做個實驗:
function fobj() {} // empty function to construc an new object
fobj.prototype={x:20,y:30,z:50}// use a defined object to be construct.
a=new fobj();
a.x+a.y+a.z // a is empty, but {x:20, y:30, z:50 }did exist
結論: 當建構式傳回的是物件時,new建構式直接傳回該物件,否則繼承函式的原型物件後回傳,有點像是Object.create(fobj.prototype),而原型物件在函式宣告後,只要在使用 new 建構式之前都是可以被替換掉的,原型物件所有特性也將會被繼承.而Object.create(fobj)則只是繼承函式體罷了,繼承者並不俱備執行功能. fobj及fobj.prototype各自描述著不同的實體物件,fobj是函式體,記載著所有函數的特性,而fobj.prototype只是函式體內其中的一個特性(稱為原型物件),視需要可以自行替換,千萬不要搞混了.
後記:原生值(Primitive values)
Javascript 內有 5 種原生值(Primitive values): 字串(Strings),數字(Numbers),布林值(Booleans), 未定義(undefined),空的(null). 它們是常數(immutable value),而不是物件.除了undefined及null外,原生值也會從他始祖的原型物件繼承到原生值的特性,例如字串會從字串祖(String.prototype)繼承字串特性,數字會從數字祖(Number.prototype)繼承數字特性,布林(邏輯)值則從布林祖(Boolean.prototype)繼承布林特性,就如同函式體(函式名稱)從函式祖(Function.prototype)繼承一樣.而函式體描述著函式的特性,以此類推,原生值的名稱也應該能描述原生值的特性,因此稱他為原生體.但原生體並沒有原型物件(prototype)這個特性.且原生體也無法經由 Object.getPrototypeOf( )取得被繼承者. 我們可以試著繼承各原生祖的原型物件而產生原生體,但無法產生原生值.如同繼承函式祖無法產生函式的功能,試著繼承各原生祖:
notFunction=Object.create(Function.prototype) // 這不是函式功能 { }
繼承其它原生祖:
notString=Object.create(String.prototype) // 這不是字串 { }
notNumber=Object.create(Number.prototype) // 這不是數字 { }
notBoolean=Object.create(Boolean.prototype) // 這不是布林值{ }
雖然空物件的原生體有繼承到原生值的特性(property),但並未賦予原生值(數字,字串,邏輯,函式).正如同前面所說,可以試著繼承函式祖,但無法賦予函式功能. 因此原生體與原生值還是要分開來看才不會混淆.透過 typeof 可以知道到底是物件(object)還是原生值(string, number, boolean, undefined),所有 new 建構式都會繼承原型物件,因此傳回物件是可以理解的. 但 instanceof null 傳回 object, 據說是知名的 bug, 實際上null是原生值而不是物件.
typeof '123' // 'string'typeof 123 // 'number'
typeof true // 'boolean'
typeof false // 'boolean'
typeof undefined // undefined
typeof null // 'object'
typeof new String('123') // object
typeof new Number(123) // object
typeof new Boolean(true) // object
做個實驗:
function b( ) {return 3; }
a=b( ) // 函式b的函數值
a // 3
最後一行給名稱就可得到值,可見得原生值一定是函數值. Number( ) 就是數字構建函數,String( ) 就是字串構建函數,而Boolean()就是布林值構建函數.想像一下數字構建函數的運作原理:繼承數字祖產生一個數字原生體,該原生體其實是一個可執行函式體,正如上面的 a一樣, 開頭程式碼就指向 b( ),b( ) 就是構建函數, a 本身後面附有有一些描述內容(也就是數字原生體)包含數值及所繼承的各種特性,只要判斷所要取得的物件是什麼就指過去執行,否則傳回本身函數值.而構件函數在搭建原生值時一定是只有改變函數值後再將原生體回傳.所有的原生體都是獨立的實體且只有函數值及型態(字串,數字,布林)不同而已.
當原生值指定給一個新變數時,例如要將原生體a指定給c (c=a),由上面數字構建函數的思維來看.只能傳回函數值其實隱涵著函數值無法被改變(immutable),為了做到互不相干擾,複製一份原生體是唯一的方案. 因此指定新常數給變數就等於複製一份原生體(他其實是一段可執行函式加上原生體),所以這3種原生值指定給變數時, 所產生的變數並不是別名,而是一段真正可執行的函數原生體.原生值的比對是基於數值valueOf( )的比對,並非物件的比對.
( function ( ) {y=5; } ( ) ) // y=5 --- (1)
或是:
( function ( ) {y=5; } ) ( ) // y=5 --- (2)
或是:
y=( function ( ) {return 5;} ) ( ) // y=5 --- (3)
x=3;y=(function ( ) { x=4 ; return 5; }) ( ); // x=4, y=5
x=3;y=(function ( ) { this.x=4; return 5; }) ( ); // x=4, y=5
x=3;y=(function ( ) { var x=4 ; return 5; }) ( ); // x=3, y=5
function b( ) {return 3; }
a=b( ) // 函式b的函數值
a // 3
最後一行給名稱就可得到值,可見得原生值一定是函數值. Number( ) 就是數字構建函數,String( ) 就是字串構建函數,而Boolean()就是布林值構建函數.想像一下數字構建函數的運作原理:繼承數字祖產生一個數字原生體,該原生體其實是一個可執行函式體,正如上面的 a一樣, 開頭程式碼就指向 b( ),b( ) 就是構建函數, a 本身後面附有有一些描述內容(也就是數字原生體)包含數值及所繼承的各種特性,只要判斷所要取得的物件是什麼就指過去執行,否則傳回本身函數值.而構件函數在搭建原生值時一定是只有改變函數值後再將原生體回傳.所有的原生體都是獨立的實體且只有函數值及型態(字串,數字,布林)不同而已.
當原生值指定給一個新變數時,例如要將原生體a指定給c (c=a),由上面數字構建函數的思維來看.只能傳回函數值其實隱涵著函數值無法被改變(immutable),為了做到互不相干擾,複製一份原生體是唯一的方案. 因此指定新常數給變數就等於複製一份原生體(他其實是一段可執行函式加上原生體),所以這3種原生值指定給變數時, 所產生的變數並不是別名,而是一段真正可執行的函數原生體.原生值的比對是基於數值valueOf( )的比對,並非物件的比對.
a=123 //123 是常數,產生可執行函數原生體來描述 a.valueOf( ) 等於 123
// a 是常數(immutable)個體,因此無法設定像 a.valueOf() = 44
b=a // 既然 a 是函數原生體, 將原生體 a 複製一份給 b
a===b // 常數的比對,等同於 a.valueOf( )===b.valueOf( )
a=44 // 當 a 指定新常數時, b 並不會跟著改變
a.toString( ) // 原生值 a 有繼承到一些方法像是 toString()
b.toString( ) // 原生值 b 也是
b=a // 既然 a 是函數原生體, 將原生體 a 複製一份給 b
a===b // 常數的比對,等同於 a.valueOf( )===b.valueOf( )
a=44 // 當 a 指定新常數時, b 並不會跟著改變
a.toString( ) // 原生值 a 有繼承到一些方法像是 toString()
b.toString( ) // 原生值 b 也是
物件 this
如果在函式宣告後面加上( ),並將整個函式用另一個 ( )框住,這種函式稱為立即賦值函式(Immediately Invoke Function Express 簡稱 IIFE), 函式兩邊的括號就是一個賦值敘述(evaluate expression 白話文的意思就是:先計算出來再說),後面一對( )等於就是命令執行的意思,當賦值敘述越放在內層,要優先運算出來(堆疊的概念:先進後出,後進則要先出),舉個算式為例:
( ( (1+9)* 4 + 10 )*3+10 ) /10 =16
先從最內層算出 1+9=10
再算出外層 10*4+10 = 50
接著算出更外層 50*3+10 =160
最後才是 160/10 =16
接著再看:( ( (1+9)* 4 + 10 )*3+10 ) /10 =16
先從最內層算出 1+9=10
再算出外層 10*4+10 = 50
接著算出更外層 50*3+10 =160
最後才是 160/10 =16
( function ( ) {y=5; } ( ) ) // y=5 --- (1)
或是:
( function ( ) {y=5; } ) ( ) // y=5 --- (2)
或是:
y=( function ( ) {return 5;} ) ( ) // y=5 --- (3)
在第1式函式宣告完加上賦值敘述等於讓它執行, 第2式函式宣告完經賦值敘述得到一個函式,後面 ( ) 才讓它執行.在第3式同樣函式宣告完加上賦值敘述得到一個函式,後面 ( ) 讓它執行,傳回函數值,最後才指定給新變數.這三種寫法都讓y=5,上述不只宣告函式還讓它立即執行,最後y都被賦予了一個數值5.上述函式裡可以看到,當變數沒宣告時,就直接取自整體物件的變數(JavaScript 在瀏覽器內執行時,window就代表是整體物件).如果再將this放在函式裡頭,很容易讓人混淆或誤解.this 如果在一般的函式裡頭,代表的就是整體物件.當變數x用var宣告後,它變成是內部區域變數(private variable),它不會影響到同名稱的整體變數(global variable).一般的函式利用this這個物件可以存取整體物件的變數(有點類似隔空抓物的感覺).以下實驗可以看到在第1式與第2式會得出相同結果,但第3式 x 值並不受影響,而y等於函式值 5:
x=3;y=(function ( ) { x=4 ; return 5; }) ( ); // x=4, y=5
x=3;y=(function ( ) { this.x=4; return 5; }) ( ); // x=4, y=5
x=3;y=(function ( ) { var x=4 ; return 5; }) ( ); // x=3, y=5
new建構式也隱涵著立即賦值的意義,不同在於傳回值是一個繼承了函式內部原型物件的一個新物件(instance),而不再是函數值,同樣的,如果變數沒宣告,等同取自整體物件,從下面第1式就可明白.一旦用 new 建構式時, this 就不再是整體物件而是新物件,就是上述的新物件(instance),由下面第2式就可以明瞭,因此要將變數當區域變數,最好宣告成 var 如下面第3式一樣.最後面 new 建構式的函數值(return 5)就變的沒實質意義了.
x=3;y=new function ( ) { this.x=4; return 5; };//x=3, y={x:4},new x
x=3;y=new function ( ) { var x=4 ; return 5; };//x=3, y={ },private x
當物件包含函數特性時this的運作情形又如何呢? 由以下可知,物件的特性被改掉了,但變數 x 沒變, 因此當物件執行函數時,就好像函式將物件綁定了,而this就是該物件:
來看一個 return this 很有趣的例子, 先宣告一個空心函式.再替原型物件增添新函數,每個函數增添一名成員,最後都傳回 this.
function f( ) { };
f.prototype.setd = function(d) { this.d=1; return this; };
f.prototype.sete = function(e) { this.e=2; return this; };
f.prototype.setn = function(n) { this.n=3; return this; };
new f( ).setd(1).sete(2).setn(3) // { d:1, e:2,n:3 }
最後一式, new 建構式開始回傳一個空物件 new f( ) -> { }, 該物件繼承自原型物件,後來又再原型物件添加了函數特性,此空物件也就繼承了setd:function ( ), sete:function ( ), setn:function ( )等等函數特性.透過句點語法取用第一個特性時設定新物件的 d 後傳回 this, 他就是新物件本身,緊接著第二次也是相同程序,最後得到 {d:1,e:2,n:3} 這個新物件
無名函式呼叫綁定函數 bind( ), 經賦值敘述後再用命令符號( )同樣可以立即執行,如果沒有綁定任何物件, this 內定是整體物件.
x=3; ( function ( ) { x=4; } .bind( ) ) ( ) // global x
x=3; ( function ( ) { this.x=4; }.bind( ) ) ( ) // global x
x=3; ( function ( ) { var x=4; }.bind( ) ) ( ) // private x
另外兩個綁定函數 call( ) 及 apply( ),只要加上賦值敘述就能立即執行,無須增加命令符號( ):
x=3; ( function ( ) { this.x=4; }.bind( ) ) ( ) //x=4
x=3; ( function ( ) { this.x=4; }.call() ) // x=4
x=3; ( function ( ) { this.x=4; }.apply() ) // x=4
如果函數(bind( ), call( ), apply( ) ...等等)綁定的是新物件, 那 this 就是這個新物件.
a={x:3 };( function ( ) {this.x=4; }.bind(a) ) ( ) //a= {x:4 } new x
a={x:3 };( function ( ) {this.x=4 ; }.call(a) ) //a= {x:4 } new x
a={x:3 };( function ( ) {this.x=4 ; }.apply(a) ) //a= {x:4 } new x
call( ) 與 apply( ) 綁定函數不同在於: call( )是綁定物件後,將參數跟隨物件一一列出再呼叫函式, 而 apply 則是綁定物件跟隨一個陣列後呼叫函式, 簡單來說call的參數數量是不定的,但 apply 的參數就只是一個陣列.最後要再三強調的是只有函式方法可以呼叫 bind( ), call( ), apply( ) 等程序來綁定物件執行該函式.
特殊函式體:函式口 Closure
前面所說當變數沒宣告時,它會變成整體變數.有個例子很有趣,當變數是放在函式裡頭的宣告函式裏面時,如果該變數沒有用 var 宣告,那它是區域變數還是整體變數呢? 這個重點如果擺在'程序是否有被執行',就可發現端倪,要記住函式宣告後產生一個函式體,實際上並未執行,除非用任何賦值敘述( )讓他執行.先看以下範例:x=1;y=1;
function a( ) { x=10; function f( ) { y=11; } };
function b( ) { x=20; function f( ) { y=21; } ; f( );};
// 函數 a,b 剛宣告完, 並未執行, 因此 x 及 y 並未改變
a( )
// 執行函式體a,x改掉了,但函式體f宣告完還沒執行,因此y並未改變.
b( )
// 執行函式體b,函式f在函式b裡宣告完,緊接著就執行,於是y也改掉了
可見只要程序被執行過,如果沒用 var宣告,它還是取自整體變數, 如果將函式體傳回來,又會如何?看以下例子:
x=1;y=1;
function c( ) { x=30; function f( ) { y=31; } ; return f; };
d=c( )
d( )
// 函式體 f 最後還是被執行了, 因此 y 也被改了.
初學者可能會誤以為f( )在函式c裡頭宣告, 就用 c.f( ) 讓它執行. 雖然c是一個物件,但它並沒有f( )這個方法.因為函式體都從Function.prototype繼承而來,裏面特性除非是加在Function.protoype後面或者自行添加用c.d = function ( ) { }方式來創造特性,但這又是另外一回事.裏面的函式宣告只是一個程式的宣告, 就好比用var宣告變數是同樣的道理.因此可以將同樣的名稱在不同的函式宣告,互相並不會影響,因為該名稱被視為函式裡頭的區域變數,而區域變數是無法直接用句點語法取用到的.因此變數未經 var 宣告就是整體變數,這個原則仍舊成立.對於函式宣告而言,也是如此.如果傳回函式體而且攜帶著區域變數,又會如何呢子? 詳見以下範例:
y=1; function c(x) {var y=x; function f( ) { return y++; }; return f; }
inc=c(5)
inc( )
inc( )
在函式裡頭的 y 用 var 宣告, 因此它是區域變數,並不會影響到整體變數的 y. 函式c先初始化(y=5).再傳回函式體 f 給 inc, 此時 inc 並未執行, 透過下一步命令符號 inc( ) 執行後,區域變數 y 早已被初始成5,這個函式體會執行 return y++的動作, 就是傳回 y(=5) 後再將y加一,周而復始.可看到後面再執行inc()時都會累加一.你一定會有個疑問:外部函式停止了,所有區域變數不就消失了,為何區域變數還存活著呢? 這個就是著名的closure啦.我也不會解釋.反正真的這樣,也許要找出它的組合程式碼才會明瞭.這種攜帶著區域變數的特殊函式體簡稱 closure.只有指著一個函式的入口點,當加上命令符號( )後才會開始執行函式程序,因此翻譯成函式口,網路上翻譯成閉包,怪怪的說.看看如果將區域變數所指的的物件傳回,該物件會是如何呢?
function aobj( ) { var a={x:1}; return a; }
b=aobj( )
對於習慣c語言的來說,可能無法理解,程式結束後,區域變數不就全消失了,當區域變數指向物件後再傳回去,該物件竟然還活著!這就必須要重新思考JavaScript對於物件的記憶體管理,可能不是從堆疊(stack)記憶體去分配,而是使用了靜態(static)記憶體.合理猜測:區域變數是一個指標,從堆疊裡分配記憶體給指標使用,但物件則使用靜態記憶體,而指標會指向該物件,因此程式結束後,區域變數(指標)雖然消失了,但物件還保留在靜態記憶體中.這也許就是closure變數所指向的物件還活著的原因.因此一旦物件不使用時應該將它刪除(delete),將記憶體釋放出來.
結論:
1.函式內變數除非用 var 宣告成區域變數,否則它是整體變數
2.物件取用的特性是函數時, 該函數的式子裏面this就是該物件本身
3.當使用new建構式時,裏面的this及傳回物件就是新物件(從原型物件繼承).
3.當使用new建構式時,裏面的this及傳回物件就是新物件(從原型物件繼承).
4.只有函式可以呼叫 bind( ), call( ), apply( ) 程序來綁定物件執行該函式.
5.函式用綁定函數執行時, 如果綁定了新物件, this 就是新物件,如果沒有,它就是整體物件.
因此 this 代表的不是整體物件就是被綁定的物件.
5.函式用綁定函數執行時, 如果綁定了新物件, this 就是新物件,如果沒有,它就是整體物件.
因此 this 代表的不是整體物件就是被綁定的物件.
沒有留言:
張貼留言