更新:謝謝你們的支持,最近折騰了一個博客官網出來,方便你們系統閱讀,後續會有更多內容和更多優化,猛戳這裏查看前端
------ 如下是正文 ------webpack
this
的綁定規則總共有下面5種。git
調用位置就是函數在代碼中被調用的位置(而不是聲明的位置)。github
查找方法:web
分析調用棧:調用位置就是當前正在執行的函數的前一個調用中面試
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;
語句,運行時調試器會在那個位置暫停,同時展現當前位置的函數調用列表,這就是調用棧。找到棧中的第二個元素,這就是真正的調用位置。跨域
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
})();
複製代碼
當函數引用有上下文對象時,隱式綁定規則會把函數中的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(); // <-- 調用位置!
}
複製代碼
經過call(..)
或者 apply(..)
方法。第一個參數是一個對象,在調用函數時將這個對象綁定到this。由於直接指定this的綁定對象,稱之爲顯示綁定。
function foo() {
console.log( this.a );
}
var obj = {
a: 2
};
foo.call( obj ); // 2 調用foo時強制把foo的this綁定到obj上
複製代碼
顯示綁定沒法解決丟失綁定問題。
解決方案:
建立函數bar(),並在它的內部手動調用foo.call(obj),強制把foo的this綁定到了obj。這種方式讓我想起了借用構造函數繼承,沒看過的能夠點擊查看 JavaScript經常使用八種繼承方案
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
複製代碼
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
複製代碼
構造函數
只是使用new
操做符時被調用的普通
函數,他們不屬於某個類,也不會實例化一個類。Number(..)
)在內的全部函數均可以用new
來調用,這種函數調用被稱爲構造函數調用。使用new
來調用函數,或者說發生構造函數調用時,會自動執行下面的操做。
[[Prototype]]
鏈接。this
。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() {
// 建立一個空的對象
var obj = new Object(),
// 得到構造函數,arguments中去除第一個參數
Con = [].shift.call(arguments);
// 連接到原型,obj 能夠訪問到構造函數原型中的屬性
obj.__proto__ = Con.prototype;
// 綁定 this 實現繼承,obj 能夠訪問到構造函數中的屬性
var ret = Con.apply(obj, arguments);
// 優先返回構造函數返回的對象
return ret instanceof Object ? ret : obj;
};
複製代碼
使用這個手寫的new
function Person() {...}
// 使用內置函數new
var person = new Person(...)
// 使用手寫的new,即create
var person = create(Person, ...)
複製代碼
代碼原理解析:
一、用new Object()
的方式新建了一個對象obj
二、取出第一個參數,就是咱們要傳入的構造函數。此外由於 shift 會修改原數組,因此 arguments
會被去除第一個參數
三、將 obj
的原型指向構造函數,這樣obj
就能夠訪問到構造函數原型中的屬性
四、使用apply
,改變構造函數this
的指向到新建的對象,這樣 obj
就能夠訪問到構造函數中的屬性
五、返回 obj
st=>start: Start
e=>end: End
cond1=>condition: new綁定
op1=>operation: this綁定新建立的對象,
var bar = new foo()
cond2=>condition: 顯示綁定
op2=>operation: this綁定指定的對象,
var bar = foo.call(obj2)
cond3=>condition: 隱式綁定
op3=>operation: this綁定上下文對象,
var bar = obj1.foo()
op4=>operation: 默認綁定
op5=>operation: 函數體嚴格模式下綁定到undefined,
不然綁定到全局對象,
var bar = foo()
st->cond1
cond1(yes)->op1->e
cond1(no)->cond2
cond2(yes)->op2->e
cond2(no)->cond3
cond3(yes)->op3->e
cond3(no)->op4->op5->e
複製代碼
在new
中使用硬綁定函數的目的是預先設置函數的一些參數,這樣在使用new
進行初始化時就能夠只傳入其他的參數(柯里化)。
function foo(p1, p2) {
this.val = p1 + p2;
}
// 之因此使用null是由於在本例中咱們並不關心硬綁定的this是什麼
// 反正使用new時this會被修改
var bar = foo.bind( null, "p1" );
var baz = new bar( "p2" );
baz.val; // p1p2
複製代碼
把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
複製代碼
間接引用下,調用這個函數會應用默認綁定規則。間接引用最容易在賦值時發生。
// 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
複製代碼
new
除外),防止函數調用應用默認綁定規則。可是會下降函數的靈活性,使用硬綁定以後就沒法使用隱式綁定或者顯式綁定來修改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 );
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;
};
}
複製代碼
使用:軟綁定版本的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
複製代碼
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機制。
bind(..)
,儘可能避免使用 self = this
和箭頭函數。代碼1:
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}
checkscope()();
複製代碼
代碼2:
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}
var foo = checkscope();
foo();
複製代碼
上面的兩個代碼中,checkscope()
執行完成後,閉包f
所引用的自由變量scope
會被垃圾回收嗎?爲何?
解答:
checkscope()
執行完成後,代碼1中自由變量特定時間以後回收,代碼2中自由變量不回收。
首先要說明的是,如今主流瀏覽器的垃圾回收算法是標記清除,標記清除並不是是標記執行棧的進出,而是從根開始遍歷,也是一個找引用關係的過程,可是由於從根開始,相互引用的狀況不會被計入。因此當垃圾回收開始時,從Root(全局對象)開始尋找這個對象的引用是否可達,若是引用鏈斷裂,那麼這個對象就會回收。
閉包中的做用域鏈中 parentContext.vo 是對象,被放在堆中,棧中的變量會隨着執行環境進出而銷燬,堆中須要垃圾回收,閉包內的自由變量會被分配到堆上,因此當外部方法執行完畢後,對其的引用並無丟。
每次進入函數執行時,會從新建立可執行環境和活動對象,但函數的[[Scope]]
是函數定義時就已經定義好的(詞法做用域規則),不可更改。
checkscope()
執行時,將checkscope
對象指針壓入棧中,其執行環境變量以下
checkscopeContext:{
AO:{
arguments:
scope:
f:
},
this,
[[Scope]]:[AO, globalContext.VO]
}
複製代碼
執行完畢後出棧,該對象沒有綁定給誰,從Root開始查找沒法可達,此活動對象一段時間後會被回收
checkscope()
執行後,返回的是f
對象,其執行環境變量以下
fContext:{
AO:{
arguments:
},
this,
[[Scope]]:[AO, checkscopeContext.AO, globalContext.VO]
}
複製代碼
此對象賦值給var foo = checkscope();
,將foo
壓入棧中,foo
指向堆中的f
活動對象,對於Root
來講可達,不會被回收。
若是必定要自由變量scope
回收,那麼該怎麼辦???
很簡單,foo = null;
,把引用斷開就能夠了。
依次給出console.log輸出的數值。
var num = 1;
var myObject = {
num: 2,
add: function() {
this.num = 3;
(function() {
console.log(this.num);
this.num = 4;
})();
console.log(this.num);
},
sub: function() {
console.log(this.num)
}
}
myObject.add();
console.log(myObject.num);
console.log(num);
var sub = myObject.sub;
sub();
複製代碼
進階系列文章彙總以下,內有優質前端資料,以爲不錯點個star。
我是木易楊,網易高級前端工程師,跟着我每週重點攻克一個前端面試重難點。接下來讓我帶你走進高級前端的世界,在進階的路上,共勉!