Javascript this詳解

不管是面向對象,仍是基於對象的語言,都會有this,我更喜歡叫他this指針,若是你不理解指針,認爲它是個引用也無妨。
這一片文章就是整理一下在各個狀況下的this到底引用的是誰。一次來明白this的用法,下面將是一段段的代碼,每段代碼後面可能有簡短的說明,就是這樣簡單粗暴。javascript

說明一下,這篇文章是基於瀏覽器的,不是原生js,區別在於瀏覽器全局中的this是Window,而原生js中是global。其次,博主使用的控制檯輸出,若是你使用document.write方法或alert輸出this,因爲這兩個方法會調用對象的toString方法,你會獲得[object Window]或[object Object]。css

注意:本文中對通常函數普通函數的措辭,這個只是博主我的的說法,因爲上下文(context)的解釋並非很容易懂,博主自定義了這2個說法,幫助理解。html

普通函數中的this

function f(){
  console.log(this); //Window
}

在js中,凡是沒有定義在對象、構造函數或prototype中的函數,其中的this都是全局對象Window。下文把這樣的函數稱爲通常函數java

var a = [1,2,3,4,5];
var b = a.map(function(x){
  console.log(this);  //Window
  return x * 2;
});

同理上面這個函數也沒有定義在對象、構造函數或者prototype裏,因此獲得的依然是Window。
注意:Array.prototype.map是定義在數組原型中的,可是給map傳進去的參數函數就是一個通常函數c++

構造函數中的this

function Person(n, a, g){
  this.name = n;
  this.age = a;
  this.gender = g;
  console.log(this);
}
//做爲構造函數使用
var o = new Person("Lily", 18, "F"); //this爲當前對象 Person {name: "Lily", age: 18, gender: "F"}
//做爲普通函數使用
Person("Lily", 18, "F"); //Window

第10行代碼將函數做爲非構造函數使用方式(new方式)調用,本文把這樣調用的函數稱爲普通函數
上面代碼說明一下幾點:segmentfault

  1. 用new建立對象的時候調用了構造函數。
  2. 構造函數和普通函數的區別在於調用方式,而不是定義方式,若是按第10行的方式調用,他就是個普通函數,因爲普通函數中的this是於Window,因此上面函數在第10行調用後建立了3個全局變量。
  3. new關鍵字改變了函數內this的指向,使其指向剛建立的對象。
function Person(n, a, g){
  this.name = n;
  this.age = a;
  this.gender = g;
  this.speak = function (){   //這裏只是說明this,實際應該在prototype上定義對象方法
    console.log(this);
  };
}
//做爲構造函數使用
var o = new Person("Lily", 18, "F");
o.speak();  //Person {name: "Lily", age: 18, gender: "F"}
//做爲普通函數使用
Person("Lily", 18, "F");
speak(); //Window
  1. 對象方法中的this一樣指向當前對象
  2. 第14行之因此能夠調用speak(),是由於第13行執行後在全局建立了speak函數,印證了以前的說法。

多說一句,爲何11行獲得的是$Person{...}$,而不是$Object{...}$。其實這裏顯示的原本就應該是構造函數的名字,若是你經過$var o = {};$建立的對象,至關於$o = new Object();$,這時顯示的纔是$Object{...}$數組

function Person(n, a, g){
  this.name = n;
  this.age = a;
  this.gender = g;
}
Person.prototype.speak = function (){   //這裏只是說明this,實際應該在prototype上定義對象方法
  console.log(this);
};
//做爲構造函數使用
var o = new Person("Lily", 18, "F");
o.speak();  //this爲當前對象 Person {name: "Lily", age: 18, gender: "F"}
//做爲普通函數使用
Person("Lily", 18, "F");
speak(); //ReferenceError: speak is not defined

因而可知prototype中的方法和構造函數中直接定義方法中this是同樣的。
最後一行出現錯誤,這個不難理解,這裏很少說了。
若是構造函數有返回值呢?瀏覽器

function Person(n, a){
  this.name = n;
  this.age = a;
  return {
    name: "Lucy",
  };
}
var p1 = new Person("Bob", 10);
console.log(p1.name); //"Lucy"
console.log(p1.age);  //undefined

很明顯,這是對象p1中的this指向返回值對象
固然,構造函數還能夠返回函數:閉包

