6- js 函式的四種呼叫方式
6. 函式的四種呼叫方式
函式有下列呼叫模式
- 函式呼叫模式
- 方法呼叫模式
- 構造器模式
- 上下文模式
1. 函式呼叫 模式
要呼叫,就肯定要先定義,函式的定義方式:
- 宣告式: function fuc() {}
- 表示式式: var func = function() {};
- Function: new Function( ‘引數’,…,’函式體’ );
單獨獨立呼叫的,就是函式呼叫模式,即 函式名( 引數 ),不能加任何其他的東西, 物件 o.fuc() 就不是了。
在函式呼叫模式中, this 表示全域性物件 window
任何自呼叫函式都是函式模式。
2. 方法呼叫 模式 method
所謂方法呼叫,就是用物件的方法呼叫。方法是什麼,方法本身就是函式,但是,方法不是單獨獨立的,而是要通過一個物件引導來呼叫。
就是說方法物件一定要有宿主物件。
即 物件.方法(引數)
this表示引導方法的物件,就是指宿主物件
對比-函式呼叫模式:
- 方法呼叫模式是不是獨立的,需要宿主,而函式呼叫模式是獨立的
- 方法呼叫模式方式:obj.fuc(); 函式呼叫模式方式: fuc();
- 方法呼叫模式中,this指宿主。而函式呼叫模式中 this 指 全域性物件window
美團的一道面試題
var length = 10 ;
function fn() {
console.log( this.length ); // 10
}
var obj = {
length: 5,
method: function ( fn ) {
fn(); // 10 前面沒有引導物件,是函式呼叫模式
arguments[ 0 ](); // 2
// arguments是一個偽陣列物件, 這裡呼叫相當於通過陣列的索引來呼叫.
// 這裡 this 就是 指的這個偽陣列, 所以 this.length 為 2
}
};
obj.method( fn, 1 ); // 列印 10 和 2
//obj.method( fn, 1, 2, 3 ); // 列印 10 和 4
解析:
fn() 前面沒有引導物件,是函式呼叫模式, this是全域性物件,輸出 10
arguments[ 0 ](),arguments是一個偽陣列物件, 這裡呼叫相當於通過陣列的索引來呼叫.
這裡引導物件即宿主就是 arguments物件。
所以,執行時,this 就是指 arguments,由於傳了兩個引數,所以 輸出為 arguments.length 就是 2
3. 構造器模式(建構函式模式, 構造方法模式)
constructor
特點: 使用 new 關鍵字引導
執行步驟:var p = new Person();
new 是一個運算子, 專門用來申請建立物件, 創建出來的物件傳遞給建構函式的 this。然後利用建構函式對其初始化。
function Person () { // new了 進入建構函式時, p 物件的原型 就指向了 建構函式 Person, // p.__proto__.constructor = function Person() {}; // 而 this 指的的是 p 物件 this.name = 'jim', this.age = 19; this.gender = 'male'; } var p = new Person();
執行完 new 進入建構函式時, p 物件的原型 就指向了 建構函式 Person
而 構造時,this 指的的是 p 物件,是通過物件動態新增屬性來構造的
小貼士:如果呼叫建構函式的時候, 建構函式沒有引數, 圓括號是可以省略的。
function Person() { this.name = 'jim'; } var p = new Person; // 不傳參,可以簡寫,不影響構造 console.log( p ); // p 含有 name屬性
↑ 不傳參,可以簡寫,不影響構造
返回值
不寫 return 語句, 那麼 建構函式 預設返回 this
在建構函式 return 基本型別( return num, return 1223 ). 則忽略返回型別.
在建構函式 return 引用型別, 那麼建構函式返回該引用型別資料, 而忽略 this
function Person () { this.name = 'Jepson'; return 123; } var p1 = new Person(); console.log( p1 );
↑ 忽略了 123,返回 this 物件, 指向構建的例項
function Person () { this.name = 'Jepson'; return { 'peter': 'nihao' }; } var p1 = new Person(); console.log( p1 );
↑ 忽略了 this,返回 { ‘peter’: ‘nihao’ } 物件
建構函式結合性
如果建構函式沒有引數, 可以省略 圓括號
var p = new Person;
如果希望建立物件並直接呼叫其方法
( new Person () ).sayHello()
-> 可以省略調整結核性的圓括號 new Person().sayHello()
-> 如果想要省略建構函式的圓括號, 就必須新增結核性的圓括號 (new Person).sayHello()
面試題
一道面試題,大家可以自己嘗試先做一下,再看下面的答案和解析
請問順序執行下面程式碼,會怎樣 alert
function Foo(){
getName = function(){ alert(1); };
return this;
}
Foo.getName = function(){ alert(2); };
Foo.prototype.getName = function(){ alert(3); };
var getName = function(){ alert(4); };
function getName(){ alert(5); }
Foo.getName(); // alert ??
getName(); // alert ??
Foo().getName(); // alert ??
getName(); // alert ??
new Foo.getName(); // alert ??
new Foo().getName(); // alert ??
new new Foo().getName(); // alert ??
預解析,簡化後的程式碼,以及答案
/* function getName(){ alert(5); } 執行到下面被覆蓋了,直接刪除 */
function Foo() {
getName = function () { alert(1); };
return this;
}
Foo.getName = function () { alert(2); };
Foo.prototype.getName = function () { alert(3); };
var getName = function () { alert(4); };
Foo.getName(); // ------- 輸出 2 -------
getName(); // ------- 輸出 4 -------
Foo().getName(); // ------- 輸出 1 -------
getName(); // ------- 輸出 1 -------
new Foo.getName(); // ------- 輸出 2 -------
new Foo().getName(); // ------- 輸出 3 -------
var p = new new Foo().getName(); // ------- 輸出 3 -------
全部解析過程 ↓
function Foo() {
getName = function () { alert(1); };
return this;
}
Foo.getName = function () { alert(2); };
Foo.prototype.getName = function () { alert(3); };
var getName = function () { alert(4); };
Foo.getName(); // ------- 輸出 2 -------
// 呼叫 Foo函式 作為 物件 動態新增的屬性方法 getName
// Foo.getName = function () { alert(2); };
getName(); // ------- 輸出 4 -------
// 這裡 Foo函式 還沒有執行,getName還沒有被覆蓋
// 所以 這裡還是 最上面的 getName = function () { alert(4); };
Foo().getName(); // ------- 輸出 1 -------
// Foo()執行,先覆蓋全域性的 getName 再返回 this,
// this 是 window, Foo().getName() 就是呼叫 window.getName
// 此時 全域性的 getName已被覆蓋成 function () { alert(1); };
// 所以 輸出 1
/* 從這裡開始 window.getName 已被覆蓋 alert 1 */
getName(); // -------- 輸出 1 --------
// window.getName alert(1);
new Foo.getName(); // ------- 輸出 2 -------
// new 就是 找 建構函式(),由建構函式結合性,這裡即使 Foo無參,也不能省略 (),所以不是 Foo().getName()
// 所以 Foo.getName 為一個整體,等價於 new (Foo.getName)();
// 而 Foo.getName 其實就是函式 function () { alert(2); } 的引用
// 那 new ( Foo.getName )(), 就是在以 Foo.getName 為建構函式 例項化物件。
// 就 類似於 new Person(); Person 是一個建構函式
// 總結來看 new ( Foo.getName )(); 就是在以 function () { alert(2); } 為建構函式來構造物件
// 構造過程中 alert( 2 ),輸出 2
new Foo().getName(); // ------- 輸出 3 -------
// new 就是 找 建構函式(),等價於 ( new Foo() ).getName();
// 執行 new Foo() => 以 Foo 為建構函式,例項化一個物件
// ( new Foo() ).getName; 訪問這個例項化物件的 getName 屬性
// 例項物件自己並沒有 getName 屬性,構造的時候也沒有 新增,找不到,就到原型中找
// 發現 Foo.prototype.getName = function () { alert(3); };
// 原型中有,找到了,所以 ( new Foo() ).getName(); 執行,alert(3)
var p = new new Foo().getName(); // ------- 輸出 3 -------
// new 就是 找 建構函式(),等價於 new ( ( new Foo() ).getName )() 輸出 3
// 先看裡面的 ( new Foo() ).getName
// new Foo() 以Foo為建構函式,例項化物件
// new Foo().getName 找 例項物件的 getName屬性,自己沒有,去原型中找,
// 發現 Foo.prototype.getName = function () { alert(3); }; 找到了
// 所以裡層 ( new Foo() ).getName 就是 以Foo為建構函式例項出的物件的 一個原型屬性
// 屬性值為一個函式 function () { alert(3); } 的引用
// 所以外層 new ( (new Foo()).getName )()在以該函式 function () { alert(3); } 為建構函式,構造例項
// 構造過程中 執行了 alert(3), 輸出 3
4. 上下文呼叫模式
就是 環境呼叫模式 => 在不同環境下的不同調用模式
簡單說就是統一一種格式, 可以實現 函式模式與方法模式
-> 語法(區分)
- call 形式, 函式名.call( … )
- apply 形式, 函式名.apply( … )
這兩種形式功能完全一樣, 唯一不同的是引數的形式. 先學習 apply, 再來看 call 形式
apply方法的呼叫形式
存在上下文呼叫的目的就是為了實現方法借用,且不會汙染物件。
如果需要讓函式以函式的形式呼叫, 可以使用
foo.apply( null ); // 上下文為 window
如果希望他是方法呼叫模式, 注意需要提供一個宿主物件
foo.apply( obj ); // 上下文 為 傳的 obj 物件
function foo () {
console.log( this );
}
var o = { name: 'jim' };
// 如果需要讓函式以函式的形式呼叫, 可以使用
foo.apply( null ); // this => window // 或 foo.apply()
// 如果希望他是方法呼叫模式, 注意需要提供一個宿主物件
foo.apply( o ) // this => o 物件
帶有引數的函式如何實現上下文呼叫?
function foo ( num1, num2 ) {
console.log( this );
return num1 + num2;
}
// 函式呼叫模式
var res1 = foo( 123, 567 );
// 方法呼叫
var o = { name: 'jim' };
o.func = foo;
var res2 = o.func( 123, 567 );
使用 apply 進行呼叫, 如果函式是帶有引數的. apply 的第一個引數要麼是 null 要麼是物件
如果是 null 就是函式呼叫
如果是 物件就是 方法呼叫, 該物件就是宿主物件, 後面緊跟一個數組引數, 將函式所有的引數依次放在陣列中.
例如: 函式模式 foo( 123, 567 );
apply foo.apply( null, [ 123, 567 ] ) 以 window 為上下文執行 apply
如果有一個函式呼叫: func( '張三', 19, '男' ),
將其修改成 apply 模式: func.apply( null, [ '張三', 19, '男'] )
方法模式: o.func( 123, 567 )
apply var o = { name: 'jim' };
foo.apply( o, [ 123, 567 ] ); 以 o 為上下文執行 apply
方法借用的案例
需求, 獲得 div 與 p 標籤, 並新增邊框 border: 1px solid red
一般做法:
var p_list = document.getElementsByTagName('p');
var div_list = document.getElementsByTagName('div');
var i = 0;
for( ; i < p_list.length; i++ ) {
p_list[ i ].style.border = "1px solid red";
}
for( i = 0; i < div_list.length; i++ ) {
div_list[ i ].style.border = "1px solid red";
}
- 利用方法借用優化,元素獲取
var t = document.getElementsByTagName;
var p_list = t.apply( document, [ 'p' ] ); // 方法借用
var div_list = t.apply( document, [ 'div' ] ); // 方法借用
接下來考慮下面的優化,兩個for迴圈,只要將數組合並了,就可以只用一個 for 迴圈
數組合並
var arr1 = [ 1, 2, 3 ]; var arr2 = [ 5, 6 ]; arr1.push.apply( arr1, arr2 ); // 方法呼叫,第一個給上物件 // 等價於 Array.prototype.push.apply( arr1, arr2 );
所以同理,利用 apply 方法借用,將兩個偽數組合併成同一個陣列
var arr = []; // 偽陣列沒有 push 方法,所以這裡要 宣告一個 陣列 arr.push.apply( arr, p_list ); // 將 p_list裡的內容,一個個當成引數放了進來,相當於不用遍歷了 arr.push.apply( arr, div_list ); // 同上,方法借用 console.log( arr );
將兩者綜合, 使用forEach,最終 6 行就解決了
var t = document.getElementsByTagName, arr = [];
arr.push.apply( arr, t.apply( document, [ 'p' ] ) );
arr.push.apply( arr, t.apply( document, [ 'div'] ) );
arr.forEach( function( val, index, arr ) {
val.style.border = '1px solid red';
});
call 呼叫
在使用 apply 呼叫的時候, 函式引數, 必須以陣列的形式存在. 但是有些時候陣列封裝比較複雜
所以引入 call 呼叫, call 呼叫與 apply 完全相同, 唯一不同是 引數不需要使用陣列
foo( 123, 567 );
foo.apply( null, [ 123, 567 ] );
foo.call( null, 123, 567 );
函式呼叫: 函式名.call( null, 引數1,引數2,引數3… );
方法呼叫: 函式名.call( obj, 引數1,引數2, 引數3… );
不傳參時,apply 和 call 完全一樣
借用構造方法實現繼承
function Person ( name, age, gender ) {
this.name = name;
this.age = age;
this.gender = gender;
}
function Student ( name, age, gender, course ) {
// 原型鏈結構不會變化,同時實現了繼承的效果
Person.call( this, name, age, gender );// 借用Person構造方法
this.course = course;
}
var p = new Student ( 'jim', 19, 'male', '前端' );
console.log( p );
補充知識 1. 函式的 bind 方法 ( ES5 )
bind 就是 繫結
還是上面那個案例 獲得 div 與 p 標籤, 並新增邊框 border: 1px solid red
var t = document.getElementsByTagName, arr = [];
arr.push.apply( arr, t.call( document, 'p' ) );
arr.push.apply( arr, t.call( document, 'div' ) );
arr.forEach( function( val, index, arr ) {
val.style.border = '1px solid red';
});
我們 讓 t 包含函式體(上面的方式),同時包含 物件,就可以更精簡
var t = document.getElementsByTagName.bind( document ), arr = [];
arr.push.apply( arr, t('p') );
arr.push.apply( arr, t('div') );
arr.forEach( function( val, index, arr ) {
val.style.border = '1px solid red';
});
bind : 就是讓函式繫結物件的一種用法
函式本身就是可以呼叫, 但是其如果想要作為方法呼叫, 就必須傳入宿主物件, 並且使用 call 或 apply 形式
但是 bind 使得我的函式可以與某一個物件繫結起來, 那麼在呼叫函式的時候, 就好像是該物件在呼叫方法,就可以直接傳參,而不需要傳宿主物件。
語法: 函式.bind( 物件 )
返回一個函式 foo,那麼呼叫 返回的函式 foo, 就好像 繫結的物件在呼叫 該方法一樣
t.call( document, 'p' );
t( 'p' ); 繫結後,就不用傳宿主物件了,這裡呼叫時上下文已經變成了 document
bind 是 ES 5 給出的函式呼叫方法
補充知識 2. Object.prototype 的成員
Object.prototype 的成員
- constructor
- hasOwnProperty 判斷該屬性是否為自己提供
- propertyIsEnumerable 判斷屬性是否可以列舉
- isPrototypeOf 判斷是否為原型物件
- toString, toLocaleString, valueOf
function Person() {
this.name = 'jim';
}
Person.prototype.age = 19;
var p = new Person();
console.log( p.hasOwnProperty( 'name' ) ); // true; p 是否含有 name 屬性,原型上不管
console.log( p.hasOwnProperty( 'age' ) ); // false; p 是否含有 age 屬性
/* Person.prototype 是 p 的原型 */
console.log( p.isPrototypeOf( Person.prototype ) ); // false
console.log( Person.prototype.isPrototypeOf( p ) ); // true;
用途:一般把一個物件拷貝到另一個物件時,可以進行判斷,更加嚴謹,以防把原型中的屬性也拷貝過去了…
補充知識 3. 包裝型別
字串 string 是基本型別, 理論上講不應該包含方法
那麼 charAt, substr, slice, …等等方法,理論上都不應該有,但確是有的
所以引入包裝物件的概念,在 js 中為了更好的使用資料, 為三個基本型別提供了對應的物件型別
- Number
- String
- Boolean
在 開發中常常會使用基本資料型別, 但是基本資料型別沒有方法, 因此 js 引擎會在需要的時候自動的將基本型別轉換成物件型別, 就是包裝物件
“abc”.charAt( 1 )
“abc” -> s = new String( “abc” )
s.charAt( 1 ) 返回結果以後 s 就被銷燬
當基本型別.方法 的時候. 直譯器首先將基本型別轉換成對應的物件型別, 然後呼叫方法.
方法執行結束後, 這個物件就被立刻回收
在 apply 和 call 呼叫的時候, 也會有轉換髮生. 上下文呼叫的第一個引數必須是物件. 如果傳遞的是數字就會自動轉換成對應的包裝型別
補充知識 4. getter 和 setter 的語法糖 ( ES5 )
語法糖: 為了方便開發而給出的語法結構
本身實現:
var o = (function () {
var num = 123;
return {
get_num: function () {
return num;
},
set_num: function ( v ) {
num = v;
}
};
})();
希望獲得資料 以物件的形式
o.get_num(); => o.num 形式
希望設定資料 以物件的形式
o.set_num( 456 ); => o.num = 456 形式
所以 getter 和 setter 誕生 了
var o = (function () {
var num = 123;
return {
// get 名字 () { 邏輯體 }
get num () {
console.log( '執行 getter 讀寫器了' );
return num;
},
// set 名字 ( v ) { 邏輯體 }
set num ( v ) {
console.log( '執行 setter 讀寫器了' );
num = v;
}
};
})();
console.log( o.num ); // 執行 getter 讀寫器了 123
o.num = 33; // 執行 setter 讀寫器了
console.log( o.num ); // 執行 getter 讀寫器了 33
為什麼不直接用 物件呢 var o = { num : 123 } ,也可以讀寫呀?
因為語法糖還可以 限制其賦值的範圍,使用起來特別爽
var o = (function () {
var num = 13;
return {
// get 名字 () { 邏輯體 }
get num () {
console.log( '執行 getter 讀寫器了' );
return num;
},
// set 名字 ( v ) { 邏輯體 }
set num ( v ) {
console.log( '執行 setter 讀寫器了' );
if ( v < 0 || v > 150 ) {
console.log( '賦值超出範圍, 不成功 ' );
return;
}
num = v;
}
};
})();
o.num = -1; // 執行 setter 讀寫器了
// 讀寫器賦值超出範圍, 不成功
補充知識 5. ES5 中引入的部分陣列方法
- forEach
- map
- filter
- some
- every
- indexOf
- lastIndexOf
forEach, 陣列遍歷呼叫,遍歷arr,引數三個 1某項, 2索引, 3整個陣列
var arr = [ 'hello', ' js', { }, function () {} ]; // 遍歷 陣列 arr.forEach( function ( v, i, ary ) { console.log( i + '=====' + v ); console.log( ary ); });
map 對映
語法: 陣列.map( fn )
返回一個數組, 陣列的每一個元素就是 map 函式中的 fn 的返回值
就是對每一項都進行操作,並返回
var arr = [ 1, 2, 3, 4 ]; // 數學中: x -> x * x var a = arr.map(function ( v, i ) { return v * v; }); // a [1, 4, 9, 16]
filter 就是篩選, 函式執行結果是 false 就棄掉, true 就收著
語法: 陣列.filter( function ( v, i ) { return true/false })
var arr = [ 1, 2, 3, 4, 5, 6 ]; // 篩選奇數 var a = arr.filter( function ( v ) { return v % 2 === 1; }); // a [ 1, 3, 5 ]
some 判斷陣列中至少有一個數據複合要求 就返回 true, 否則返回 false
var arr = [ '123', {}, function () {}, 123 ]; // 判斷陣列中至少有一個數字 var isTrue = arr.some( function ( v ) { return typeof v === 'number'; } ); // true;
every 必須滿足所有元素都複合要求才會返回 true
var arr = [ 1, 2, 3, 4, 5, '6' ]; // 判斷陣列中每一個都是數字 var isTrue = arr.every( function ( v ) { return typeof v === 'number'; } ); } ); // false;
indexOf 在陣列中查詢元素, 如果含有該元素, 返回元素的需要( 索引 ), 否則返回 -1
var arr = [ 1, 2, 3, 4, 5 ]; var res = arr.indexOf( 4 ); // 要找 4 console.log( res ); // 3 找 4 在 索引為 3 找到 var arr = [ 1, 2, 3, 4, 5, 4, 5, 6 ]; var res = arr.indexOf( 4, 4 ); // 要找 4, 從索引 4開始找 console.log( res ); // 找到了 索引為 5
lastIndexOf 從右往左找
var arr = [ 1, 2, 3, 4, 5, 4, 5, 6 ]; var res = arr.lastIndexOf( 4 ); console.log( res ); // 索引為 5, 從最後一項開始找,即從 length-1 項開始找