函數的四種調用模式
概念
- 在 js 中,無論是函數, 還是方法, 還是事件, 還是構造器,...這些東西的本質都是函數
- 函數, 方法, 事件, 構造器,...只是所處的位置不同
- 這四種模式分別是
- 函數模式
- 方法模式
- 構造器模式
- 上下文模式
函數模式
特征: 簡單的函數調用, 函數名前面沒有任何引導內容
function foo(){}
var fn = function(){};
...
foo();
fn();
(function(){})();
// 上面的三種都是簡單的函數調用
this的含義
- 在函數中 this 表示全局對象
- 在瀏覽器中 this 表示 window(js 中的 global)
方法模式
特征: 方法一定是依附於一個對象, 將函數賦值給對象的一個屬性, 那么就成為方法.就是函數前面必須有引導對象
function foo(){
this.method = function(){};
}
var o = {
method : function(){}
}
this的含義
- 這個依附的對象(引導函數的對象)
注意點
在 arguments 這種偽數組, 或者 [] 數組這樣的對象中, 這樣調用函數也是方法調用, this 會只指向對象
構造器模式
構造函數在創建對象的時候, 做了些什么
- 使用 new 引導構造函數, 創建了一個實例對象
- 在創建對象的同時, 將this指向這個剛剛創建的對象
- 在構造函數中, 不需要 return , 會默認的 return this
分析:
由於構造函數只是給 this 添加成員, 而方法也可以完成這個操作,對與 this 來說, 構造函數和方法沒有本質區別
關於return的補充, 在構造函數中
普通情況, 可以理解為構造函數已經默認進行了 return this, 添加在后面的都不會執行
- 如果手動的添加 return ,就相當於 return this.
- 如果手動的添加 return 基本類型(字符串, 數字, 布爾), 無效, 還是 return this
- 如果手動的添加 return null 或 return undefined, 無效, 還是 return this
特殊情況, return 對象, 最終返回對象
- 手動添加 return 對象類型, 那么原來創建的 this 會被丟掉, 返回 return 后面的對象
上下文模式
概念: 上下文就是環境, 就是自定義this的含義
語法:
- 函數名.apply( 對象, [參數]);
- 這個參數可以是數組, 也可以是偽數組
- 函數名.call( 對象, 參數);
- 多個參數可以通過
,
進行隔離
- 多個參數可以通過
描述:
- 函數名表示的是函數本身, 使用函數進行調用的時候,默認this指的是全局變量
- 函數名也可以是方法提供, 使用方法調用的時候, this指的是當前對象
- 使用 apply 或者 call 進行調用后, 無論是函數, 還是方法的 this 指向全部無效了, this 的指向由 apply 或者 call 的第一個參數決定
注意:
- 如果函數或方法中沒有this的操作, 那么無論是哪一種函數調用模式, 其實都一樣
- 如果是函數調用 foo(), 其實和 foo.apply(window) 類似
- 如果是方法調用 o.method(), 其實和 o.method.apply(o)
無論是 call 還是 apply 在沒有后面參數的情況下(函數無參數, 方法無參數), 兩者一致
function foo(){
console.log(this); // this => window
}
var obj = {};
foo.apply( obj ); // this => obj
foo.call( obj ); // this => obj
apply 和 call 第一個參數的使用規則
- 如果傳入的是一個對象, 就相當於設置該函數中的this為參數
- 如果不傳參數, 或者傳入 null, undefined 等,那么this就默認是 window
foo();
foo.apply();
foo.apply(null);
foo.apply(undefined);
foo.call();
foo.call(null);
foo.call(undefined);
// 上面都this都指向window
- 如果傳入的是基本類型, 那么this指向的就是基本類型的包裝類型的引用
- number => Number
- boolean => Boolean
- string => String
除 this 外的其他參數
再使用上下文調用的時候, 原函數(方法)可能會帶有參數, 那么要讓這些參數在上下文中調用, 就需要這個第二, ( n )個參數來表示
function foo(num){
console.log(num);
}
foo.apply(null, [123]);
// 相當於
foo(123);
應用
上下文調用只是修改this, 但是使用最多的地方是借用函數調用
- 將偽數組轉換為數組
- 傳統方法
var a = {}; a[0] = 'a'; a[1] = 'b'; a.length = 2; // 使用數組自帶方法 concat(); // 不修改原數組 var arr = []; var newArr = arr.concat(a);
分析
由於 a 是偽數組, 並不是真正的數組, 不能使用數組的方法, concat 會將 a 作為一個整體 Object 加入數組
apply 方法有一個特性, 可以將數組或者偽數組作為參數foo.apply( obj, 偽數組 ); // IE8 不支持
將 a 作為 apply 的第二個參數
var arr = []; var newArr = Array.prototype.concat.apply( arr, a);
由上面的數組轉換, 我們可以得到結論, 應該涉及到數組操作的方法理論上都應該可以
push, pop, unshift, shift
slice
splice - 讓偽數組使用 push 方法
小技巧, 偽數組添加元素
偽數組使用 push 方法var a = {length : 0}; // 設置偽數組的長度 a[ a.length++ ] = 'a'; a[ a.length++ ] = 'b'; // 在給偽數組的元素賦值時, 同時修改偽數組的 length 屬性 // a[0] = 'a'; a.length++;
var arr = []; arr.push.apply( arr, a ); // 利用 apply 可以處理偽數組的特性, 進行傳參 // 相當於 arr.push(a[0], a[1])
- 讓偽數組使用 slice 方法
數組的 slice 語法- arr.slice( index, endIndex ), 不包含 endIndex
- 如果第二個參數不傳參, 那么截取從 index 一直到數組結尾
- slice 方法不會修改原數組
通過 apply 實現偽數組使用 slice 方法
var a = { length : 0 }; a[a.length++] = 'a'; a[a.length++] = 'b'; var arr =[]; var newArr = arr.slice.apply(a ,[0])
獲取數組中的最大值
傳統方法
var arr = [1,2,3,4,5,6,7,8,9];
var max = arr[0];
for(var i = 0;i < arr.length;i++){
if(max < arr[i]){
max = arr[i]
}
}
使用 apply 借用 Math.max 獲取數組中最大值
利用 apply 可以傳入參數可以是數組或是偽數組的特性
var arr = [1,2,3,4,5,6,7,8,9];
Math.max.apply(null, arr);
創建對象的幾種模式
了解了四種函數調用模式, 我們可以深一步了解創建對象的幾種方式, 對象是通過構造函數創建的
- 工廠模式
特點:- 大量重復執行的代碼, 解決重復實例化的問題
- 函數創建對象並返回
- 最典型的工廠模式就是
document.createElement()
- 無法知道是誰創建了這個實例對象
function createPerson(name, age, gender){ var o = {}; o.name = name; o.age = age; o.gender = gender; return o; }
- 構造方法
特點:- 解決了重復實例化問題
- 能夠知道是誰創建了這個對象(constructor)
- 需要通過 new 運算符穿件對象
function Person(name,age,gender){ this.name = name; this.age = age; this.gender = gender; }
- 寄生式構造函數創建對象
特點:- 外表看起來就是構造犯法, 但本質不是通過構造方法創建對象
- 工廠模式 + 構造函數模式
- 不能確定對象的關系, 不推薦使用
function createPerson(name,age,gender){ var o = {}; o.name = name; o.age = age; o.gender = gender; return o; } var p = new createPerson('Bob',19,'male')
- 混合式創建
- 構造函數 + 原型
- 解決了構造函數傳參和共享的問題
- 不共享的參數使用構造函數
- 共享的使用原型
- 這種混合模式很好的解決了傳參和引用共享的難題
function createPerson(name,age,gender){ this.name = name; this.age = age; this.gender = gender; } createPerson.prototype = { constructor : createPerson, wife : '高圓圓' }
- 借用構造函數繼承(對象冒充)
特點:- 借用構造函數(對象冒充)只能繼承構造函數的成員, 無法繼承原型的成員
function Person(name,age,gender){ this.name = name; this.age = age; this.gender = gender; } function Studner(name,age,gender,course){ // 借用構造函數Person, 創建 Student 對象 Person.call(this,name,age,gender); this.course = course; } var boy = new Student('Bob',19,'male',;Math);
- 寄生式繼承
特點:- 原型 + 工廠模式
- 通過臨時中轉
// 臨時中轉 function person(o){ function Foo(){}; F.prototype = o; return new F(); } // 寄生函數 function create(o){ var fn = person(o); fn.move = function(){}; fn.eat = function(){}; fn.sleep = function(){}; return fn; } var boy = { name : 'Bob', age : 19, famliy : ['father','mother','wife'] } var boy1 = create(boy); console.log(boy1.name); console.log(boy1.family); // 此時 boy1 有了 boy 的成員
經典例題
例題 1
function Foo(){
getName = function(){ alert(1); };
return this;
}
function getName(){
alert(5);
}
Foo.getName = function(){ alert(2); };
Foo.prototype.getName = function(){ alert(3); };
getName = function(){ alert(4); };
Foo.getName(); // 2
getName(); // 4
Foo().getName(); // 4 1
getName(); // 4 1
new Foo.getName(); // 2
new Foo().getName(); // 3
new new Foo().getName(); // 3
分析
- 預解析
- 函數名 Foo 和函數名 getName 聲明提升,函數名和函數體綁定
- 執行代碼
- 執行 Foo.getName();
- 輸出 2
- 執行 getName();
- 此時 getName 是在全局中, 未被修改, 輸出4
- Foo().getName();
- 此事 Foo() 只是一個函數, 執行完成后 getName 被重新賦值
- getName 因為被重新賦值為 1, 輸出1
- getName()
- 由於 getName 被重新賦值, 所以輸出 1
- new Foo.getName();
- Foo.getName 並未被修改
- new 沒有起任何作用
- 輸出2
- new Foo().getName();
- 構造函數創建了實例對象
- 對象中沒有 getName 方法, 要從對象的構造函數中的原型中尋找
- 在 Foo.prototype 中得到 getName 輸出為 3
- new new Foo().getName();
- 構造函數創建了實例對象
- 對象中沒有 getName 方法, 要從對象的構造函數中的原型中尋找
- new 沒有起作用
- 在 Foo.prototype 中得到 getName 輸出為 3
例題 2
var length = 10;
function fn() {
console.log( this.length );
}
var obj = {
length: 5,
method: function ( fn ) {
fn();
arguments[ 0 ](); // [ fn, 1, 2, 3, length: 4]
}
};
obj.method( fn, 1, 2, 3 );
分析
- 預解析
- 變量名 length, obj 和 函數名fn 聲明提升, 函數名和函數體綁定
- 執行代碼
- length = 10, 此時 length 可以看作是 window 下的屬性
- obj = {}, 進行賦值
- 執行 obj 中的 method 方法
- 將 函數體 fn 進行傳參
- 跳進函數 fn,執行函數 fn(), 函數中的 this 指的是 window 下的 length, 為10
- 明確argument是一個對象, argument[0] 指的是 fn
- 使用對象調用函數, 這里的 this 指的是 argument 這個對象
- 得到 argument.length 為 4
- 最后輸出結果為 10 4
例題 3
var o = {
method : function(){
console.log(this);
}
};
o.method();
var f = o.method;
f();
(o.method)();
var obj = {};
(obj.fn = o.method)();
(obj.fn)();
分析
- 預解析
- 變量名 o, f ,obj 聲明提升
- 執行函數
- o = {},進行賦值
- 執行o.method方法, 這里的 this 指的是 o 這個對象
- 將 o.method 函數體,賦值給f
- 執行 f(), 這里的 this 指的是window
- (o.method)是一個函數表達式, 和 o.method() 的結果一致
- obj = {}, obj進行賦值
- o.method 的函數體, 賦值給obj.fn, 執行之后, 這里的 this 指的是window
- (obj.fn)是一個函數表達式, 和 obj.fn() 的結果一致, tshi 指向 obj