function Fun(x){
  console.log(this);
  return function(){
    this.x = x;
    this.get = function(){
      alert(this.x);
    }
  }
}
var o1 = new Fun(2);   //Fun {}
var o2 = Fun(2);    //window
console.log(o1 == o2);   //false, 這裏的o1,o2形式是同樣的,因爲構成閉包結構,因此應用不一樣

但若是構造函數返回了一個基本類型:app

function Fun(n){
  this.name = n;
  return 2;
}
var o;
console.log(o = new Fun("Bob"));   // {name: "Bob"}

此時獲得的對象和返回值無關。

到此咱們就明白了,構造函數的返回值若是是基本數據類型,那返回值和獲得的對象無關;不然,獲得的對象就是返回值的引用並構成閉包。

區分一下面這個具體問題:

<html>
<body>
  <button onclick="click()">Click Here</button>
  <button id="btn">Click Here</button>
<body>
<script>
  function click(){
    console.log(this); //window
  }

  var btn = document.getElementById("btn");
  btn.onclick = function(){
    console.log(this);
  };
</script>
</html>

第一個按鈕獲得Window,而第二個獲得input元素!爲何!
再想一想,click函數定義在全局,不在對象上。而btn.onclick = function(){}中的函數明顯是在btn對象上定義的。

對象方法中的閉包

說閉包前先理解一個簡單的:

var o = {
  name: "Lily",
  age: 18,
  gender: "F",
  speak: function (){
    function fun(){
      console.log(this);
    }
    fun();
  }
};
o.speak();  //Window

什麼,這裏是Window了?對!咱們仔細想一想,這個fun函數是對象的方法嗎?顯然不是,它是個通常函數。它僅僅是在另外一個函數中的一個函數,顯然符合上文描述的:「凡是沒有定義在對象、構造函數或prototype中的函數,其中的this都是Window」
若是想在內部函數訪問這個對象,也很好解決:

var o = {
  name: "Lily",
  age: 18,
  gender: "F",
  speak: function (){
    var _this = this; //首選_this,有的資料上會用self。
    function fun(_this){
      console.log(_this);
    }
    fun();
  }
};
o.speak();  //Object {name: "Lily", age: 18, gender: "F"}

下面作個閉包,爲了說明this的值,這裏不定義太多變量,若是對閉包和做用域有疑惑能夠參看博主的另外一篇文章:Javascript 函數、做用域鏈與閉包

var o = {
  name: "Lily",
  age: 18,
  gender: "F",
  speak: function (){
    return function(){
      console.log(this);
    }
  }
};
o.speak()();  //Window

這個難理解嗎?返回的函數依然是個定義在別的函數裏面的通常函數。若是想讓返回的函數能夠繼續訪問該對象,依然使用上面的$var _this = this$解決。不過這裏引出了一個新的問題:

var o = {
  name: "Lily",
  age: 18,
  gender: "F",
  speak: function (){
    console.log(this);
  }
};
var fun = o.speak;
fun(); //Window

什麼?這裏仍是Window!o.speak明顯是一個對象方法啊!那麼問題來了?第10行調用的是誰?是fun函數。那麼fun函數怎麼定義的?對,fun的定義決定它是一個通常函數。那怎麼解決?這個不用解決,沒人會試圖在對象外獲取對象方法,即使是有須要也應該獲取對象方法內的閉包。固然,若是你要強行解決它,那就用bind方法吧。

原型中的this

什麼?原型方法中的this? 看看下面代碼就明白了,這個理解起來不會很難

function F(){
  return F.prototype.init();
}
F.prototype = {
  init: function(){
    return this;
  },
  test: "test"
}
var f = F();
console.log(f); //F{test:test}

可見,原型中方法裏的this.就是一個該構造函數的實例化對象。jQuery中使用的就是這個構造方法。

bind call和apply方法

這3個方法用來改變調用函數內的this值

bind方法

將對象綁定到函數,返回內部this值爲綁定對象的函數。
若是咱們不能修改庫中對象的方法,咱們就不能用$var \_this = this;$的方法改變this值,那麼咱們換個角度考慮上面的問題:

var o = {
  name: "Lily",
  age: 18,
  gender: "F",
  speak: function (){
    return function(){
      console.log(this);
    }
  }
};
o.speak()();  //Window

