Javascript 當中的 this 與其他語言是完全不同的機制,很有可能會讓一些寫其他語言的工程師迷惑。
1. 誤以為 this 指向函數自身
根據 this 的英文文法,很容易將函數中出現的 this 理解為函數本身。在 javascript 當中函數作為一等公民,確實可以在呼叫的時候將屬性值儲存起來。但是如果使用方法不對,就會發生與實際預期不一致的情況。具體情況,請看下面程式碼
function fn(num){ this.count++; } fn.count = 0; for(var i=0;i<3;i++){ fn(i); } console.log(fn.count); // 0
如果 fn 函數裡面的 this 指向自身函數,那麼 count 屬性的屬性值就應該產生變化,但實際上卻是紋絲不動。對於這個問題,有些人會利用作用域來解決,例如這麼寫
var data = { count:0 }; function fn(num){ data.count++; } for(var i=0;i<3;i++){ fn(i); } console.log(data.count); //3
又或者更直接的這樣寫
function fn(num){ fn.count++; } fn.count = 0; for(var i=0;i<3;i++){ fn(i); } console.log(fn.count);//3
雖然這兩種方式都輸出了正確的結果,但是卻避開了 this 到底綁定在哪裡的問題。如果對一個事物的工作原理不清晰,就往往會產生頭痛治頭,腳痛治腳的問題,從而導致代碼變得的醜陋,而且維護性也會變得很差。
2. this神奇的綁定規則
2.1 預設綁定規則
第一種是最常見的 this 的綁定,看看下面的程式碼
function fn(){ console.log(window === this); //浏览器环境 } fn(); //true
函數fn 是直接在全域作用域下呼叫的,沒有帶其他任何修飾,這種情況下,函數呼叫的時候使用了 this 的預設綁定,指向了全域物件。
這樣就清楚了第一個例子中的this 指向, fn 函數中的this 指向了全域變量,所以this.count++ 相當於window.count++(瀏覽器環境下),當然不會對fn 函數的count屬性產生影響。
有一點要說明的是,上面種情況只能在非嚴格模式(strict mode)下才能發生,在嚴格模式下,會將 this 預設綁定為 undefined。以避免全域變數的污染。
2.2 隱式綁定規則
如果函數在以物件為上下文進行調用,那麼 this 的綁定就會產生變化。 this 會綁定到呼叫這個函數的對象,看看下面程式碼:
var obj = { a:1, fn:function(){ console.log(this.a); } } obj.fn(); //1
即使函數宣告不在物件當中,this 指向仍會產生變化
function fn(){ console.log(this.a); } var obj = { a:1, fn:fn } obj.fn(); //1
由此可見,this 的綁定,不與函數定義的位置有關,而是與呼叫者和呼叫方式有關。
在隱式的綁定規則下,有一些特殊的地方,需要注意。
2.2.1 多層物件呼叫 this 的指向
function fn(){ console.log(this.a); } var obj = { a:1, obj2:obj2 } var obj2 = { a:2, obj3:obj3 } var obj3 = { a:3, fn:fn } obj.obj2.obj3.fn(); //3
在多層物件參考下,this 指向的是呼叫的函數的那個物件。
2.2.2 隱式賦值可能有遺失現象
看下面程式碼
function fn(){ console.log(this); } var obj = { fn:fn } var fun = obj.fn; fun(); //window
雖然 fn 引用了 obj.fun ,但是函數的呼叫方式,仍是不帶任何修飾的,所以 this 還是綁定在了 window 上。
還有一種情況,容易讓大家忽略,那就是傳參的時候,其實會隱式賦值。
function fn(){ console.log(this); } function doFn(fn){ fn(); } var obj = { fn:fn } doFn(obj.fn); //window
隱式綁定 this 不是一種很推薦的方式,因為很有可能就發生丟失的情況,如果業務當中對 this 的綁定有要求,建議還是使用顯示綁定的方式。
2.3 明確綁定規則
顯示綁定就是利用函數原型上的 apply 與 call 方法來對 this 進行綁定。用法就是把想要綁定的物件當作第一個參數傳進去。
function fn(){ console.log(this); } var obj = {}; fn.call(obj); //{}
有些時候會想將函數的 this 綁定在某個物件上,但是不需要立即調用,這樣的話,直接利用 call 或 apply 是無法做的。
function fn(){ console.log(this); } function bind(fn){ fn(); } var obj = { fn:fn } bind.call(obj,fn); //window
上面這個例子,看似好像可以,但實際上是 bind 函數的 this 綁定到了 obj 這個對象,但是 fn 仍然是沒有任何修飾的調用,所以 fn 仍然是默認的綁定方式。
function fn(){ console.log(this); } function bind(fn,obj){ return function(){ fn.apply(obj,arguments); } } var obj = { fn:fn } var fun = bind(fn,obj); fun(); //obj
这样调用,就可以将灵活多变的 this ,牢牢的控制住了,因为 fn 的调用方式为 apply 调用。所以,this 就被绑定在传入的 obj 对象上,在 ES5 当中,函数的原型方法上多了一个 bind。效果与上面的函数基本一致,具体用法限于篇幅就不多说了。
2.4 new 绑定
new 是一个被很多人误解的一个关键字,但实际上 javascript 的 new 与传统面向对象的语言完全不同。
个人把 new 理解为一种特殊的函数调用,当使用 new 关键字来调用函数的时候,会执行下面操作,
function fn(a){ this.a = a; } fn.prototype.hi = function(){ console.log('hi') } var obj = new fn(2); console.log(obj); function fn(a){ this.a = a; return {}; } var obj = new fn(2); console.log(obj); //{}
2.5 特殊的传参
null 和 undefined 也是可以作为 this 的绑定对象的,但是实际上应用的是默认的绑定。
但是这种传参的实际效用是什么呢?
常见的用法是将一个数组展开,作为参数传入参数。比如
function fn(a,b){ console.log('a:',a,'b:',b); } fn.apply(null,[1,2]); // a: 1 b: 2
但是这种用法会有一个坑,那就是如果函数存在了 this ,那么就会应用默认的绑定规则,将 this 绑定在全局对象上,发生于预期不一致的情况。为了代码更加稳健,可以使创建一个比空对象更空的对象。
var obj = Object.create(null); console.log(obj.__proto__); //undefined var obj2 = {} console.log(obj2.__proto__); //Object {}
Object原型上有一个 create 方法,这个方法会创建一个对象,然后将对象的原型指向传入的参数,所以传入 null 的话,产生一个没有 prototype 的对象,所以会比空对象更加"空"。
所以传入这个对象,会比传入 null 更加安全。
var obj = Object.create(null); fn.apply(obj,[1,2]);
2.6 根据作用域来决定 this 的绑定
在 ES6 当中,出现了一个新的函数类型,箭头函数。
如果使用箭头函数,那么就不会使用上面提到的四种 this 绑定方式,而是根据作用域来决定
比较常见的是用于事件函数和定时器的情况。
下面是比较常见的传统 this 写法
function fn(){ var _this = this; setTimeout(function(){ console.log(_this.a); },100) } var obj = { a:2 } fn.call(obj); //2
如果使用箭头函数则可以这么写
function fn(){ setTimeout(()=>{ //this 来源于 fn 函数的作用域 console.log(this.a); },100) } var obj = { a:2 } fn.call(obj); //2
2.7 事件函数当中 this 的绑定机制
如果是在事件函数当中,this 的绑定是指向触发事件的 DOM 元素的,
$('body')[0].addEventListener('click',function(){ console.log(this); },false);
点击 body 元素之后,控制台则会显示 body 元素
3. 小结
如果想判断一个函数的 this 绑定在哪里,首先是找到函数的调用位置,之后是按照规则来判断。
以上就是关于Javascript中神奇的this的相关介绍,希望对大家的学习有所帮助。