最近在拜讀《你不知道的js》,而此篇是對於《你不知道的js》中this部分的筆記整理,但願能有效的梳理,而且鞏固關於this的知識點瀏覽器
調用位置就是函數在代碼中被調用的位置(而不是聲明的位置)安全
關鍵:分析調用棧,即爲了到達當前執行位置所調用的全部函數。而咱們關心的調用位置就在當前正在執行的函數的前一個調用中bash
先來看一段代碼:app
function baz() {
//當前調用棧是:baz
// 所以,當前調用位置是全局做用域
console.log("baz");
bar(); // bar的調用位置
}
function bar() {
// 當前調用棧是baz -> bar
// 所以,當前調用位置在baz中
console.log("bar");
foo(); // foo的調用位置
}
function foo() {
// 當前調用棧是baz -> bar -> foo
// 所以,當前調用位置在bar中
console.log("foo");
}
baz(); // <-- baz的調用位置
複製代碼
咱們能夠把調用棧想象成一個函數調用鏈,但這種方法麻煩且易出錯。函數
但咱們可使用另外一種方式:使用瀏覽器的調試工具,設立斷點,或直接在代碼中插入debugger。運行代碼時,調試器會在那個位置暫停,同時會展現當前位置的函數調用列表,這就是你的調用棧。真正的調用位置是棧中的第二個元素工具
最經常使用的函數調用類型是獨立函數調用。可把這規則看作是沒法應用其餘規則時的默認規則。oop
先看一段代碼:測試
function foo() {
//當前調用棧是:baz
// 所以,當前調用位置是全局做用域
console.log(this.a);
}
var a = 2;
foo(); // 2
複製代碼
從代碼中發現this指向了全局對象,並且函數調用時應用了this的默認綁定。ui
如何判斷是默認綁定?this
可從分析調用位置來看看foo()是如何調用的。在代碼中,foo()是直接使用不帶任何修飾的函數引用進行調用的,所以只能是默認綁定,沒法應用其餘規則
但若是是在嚴格模式下,又會有怎樣的結果呢?請看以下代碼:
function foo() {
"use strict"
console.log(this.a);
}
var a = 2;
foo(); // TypeError:this is undefined
複製代碼
這段代碼表示:雖然this的綁定規則徹底取決於調用位置,但只有在非嚴格模式下,默認綁定才綁定全局對象;在嚴格模式下則會綁定到undefined。
可是在嚴格模式下調用則不影響默認綁定:
function foo() {
console.log(this.a);
}
var a = 2;
(function() {
"use strict"
foo(); // 2
})();
複製代碼
注意:一般來講不該該在代碼中混合使用strict模式與非strict模式
這條規則是指調用位置是否有上下文對象,或者是否被某個對象擁有或包含
先看如下代碼:
function foo() {
console.log(this.a);
}
var obj = {
a: 2,
foo: foo
};
obj.foo();// 2
複製代碼
該調用位置使用了obj上下文來引用函數,或者說函數被調用時obj對象「擁有」或「包含」它。
所以當函數引用有上下文對象時,隱式綁定規則會把函數調用中的this綁定到這個上下文對象
上述代碼調用foo()時,this被綁定到obj,所以this指向了obj,this.a 與 obj.a 是同樣的。
另外對象屬性引用鏈中只有上一層或最後一層在調用位置中起做用。例如:
function foo() {
console.log(this.a);
}
var obj2 = {
a: 42,
foo: foo
};
var obj1 = {
a: 2,
obj2: obj2
};
obj1.obj2.foo();// 42
複製代碼
隱式丟失
被隱式綁定的函數會丟失綁定對象這是一個常見的this綁定問題,也就是說丟失後它會應用默認綁定,從而把this綁定到全局對象或undefined上,取決因而否是嚴格模式。
例1:
function foo() {
console.log(this.a);
}
var obj = {
a: 2,
foo: foo
};
var bar = obj.foo; // 函數別名
var a = "oops, global";// a是全局對象的屬性
bar(); // "oops, global"
複製代碼
雖然bar是obj.foo的引用,但卻引用了foo函數的自己,此時的bar()是不帶任何修飾的函數調用,所以使用了默認綁定
例2:
function foo() {
console.log(this.a);
}
function doFoo(fn) {
// fn其實引用的是foo
fn(); // 調用位置
}
var obj = {
a: 2,
foo: foo
};
var a = "oops, global";// a是全局對象的屬性
doFoo(obj.foo); // "oops, global"
複製代碼
這裏使用了參數傳遞,也是隱式賦值,因此結果和例1同樣
例3:
function foo() {
console.log(this.a);
}
var obj = {
a: 2,
foo: foo
};
var a = "oops, global";// a是全局對象的屬性
setTimeout(obj.foo, 100);// oops, global
複製代碼
回調函數丟失this綁定是常見的,調用回調函數的函數可能會修改this
總結: 分析隱式綁定時,咱們必須在一個對象內部包含一個指向函數的屬性,並經過這個屬性間接引用函數,從而把this間接(隱式)綁定到這個對象上
方法:可使用call或apply直接指定this的綁定對象
缺點:沒法解決丟失綁定的問題
例:
function foo() {
console.log(this.a);
}
var obj = {
a: 2
};
foo.call(obj); // 2
複製代碼
若是你傳入了一個原始值做爲this綁定對象,這個原始值會被轉換成它的對象形式(new xxx()),這叫裝箱
此爲顯式綁定的一個變種,能夠解決丟失綁定問題 缺點:會大大下降函數的靈活性,使用綁定以後就沒法使用隱式綁定或者顯式綁定來修改this
例:
function foo() {
console.log(this.a);
}
var obj = {
a: 2
};
var bar = function() {
foo.call(obj);
}
bar(); // 2
setTimeout(bar, 100); // 2
// 硬綁定的bar不可能再修改它的this
bar.call(window); // 2
複製代碼
foo.call(obj)強制把this綁定到了obj,以後調用函數bar,它總會在obj上調用foo,這是顯式的強制綁定,叫作硬綁定
典型應用場景一:建立一個包裹函數,負責接收參數並返回值
function foo(something) {
console.log(this.a, something);
return this.a + something;
}
var obj = {
a: 2
};
var bar = function() {
return foo.apply(obj, arguments);
}
var b = bar(3); // 2 3
console.log(b); // 5
複製代碼
典型應用場景二:建立一個能夠重複使用的輔助函數
function foo(something) {
console.log(this.a, something);
return this.a + something;
}
// 簡單的輔助綁定函數
function bind(fn, obj) {
return function() {
return fn.apply(obj, arguments)
}
}
var obj = {
a: 2
};
var bar = bind(foo, obj);
var b = bar(3); // 2 3
console.log(b); // 5
複製代碼
因爲硬綁定是一種經常使用模式,因此ES5提供了內置方法Function.prototype.bind:
function foo(something) {
console.log(this.a, something);
return this.a + something;
}
var obj = {
a: 2
};
var bar = foo.bind(obj);
var b = bar(3); // 2 3
console.log(b); // 5
複製代碼
bind會返回一個硬編碼的新函數,會把你指定的參數位置爲this的上下文並調用原始函數
經過 call() 或 apply() 實現
使用new來調用函數,或者說發生構造函數調用時,會自動執行下面操做 a、建立一個全新對象 b、新對象會被執行[[Prototype]]連接 c、新對象被綁定到函數調用的this d、若是函數沒有返回其餘對象,則自動返回新對象 代碼:
var obj = {};
obj.__proto__ = Base.prototype;
var result = Base.call(obj);
return typeof result === 'obj' ? result : obj;
複製代碼
function foo() {
console.log(this.a);
}
var obj1 = {
a: 2,
foo: foo
};
var obj2 = {
a: 3,
foo: foo
};
obj1.foo(); // 2
obj2.foo(); // 3
obj1.foo.call(obj2); // 3
obj2.foo.call(obj1); // 2
複製代碼
顯然:顯式綁定 > 隱式綁定
function foo(something) {
this.a = something;
}
var obj1 = {
foo: foo
};
var obj2 = {};
obj1.foo(2);
console.log(obj1.a);// 2
obj1.foo.call(obj2, 3); // 3
console.log(obj2.a);// 3
var bar = new obj1.foo(4);
console.log(obj1.a);// 2
console.log(bar.a);// 4
複製代碼
new綁定 > 隱式綁定
new和call/apply沒法一塊兒使用,所以沒法經過new foo.call(obj1) 來直接測試,但咱們可使用硬綁定來測試
function foo(something) {
this.a = something;
}
var obj1 = {};
var bar = foo.bind(obj1);
bar(2);
console.log(obj1.a);// 2
var baz = new bar(3);
console.log(obj1.a);// 2
console.log(bar.a);// 3
複製代碼
這裏bar被硬綁定在了obj1上,但new bar(3)並無把obj1.a修改成3。相反,new修改了硬綁定(到obj1的)調用bar()中的this。由於使用了new綁定,咱們獲得了一個名爲baz的新對象,而且baz.a的值爲3 new綁定 > 硬綁定(顯式綁定)
(1)、由new調用? 綁定到新建立的對象(new綁定)
var bar = new foo();
複製代碼
(2)、由call或apply或bind調用?綁定到指定對象(顯式綁定)
var bar = foo.call(obj2);
複製代碼
(3)、由上下文對象調用?綁定到那個上下文對象(隱式綁定)
var bar = obj1.foo();
複製代碼
(4)、默認綁定:嚴格模式下綁定到undefined,不然爲全局對象
var bar = foo();
複製代碼
若是你把null貨undefined做爲this的綁定對象傳入call、apply、bind,這些值在調用時會被忽略,實際應用默認綁定規則:
function foo() {
console.log(this.a);
}
var a = 2;
foo.call(null); // 2
複製代碼
function foo(a, b) {
console.log("a:"+ a + ", b:" + b);
}
foo.apply(null, [2, 3]);// a:2, b:3
var bar = foo.bind(null, 2);
bar(3); // a:2, b:3
複製代碼
老是用null來忽略this綁定可能會產生一些反作用。若是某個函數使用了this(如第三方庫中的一個函數),那默認綁定規則會把this綁定到全局對象(瀏覽器中爲window),這會致使不可預計的後果(如修改全局對象),或者致使更多難以分析和追蹤的bug
更安全的this
一種更安全的作法是傳入一個特殊對象,把this綁定到這個對象不會對你的程序產生任何反作用。
可建立一個"DMZ"非軍事區對象,一個空的非委託的對象,任何對於this的使用都會被限制在這個空對象中,不會對全局對象產生任何影響
function foo(a, b) {
console.log("a:"+ a + ", b:" + b);
}
// 咱們的DMZ空對象
var __null = Object.create(null);
foo.apply(__null, [2, 3]);// a:2, b:3
var bar = foo.bind(__null, 2);
bar(3); // a:2, b:3
複製代碼
間接引用的狀況下,調用這個函數會應用默認綁定規則,而且最容易在賦值時發生:
function foo(a, b) {
console.log(this.a);
}
var a = 2;
var o = { a: 3, foo: foo };
var p = { a: 4 };
o.foo();// 3
(p.foo = o.foo)(); // 2
複製代碼
賦值表達式p.foo = o.foo的返回值是目標函數的引用,所以調用位置是foo() 而不是p.foo()或o.foo(),這裏會使用默認綁定
對於默認綁定來講,決定this綁定對象的並非調用位置是否處於嚴格模式,而是函數體是否處於嚴格模式
給默認綁定指定一個全局對象和undefined之外的值,可實現和硬綁定相同的效果,同時保留隱式綁定或顯式綁定修改this的能力
if(!Function.prototype.softBind) {
Function.prototype.softBind = function(obj) {
var fn = this;
// 捕獲全部curried參數
var curried = [].slice.call(arguments, 1);
var bound = function() {
return fn.apply(
(!this || this === (window || global)) ?
obj : this,
curried.concat.apply(curried, arguments)
);
};
bound.prototype = Object.create(fn.prototype);
return bound;
}
}
複製代碼
function foo(a, b) {
console.log("name: " + this.name);
}
var obj = { name: 'obj' },
obj2 = { name: 'obj2' },
obj3 = { name: 'obj3' };
var fooOBJ = foo.softBind(obj);
fooOBJ(); // name: obj
obj2.foo = softBind(obj);
obj2.foo(); // name: obj2
fooOBJ.call(obj3); // name: obj3
setTimeout(obj2.foo, 100); // name: obj 使用了軟綁定
複製代碼
從上述代碼中能夠看到軟綁定版本的foo()能夠手動將this綁定到obj2或obj3上,但若是應用默認綁定,則會將this綁定到obj
箭頭函數不使用this的四種標準規則,而是根據外層(函數或全局)做用域來決定this
function foo() {
// 返回一個箭頭函數
return (a) => {
// this繼承自foo();
console.log(this.a);
};
}
var obj1 = {
a: 2
};
var obj2 = {
a: 3
};
var bar = foo.call(obj1);
bar.call(obj2);// 2, 不是3
複製代碼
foo()內部建立的箭頭函數會捕獲調用時foo()的this。因爲foo()的this綁定到obj1,bar(引用箭頭函數)的this也會綁定到obj1,箭頭函數的綁定沒法被修改(new也不行)
function foo() {
var self = this;
setTimeout(function(){
console.log(self.a);
}, 100);
}
var obj = {
a: 2
};
foo.call(obj);// 2
複製代碼
self=this與箭頭函數均可以取代bind,但本質上是替代了this機制
常常編寫this風格代碼,但絕大部分時候會使用self=this或箭頭函數來否認this機制,應當注意如下兩點:
a、只是用詞法做用域並徹底拋棄錯誤this風格的代碼
b、徹底採用this風格,在必要時使用bind(),儘可能避免使用self=this和箭頭函數
兩種風格混用一般會使代碼更難維護,而且可能也會更難編寫