最後一行中,o.speak()執行完後獲得一個函數,這是個臨時函數,定義在全局做用域,若是咱們把這個臨時函數綁定到o對象上,再繼續調用這個函數不就能夠了麼:

var o = {
  name: "Lily",
  age: 18,
  gender: "F",
  speak: function (){
    return function(){
      console.log(this);
    }
  }
};
o.speak().bind(o)();  //Object {name: "Lily", age: 18, gender: "F"}

bind不僅能夠傳入一個參數,後面的多個參數能夠做爲返回函數的綁定參數,以下:

function add(a, b){
  console.log(a+b);
  return a+b;
}

var add2 = add.bind(null, 2); //參數順序保持一致,第一參爲null,不改變this值(但這裏會改變,由於add2在全局中定義)
add2(4); //6

可若是是構造函數呢?記住一點,函數做爲構造函數調用時,bind的第一參數無效,注意,僅僅是第一參數無效。

function Person(pname, page){
  this.name = pname;
  this.age = page;
}
var Person2 = Person.bind({name:"hello",city:"Beijing"}, "world");
var p = new Person2(12);
console.log(p);//Person{name:"world", age:12}

call方法 和 apply方法

這裏舉幾個和上文不同的例子

function Animal(){
    this.name = "Animal";
}
Animal.prototype.showName = function(){
    alert(this.name);
};

function Cat(){
    this.name = "cat";
}
var cat = new Cat();

這裏Cat沒有showName方法,怎麼實現輸出名字呢?
有c++和java經驗的人會認爲貓屬於動物,因此Cat應該繼承Animal,因此咱們能夠這樣修改:

function Animal(){
    this.name = "Animal";
}
Animal.prototype.showName = function(){
    alert(this.name);
};

function Cat(){
    this.name = "cat";
}
Cat.prototype = Animal.prototype;
var cat = new Cat();
cat.showName(); //Cat

或者:

function Animal(){
    this.name = "Animal";
}
Animal.prototype.showName = function(){
    alert(this.name);
};

function Cat(){
  Animal.call(this, "cat");  //繼承
}
var cat = new Cat();
cat.showName(); //Cat

有c++和java經驗就會知道,在作一個大型項目以前都是要作UML設計的,用例圖、活動圖、類圖、狀態圖等等十幾種圖,對於沒有必定經驗的開發者作這個簡直就是噩夢,而js把各類類或模塊獨立出來,須要的時候用call、bind、apply把多個類聯繫起來,這樣的作法即簡化了設計,又簡化了維護。
因此js裏面不多有上面的寫法,怎麼寫看下面:

function Animal(){
    this.name = "Animal";
}
Animal.prototype.showName = function(){
    alert(this.name);
}

function Cat(){
    this.name = "Cat";
}
var cat = new Cat();
Animal.prototype.showName.call(cat);   //cat
Animal.prototype.showName.apply(cat);   //cat

對,不過感受那裏怪怪的,call和apply同樣?他們功能上同樣,只是接受的參數不一樣,簡單寫就是下面這樣:

func.call(func1,var1,var2,var3,...);
func.apply(func1,[var1,var2,var3,...]);

它們的第一個參數都是指定調用該函數的對象,若是爲空就是全局對象。後面的時傳入該函數的參數,區別在於使用call時參數逐一傳入,而使用apply時參數構成一個數組或類數組對象傳入

實例

例子1:

//求下列數組元素的最大值
var numbers = [5, 6, 9, 3, 7];
var maxValue = Math.max(numbers);
alert(maxValue);  //NaN
maxValue = Math.max.apply(null, numbers);
alert(maxValue);  //9

//不然你只能這麼寫:
var max = +Infinity;
for (var i = 0, len = numbers.length; i < len; i++) {
  if (numbers[i] > max)
    max = numbers[i];
}

例子2

//自定義typeof函數(注意,系統自帶的typeof是運算符,不是函數)
function typeOf(o){
  return Object.prototype.toString.call(o).slice(8,-1);
}
//自定義typeOf函數測試
console.log(typeOf (2.1));  //Number
console.log(typeOf (undefined));  //Undefined
console.log(typeOf ({}));  //Object
console.log(typeOf ("hello"));  //String
console.log(typeOf (false));  //Boolean
console.log(typeOf (typeOf));  //Function
console.log(typeOf (null));  //Null
console.log(typeOf ([]));  //Array
console.log(typeOf (new Date));  //Date
console.log(typeOf (/\d/));  //RegExp
console.log(typeOf (document.  getElementsByTagName('body')[0]));  //HTMLBodyElement

