本篇參考《你不知道的JavaScript》與“高級前端進階”公衆號第3-1期。
1 調用位置
調用位置就是函數在代碼中被調用的位置(而不是聲明的位置)。
查找方法:
分析調用棧:調用位置就是當前正在執行的函數的前一個調用中
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;語句,運行時調試器會在那個位置暫停,同時展示當前位置的函數調用列表,這就是調用棧。找到棧中的第二個元素,這就是真正的調用位置。
2 綁定規則 2.1 默認綁定
獨立函數調用,可以把默認綁定看作是無法應用其他規則時的默認規則,this指向全局對象。
嚴格模式下,不能將全局對象用於默認綁定,this會綁定到undefined。只有函數執行上下文在非嚴格模式下,默認綁定才能綁定到全局對象。在嚴格模式下調用函數則不影響默認綁定。
function foo() { // 運行在嚴格模式下,this會綁定到undefined
"use strict";
console.log( this.a );
}
var a = 2;
// 調用
foo(); // TypeError: Cannot read property 'a' of undefined
function foo() { // 運行
console.log( this.a );
}
var a = 2;
(function() { // 嚴格模式下調用函數則不影響默認綁定
"use strict";
foo(); // 2
})();
2.2 隱式綁定
當函數引用有上下文對象時,隱式綁定規則會把函數中的this綁定到這個上下文對象。對象屬性引用鏈中只有上一層或者說最後一層在調用中起作用。
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
obj.foo(); // 2
隱式丟失
被隱式綁定的函數特定情況下會丟失綁定對象,應用默認綁定,把this綁定到全局對象或者undefined上。
// 雖然bar是obj.foo的一個引用,但是實際上,它引用的是foo函數本身。
// bar()是一個不帶任何修飾的函數調用,應用默認綁定。
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
var bar = obj.foo; // 函數別名
var a = "oops, global"; // a是全局對象的屬性
bar(); // "oops, global"
參數傳遞就是一種隱式賦值,傳入函數時也會被隱式賦值。回調函數丟失this綁定是非常常見的。
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"
// ----------------------------------------
// JS環境中內置的setTimeout()函數實現和下面的僞代碼類似:
function setTimeout(fn, delay) {
// 等待delay毫秒
fn(); // <-- 調用位置!
}
2.3 顯式綁定
通過call(…) 或者 **apply(…)**方法。第一個參數是一個對象,在調用函數時將這個對象綁定到this。因爲直接指定this的綁定對象,稱之爲顯示綁定。
function foo() {
console.log( this.a );
}
var obj = {
a: 2
};
foo.call( obj ); // 2 調用foo時強制把foo的this綁定到obj上
顯示綁定無法解決丟失綁定問題。
解決方案:
1、硬綁定
創建函數bar(),並在它的內部手動調用foo.call(obj),強制把foo的this綁定到了obj。
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
典型應用場景是創建一個包裹函數,負責接收參數並返回值。
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,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
2、API調用的“上下文”
JS許多內置函數提供了一個可選參數,被稱之爲“上下文”(context),其作用和bind(…)一樣,確保回調函數使用指定的this。這些函數實際上通過call(…)和apply(…)實現了顯式綁定。
function foo(el) {
console.log( el, this.id );
}
var obj = {
id: "awesome"
}
var myArray = [1, 2, 3]
// 調用foo(..)時把this綁定到obj
myArray.forEach( foo, obj );
// 1 awesome 2 awesome 3 awesome
2.4 new綁定
在JS中,構造函數只是使用new操作符時被調用的普通函數,他們不屬於某個類,也不會實例化一個類。
包括內置對象函數(比如Number(…))在內的所有函數都可以用new來調用,這種函數調用被稱爲構造函數調用。
實際上並不存在所謂的“構造函數”,只有對於函數的“構造調用”。
使用new來調用函數,或者說發生構造函數調用時,會自動執行下面的操作。
- 1、創建(或者說構造)一個新對象。
- 2、這個新對象會被執行[[Prototype]]連接。
- 3、這個新對象會綁定到函數調用的this。
- 4、如果函數沒有返回其他對象,那麼new表達式中的函數調用會自動返回這個新對象。
使用new來調用foo(…)時,會構造一個新對象並把它(bar)綁定到foo(…)調用中的this。
function foo(a) {
this.a = a;
}
var bar = new foo(2); // bar和foo(..)調用中的this進行綁定
console.log( bar.a ); // 2
手寫一個new實現
function create() {
// 創建一個空的對象
let obj = new Object()
// 獲得構造函數
let Con = [].shift.call(arguments)
// 鏈接到原型
obj.__proto__ = Con.prototype
// 綁定 this,執行構造函數
let result = Con.apply(obj, arguments)
// 確保 new 出來的是個對象
return typeof result === 'object' ? result : obj
}
手寫new
function Person() {...}
// 使用內置函數
new var person = new Person(...)
// 使用手寫的new,即create
var person = create(Person, ...)
代碼原理解析:
- 1、用new Object()的方式新建了一個對象obj
- 2、取出第一個參數,就是我們要傳入的構造函數。此外因爲 shift 會修改原數組,所以 arguments會被去除第一個參數
- 3、將 obj的原型指向構造函數,這樣obj就可以訪問到構造函數原型中的屬性
- 4、使用apply,改變構造函數this 的指向到新建的對象,這樣 obj就可以訪問到構造函數中的屬性
- 5、返回 obj
3 綁定例外
3.1 被忽略的this
把null或者undefined作爲this的綁定對象傳入call、apply或者bind,這些值在調用時會被忽略,實際應用的是默認規則。
下面兩種情況下會傳入null
使用apply(…)來“展開”一個數組,並當作參數傳入一個函數
bind(..)可以對參數進行柯里化(預先設置一些參數)
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
總是傳入null來忽略this綁定可能產生一些副作用。如果某個函數確實使用了this,那默認綁定規則會把this綁定到全局對象中。
更安全的this
安全的做法就是傳入一個特殊的對象(空對象),把this綁定到這個對象不會對你的程序產生任何副作用。
JS中創建一個空對象最簡單的方法是Object.create(null),這個和{}很像,但是並不會創建Object.prototype這個委託,所以比{}更空。
function foo(a, b) {
console.log( "a:" + a + ",b:" + b );
}
// 我們的空對象
var ø = Object.create( null );
// 把數組”展開“成參數
foo.apply( ø, [2, 3] ); // a:2,b:3
// 使用bind(..)進行柯里化
var bar = foo.bind( ø, 2 );
bar( 3 ); // a:2,b:3
4.2 間接引用
間接引用下,調用這個函數會應用默認綁定規則。間接引用最容易在賦值時發生。
// p.foo = o.foo的返回值是目標函數的引用,所以調用位置是foo()而不是p.foo()或者o.foo()
function foo() {
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
4.3 軟綁定
硬綁定可以把this強制綁定到指定的對象(new除外),防止函數調用應用默認綁定規則。但是會降低函數的靈活性,使用硬綁定之後就無法使用隱式綁定或者顯式綁定來修改this。
如果給默認綁定指定一個全局對象和undefined以外的值,那就可以實現和硬綁定相同的效果,同時保留隱式綁定或者顯示綁定修改this的能力。
// 如果this綁定到全局對象或者undefined,那就把指定的默認對象obj綁定到this,否則不會修改this
if(!Function.prototype.softBind) {
Function.prototype.softBind = function(obj) {
var fn = this;
// 捕獲所有curried參數
var curried = [].slice.call( arguments, 1 ); //等同於 arguments.slice(1);不要arguments的第一個參數
var bound = function() {
return fn.apply(
(!this || this === (window || global)) ?
obj : this, //此處的global可以在node裏console一下,是js在node下的執行環境
curried.concat.apply( curried, arguments )
);
};
bound.prototype = Object.create( fn.prototype );
return bound;
};
}
使用:軟綁定版本的foo()可以手動將this綁定到obj2或者obj3上,但如果應用默認綁定,則會將this綁定到obj。
function foo() {
console.log("name:" + this.name);
}
var obj = { name: "obj" },
obj2 = { name: "obj2" },
obj3 = { name: "obj3" };
// 默認綁定,應用軟綁定,軟綁定把this綁定到默認對象obj
var fooOBJ = foo.softBind( obj );
fooOBJ(); // name: obj
// 隱式綁定規則
obj2.foo = foo.softBind( obj );
obj2.foo(); // name: obj2
// 顯式綁定規則
fooOBJ.call( obj3 ); // name: obj3
// 綁定丟失,應用軟綁定
setTimeout( obj2.foo, 10 ); // name: obj
5 this詞法
ES6新增一種特殊函數類型:箭頭函數,箭頭函數無法使用上述四條規則,而是根據外層(函數或者全局)作用域(詞法作用域)來決定this。
foo()內部創建的箭頭函數會捕獲調用時foo()的this。由於foo()的this綁定到obj1,bar(引用箭頭函數)的this也會綁定到obj1,箭頭函數的綁定無法被修改(new也不行)。
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!
ES6之前和箭頭函數類似的模式,採用的是詞法作用域取代了傳統的this機制。
function foo() {
var self = this; // lexical capture of this
setTimeout( function() {
console.log( self.a ); // self只是繼承了foo()函數的this綁定
}, 100 );
}
var obj = {
a: 2
};
foo.call(obj); // 2
代碼風格統一問題:如果既有this風格的代碼,還會使用 seft = this 或者箭頭函數來否定this機制。
只使用詞法作用域並完全拋棄錯誤this風格的代碼;
完全採用this風格,在必要時使用bind(…),儘量避免使用 self = this 和箭頭函數。
摯謝閱讀!