講下函式呼叫
- 函式呼叫三種方法(ES5):
- func(p1, p2)
- obj.child.method(p1, p2)
- func.call(context, p1, p2) // 先不講 apply
- 第三中呼叫形式才是正確的呼叫形式,其他兩種都是語法糖,可以等價的變換:
// 稱此程式碼為「轉換程式碼」
func(p1, p2) 等價於
func.call(undefined, p1, p2)
obj.child.method(p1, p2) 等價於
obj.child.method.call(obj.child, p1, p2)
// 第二個等價舉例
var obj = {
foo: function(){
console.log(this)
}
}
obj.foo()
// 等價於
obj.foo.call(obj)
複製程式碼
- 所以,this 就是你 call 一個函式時,傳入的 context。
- 如果你的函式呼叫形式不是 call 形式,請按照「轉換程式碼」將其轉換為 call 形式。
- 但是因為本文章是後來更新的緣故,只用「轉換程式碼」來講解其中的[]呼叫,其他的呼叫還是按照文章初創時的思路;
獨立呼叫
- 預設繫結規則:this繫結給window;
- 在嚴格模式下,預設繫結規則會把this繫結undefined上;
function foo() {
console.log( this.a );
}
var a = 2;
(function(){
"use strict";
foo(); //2
})();
複製程式碼
- 這裡有一個微妙但是非常重要的細節,雖然 this 的繫結規則完全取決於呼叫位置,但是隻有 foo()執行在非 嚴格模式下時,預設繫結才能繫結到全域性物件; 嚴格模式下呼叫foo()不會影響預設繫結規則;
function foo() {
"use strict";
console.log( this.a );
}
var a = 2;
foo(); //undefined
複製程式碼
- 無論函式是在哪個作用域中被呼叫,只要是獨立呼叫則就會按預設繫結規則被繫結到全域性物件或者undefined上。
隱式呼叫:(使用物件的屬性呼叫)
隱式繫結
-
隱式繫結的規則:
- this給離函式最近的那個物件;
- 判斷函式呼叫位置是否有上下文物件,或者說是否被某個物件擁有或者包含;
//隱式繫結的規則是呼叫位置是否有上下文物件,或者說是否被某個物件擁有或者包含 //當函式引用有上下文物件時,隱式繫結規則會把函式呼叫中的 this 繫結到這個上下文物件 function foo() { console.log( this.a );//2 } var obj = { a: 2, foo: foo }; obj.foo(); 複製程式碼
- 當函式引用有上下文物件時,隱式繫結規則會把函式呼叫中的 this 繫結到這個上下文物件;
- 物件屬性引用鏈中只有最頂層或者說最後一層會影響呼叫位置;
//物件屬性引用鏈中只有最頂層或者說最後一層會影響呼叫位置 function foo() { console.log( this.a ); } var obj2 = { a: 42, foo: foo }; var obj1 = { a: 2, obj2: obj2 }; obj1.obj2.foo(); //42 複製程式碼
隱式丟失
- 將函式通過隱式呼叫的形式賦值給一個變數;
注意:經典面試題,這是一個隱式丟失:
function foo() {
console.log( this.a );//oops, global
}
var a = "oops, global";
var obj = {
a: 2,
foo: foo
};
var bar = obj.foo; //把obj.foo賦予別名bar,造成了隱式丟失,因為只是把foo()函式賦給了bar,而bar與obj物件則毫無關係
bar();
//等價於
var a = "oops, global";
var bar = function foo(){
console.log( this.a );
}
bar();//oops, global
複製程式碼
- 將函式通過隱式呼叫的形式進行傳參;
var a = 0;
function foo(){
console.log(this.a);
};
function bar(fn){
fn();
}
var obj = {
a : 2,
foo:foo
}
//把obj.foo當作引數傳遞給bar函式時,有隱式的函式賦值fn=obj.foo。與上例類似,只是把foo函式賦給了fn,而fn與obj物件則毫無關係。
bar(obj.foo);//0
//等價於
var a = 0;
function bar(fn){
fn();
}
bar(function foo(){
console.log(this.a);
});
複製程式碼
- 內建函式:內建函式與上例類似,也會造成隱式丟失
var a = 0;
function foo(){
console.log(this.a);
};
var obj = {
a : 2,
foo:foo
}
setTimeout(obj.foo,100);//0
//等價於
var a = 0;
setTimeout(function foo(){
console.log(this.a);
},100);//0
複製程式碼
顯式繫結:call()、apply()、bind()
- 通過call()、apply()、bind()方法把物件繫結到this上,叫做顯式繫結。
//普通物件的屬性查詢
function foo(a,b) {
console.log( this.a,a,b );
}
var obj = {
a:2
};
foo.call( obj,"a","b"); //2 a b
foo.apply(obj,["a","b"])//2 a b
複製程式碼
- 顯式繫結規則:call,apply和bind指定的物件(第一個引數);
- 硬繫結:硬繫結是顯式繫結的一個變種,使this不能再被修改。它有一個包裹函式,有一個目標函式的顯示呼叫(bind,返回只是一個函式);可以用來解決隱式丟失。
// 我們來看看這個顯式繫結變種到底是怎樣工作的。我們建立了函式 bar() ,並在它的內部手動呼叫了 foo.call(obj) ,因此強制把 foo 的 this 繫結到了 obj 。無論之後如何呼叫函式 bar ,它總會手動在 obj 上呼叫 foo 。這種繫結是一種顯式的強制繫結,因此我們稱之為硬繫結。
function foo() {
console.log( this.a );
}
var a =1;
var obj = {a:2};
var obj_test = {a:"test"};
var bar = function() {
console.log( this.a );
foo.call( obj );};
bar(); // 1 2
setTimeout( bar, 1000 ); // 1 2
bar.call( obj_test ); //test 2
//硬繫結的bar不可能再修改它的this(指的是foo中的this)
//硬繫結的典型應用場景就是建立一個包裹函式,傳入所有的引數並返回接收到的所有值
function foo(arg1,arg2) {
console.log( this.a,arg1,arg2);
return this.a + arg1;
}
var obj = {a:2};
var bar = function() {
return foo.apply( obj, arguments);
};
var b = bar(3,2); // 2 3 2
console.log( b ); // 5
複製程式碼
new繫結
//3. 這個新物件會繫結到函式呼叫的 this 。
function foo(a) {
this.a = a;
}
var bar = new foo(2);
console.log( bar.a ); // 2
//使用 new 來呼叫 foo(..) 時,我們會構造一個新物件並把它繫結到 foo(..) 呼叫中的 this 上。 new 是最
//後一種可以影響函式呼叫時 this 繫結行為的方法,我們稱之為 new 繫結。
複製程式碼
繫結例外
箭頭函式
- 箭頭函式:this的繫結和作用域有關。如果在當前的箭頭函式作用域中找不到變數,就向上一級作用域裡去找。
- 箭頭函式內部的 this 是詞法作用域,由上下文確定,此作用域稱作 Lexical this ,在程式碼執行前就可以確定。沒有其他大佬可以覆蓋。
- 這樣的好處就是方便讓回撥函式的this使用當前的作用域,不怕引起混淆。所以對於箭頭函式,只要看它在哪裡建立的就行。
function foo() {
setTimeout(() => {
console.log('id:', this.id); //id: 42
}, 100);
}
var id = 21;
foo.call({ id: 42 })
// 再舉個栗子
var returnThis = () => this
returnThis() // window
new returnThis() // TypeError
var boss1 = {
name: 'boss1',
returnThis () {
var func = () => this
return func()
}
}
returnThis.call(boss1) // still window
var boss1returnThis = returnThis.bind(boss1)
boss1returnThis() // still window
boss1.returnThis() // boss1
var boss2 = {
name: 'boss2',
returnThis: boss1.returnThis
}
boss2.returnThis() // boss2
複製程式碼
被忽略的this:null\undefined
- 當被繫結的是null 或者 undefined,則this是預設的 context(嚴格模式下預設 context 是 undefined);
// 如果你把 null 或者 undefined 作為 this 的繫結物件傳入 call 、 apply 或者 bind ,這些值在呼叫時會被忽略,實際應用的是預設繫結規則;
function foo() {
console.log( this.a );
}
var a = 2222;
foo.call( null ); // 2
複製程式碼
柯里化
function foo(a,b) {
console.log( "a:" + a + ", b:" + b );
}
// 把陣列“展開”成引數
foo.apply( null, [2, 3] ); // a:2, b:3
// 使用 bind(..) 進行柯里化
var bar = foo.bind( null, [2] );
bar( 3 ); // a:2, b:3
//升級版:不會汙染全域性
function foo(a,b) {
this
console.log( "a:" + a + ", b:" + b );
}
// 我們的DMZ空物件,“DMZ”(demilitarized zone,非軍事區)
var ø = Object.create( null );//{}
// 把陣列展開成引數
foo.apply( ø, [2, 3] ); // a:2, b:3
// 使用bind(..)進行柯里化
var bar = foo.bind( ø, 2 );
bar( 3 ); // a:2, b:3
複製程式碼
[]呼叫
- 很特殊的一個呼叫,有點出乎意料的結果,特此記錄;
function fn (){ console.log(this) }
var arr = [fn, fn2]
arr[0]() // 這裡面的 this 又是什麼呢?
複製程式碼
- 這個可以用文章最前面講的函式呼叫方法來想:
// 等價於
arr[0]()
假想為 arr.0()
然後轉換為 arr.0.call(arr)
那麼裡面的 this 就是 arr 了
複製程式碼
總結
- this是函式執行的上下文物件;
- 根據函式呼叫的方式不同this的值也不同:
- 1.以函式的形式直接呼叫,this是window
- 2.以物件方法的形式呼叫,this是呼叫方法的物件
- 3.以建構函式的形式呼叫,this是新建立的那個物件
- 4.使用call、apply和bind呼叫的函式,第一個引數就是this
- 5.在建構函式 prototype 屬性中被呼叫,this仍然指的是例項物件;
- 6.在事件處理函式中:this是指觸發當前事件的HTML DOM節點元素;
- 7.箭頭函式中,this指的是建立時的詞法作用域;
- 注意:
- 在函式中 this 到底取何值,是在函式真正被呼叫執行的時候確定下來的,函式定義的時候確定不了。
- this本身不具備任何含義。
<script>
// TODO 一、以全域性&呼叫普通的函式的形式呼叫,this是window.
function fn(){
console.log(this);
}
fn();
//二、建構函式
//如果函式作為建構函式使用,那麼其中的this就代表即將new出來的物件
function Objfn(){
this.a = 10;
console.log(this);//此時輸出的是物件 Objfn {a: 10}
}
var objfn = new Objfn();
console.log('objfn.a='+objfn.a);//objfn.a=10
//但是如果直接呼叫Objfn1()函式,而不是new Objfn1(),那麼情況就變成了Objfn()是普通函式
function Objfn1(){
this.a = 10;
console.log(this);
}
var objfn1 = Objfn1();//此時輸出的是物件 Window {stop: function, open: function, alert: function, confirm: function, prompt: function…}
//console.log('objfn.a='+objfn1.a);//錯誤,Cannot read property 'a' of undefined
//三、物件方法
//如果函式作為物件的方法,方法中的this指向該物件
var obj={
a:10,
foo:function () {
console.log(this);//Object {a: 10, foo: function}
console.log(this.a);//10
}
}
obj.foo();
//注意,要是此時在物件方法中定義函式,那麼情況就不同了
//此時的函式fn雖然是在 obj1.foo1內部定義的,但它仍然屬於一個普通的函式,this仍然指向window.
var obj1={
a1:10,
foo1:function () {
function fn(){
console.log(this);//Window {stop: function, open: function, alert: function, confirm: function, prompt: function…}
console.log(this.a1);//undefined
}
fn();
}
}
obj1.foo1();
//另外,如果此時foo2不作為物件方法被呼叫,則
var obj2 = {
x: 10,
foo2: function () {
console.log(this); //Window
console.log(this.x); //undefined
}
};
var fn2 = obj2.foo2;
//等價於fn2 = function () {
//console.log(this); //Window
//console.log(this.x); //undefined
//}
fn2();//此時又是在全域性裡執行的普通函式。
//四、建構函式的prototype屬性
//在 Foof.prototype.getX函式中,this 指向的 Foof物件。不僅僅如此,即便是在整個原型鏈中,this 代表的也是當前Foof物件的值。
function Foof(){
this.x = 10;
}
Foof.prototype.getX = function () {
console.log(this); //Foof {x: 10}
console.log(this.x); //10
}
var foof = new Foof();
foof.getX();
//五、函式用call、apply或者bind呼叫
var obja = {
x: 10
}
function fooa(){
console.log(this); //Object {x: 10}
console.log(this.x); //10
}
fooa.call(obja);
fooa.apply(obja);
fooa.bind(obja)();
</script>
複製程式碼