//系統typeof運算符測試
console.log(typeof (2.1));  //number
console.log(typeof (undefined));  //Undefined
console.log(typeof ({}));  //object
console.log(typeof ("hello"));  //string
console.log(typeof (false));  //boolean
console.log(typeof (typeOf));  //function
console.log(typeof (null));  //object
console.log(typeof ([]));  //object
console.log(typeof (new Date));  //object
console.log(typeof (/\d/));  //object
console.log(typeof (document.  getElementsByTagName('body')[0]));  //object

//明顯比系統本身的實用多了

例子3

//把類數組對象轉爲數組(類數組對象就是屬性key爲0,1,2,...,還具備一個key爲length的能夠像數組同樣動態改變的值的對象)
function(){
  return Array.prototype.slice.call(arguments);
}

例子4

//用js訪問元素僞類
function getRuleSelector(selector){

  return Array.prototype.filter.call(getCssList(), function(x){
    return pure(x.selectorText) === pure(selector);
  });

  function pure(selector){
    selector.replace(/::/g, ":");   //把雙冒號替換爲單冒號
  }

  function getCssList(){
    return Array.prototype.concat.apply([], Array.prototype.map.call(document.styleSheets, function(x){
      return Array.prototype.slice.call(x.cssRules);
    }));
  }
}

例子5

//爲每一個DOM元素註冊事件
Array.prototype.forEach.call(document.querySelectAll('input[type=button]'), function(ele){
  ele.addEventLister("click", fun, false);
});

例子6

//自定義forEach函數遍歷Dom元素列表(類數組對象)
var forEach = Function.prototype.call.bind(Array.prototype.forEach);

DOMElementList = document.getElementByTagName("li");
forEach(DOMElementList, function (el) {
  el.addEventListener('click', handle);   //handle定義省略
});

箭頭函數中的this

之因此最後說箭頭函數,一方面由於這是ES6中的內容,更重要的時由於箭頭函數中的this永遠不能被call, bind和apply改變,也就是說箭頭函數中的this可不改變,僅僅與其定義的位置有關。

箭頭函數的最大特色是:它不改變this的做用域(上下文環境),可是依然構成局部做用域,咱們以前遇到過閉包內this值被改變的問題,咱們用從新定義局部變量的方式解決了這個問題。若是有了箭頭函數,解決這個問題就簡單多了

這是上面出現過的一段代碼:

var o = {
  name: "Lily",
  age: 18,
  gender: "F",
  speak: function (){
    function fun(){
      console.log(this);
    }
    fun();
  }
};
o.speak();  //window

看看用箭頭函數函數怎優雅的解決這個問題

var o = {
  name: "Lily",
  age: 18,
  gender: "F",
  speak: function (){
    (() => {console.log(this);})(); //一個當即執行的箭頭函數
  }
};
o.speak();  //Object {name: "Lily", age: 18, gender: "F"}

或者這樣也能夠:

var o = {
  name: "Lily",
  age: 18,
  gender: "F",
  speak: function (){
    return () => {console.log(this);}; //返回一個箭頭函數
  }
};
o.speak()();  //Object {name: "Lily", age: 18, gender: "F"}

with

with 能夠改變上下文環境,實際開發中十分不建議使用 with, 但關於 with 這裏簡單說明一下,看一個示例:

var a, x, y;
var r = 10;

with (Math) {
  a = round(PI * r * r);
  x = r * cos(PI);
  y = r * sin(PI / 2);
}

console.log(a, x, y);  //314 -10 10

可是若是在 with 內直接聲明變量會發生什麼:

var obj = {
  name: 'test'
};

with(obj){   //內部定義的變量都註冊在 obj 上
  name = "hello";
  var salary = 10000;
  age = 20;
}

console.log(obj.name);     //'hello'
console.log(obj.age);      //undefined
console.log(age);   //20, 若是對象不具備這個屬性,該定義會意外的出如今 全局變量中
console.log(obj.salary);      //undefined
console.log(salary);   //10000, 若是對象不具備這個屬性,該定義會意外的出如今 全局變量中
相關文章
相關標籤/搜索