不论是面向对象,还是基于对象的语言,都会有 this,我更喜欢叫他 this 指针,如果你不理解指针,认为它是个引用也无妨。
这一片文章就是整理一下在各个情况下的 this 到底引用的是谁。一次来明白 this 的用法,下面将是一段段的代码,每段代码后面可能有简短的说明,就是这样简单粗暴。

说明一下,这篇文章是基于浏览器的,不是原生 js,区别在于浏览器全局中的 this 是 Window,而原生 js 中是 global。其次,博主使用的控制台输出,如果你使用document.write方法或alert输出 this,由于这两个方法会调用对象的 toString 方法,你会得到[object Window]或[object Object]。

注意:本文中对一般函数普通函数的措辞,这个只是博主个人的说法,由于上下文 (context) 的解释并不是很容易懂,博主自定义了这 2 个说法,帮助理解。

普通函数中的 this

  1. function f(){
  2. console.log(this);
  3. }

在 js 中,凡是没有定义在对象、构造函数或 prototype 中的函数,其中的 this 都是全局对象 Window。下文把这样的函数称为一般函数

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

同理上面这个函数也没有定义在对象、构造函数或者 prototype 里,所以得到的依然是 Window。
注意:Array.prototype.map 是定义在数组原型中的,但是给 map 传进去的参数函数就是一个一般函数

构造函数中的 this

  1. function Person(n, a, g){
  2. this.name = n;
  3. this.age = a;
  4. this.gender = g;
  5. console.log(this);
  6. }
  7. var o = new Person("Lily", 18, "F");
  8. Person("Lily", 18, "F");

第 10 行代码将函数作为非构造函数使用方式(new 方式)调用,本文把这样调用的函数称为普通函数
上面代码说明一下几点:

  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 (){
    console.log(this);
    };
    }
    var o = new Person(“Lily”, 18, “F”);
    o.speak();
    Person(“Lily”, 18, “F”);
    speak();

  4. 对象方法中的 this 同样指向当前对象

  5. 第 14 行之所以可以调用 speak(), 是因为第 13 行执行后在全局创建了 speak 函数,印证了之前的说法。

多说一句,为什么 11 行得到的是 Person…Person{…},而不是 Object…Object{…}。其实这里显示的本来就应该是构造函数的名字,如果你通过 varo=;var o = {}; 创建的对象,相当于 o=newObject();o = new Object();,这时显示的才是 Object…Object{…}

  1. function Person(n, a, g){
  2. this.name = n;
  3. this.age = a;
  4. this.gender = g;
  5. }
  6. Person.prototype.speak = function (){
  7. console.log(this);
  8. };
  9. var o = new Person("Lily", 18, "F");
  10. o.speak();
  11. Person("Lily", 18, "F");
  12. speak();

由此可见 prototype 中的方法和构造函数中直接定义方法中 this 是一样的。
最后一行出现错误,这个不难理解,这里不多说了。
如果构造函数有返回值呢?

  1. function Person(n, a){
  2. this.name = n;
  3. this.age = a;
  4. return {
  5. name: "Lucy",
  6. };
  7. }
  8. var p1 = new Person("Bob", 10);
  9. console.log(p1.name);
  10. console.log(p1.age);

很明显,这是对象 p1 中的 this 指向返回值对象
当然,构造函数还可以返回函数:

  1. function Fun(x){
  2. console.log(this);
  3. return function(){
  4. this.x = x;
  5. this.get = function(){
  6. alert(this.x);
  7. }
  8. }
  9. }
  10. var o1 = new Fun(2);
  11. var o2 = Fun(2);
  12. console.log(o1 == o2);

但如果构造函数返回了一个基本类型:

  1. function Fun(n){
  2. this.name = n;
  3. return 2;
  4. }
  5. var o;
  6. console.log(o = new Fun("Bob"));

此时得到的对象和返回值无关。

到此我们就明白了,构造函数的返回值如果是基本数据类型,那返回值和得到的对象无关;否则,得到的对象就是返回值的引用并构成闭包。

区分一下面这个具体问题:

  1. <html>
  2. <body>
  3. <button onclick="click()">Click Here</button>
  4. <button id="btn">Click Here</button>
  5. <body>
  6. <script> function click(){
  7. console.log(this);
  8. }
  9. var btn = document.getElementById("btn");
  10. btn.onclick = function(){
  11. console.log(this);
  12. }; </script>
  13. </html>

第一个按钮得到 Window,而第二个得到 input 元素!为什么!
再想想,click 函数定义在全局,不在对象上。而btn.onclick = function(){}中的函数明显是在 btn 对象上定义的。

对象方法中的闭包

说闭包前先理解一个简单的:

  1. var o = {
  2. name: "Lily",
  3. age: 18,
  4. gender: "F",
  5. speak: function (){
  6. function fun(){
  7. console.log(this);
  8. }
  9. fun();
  10. }
  11. };
  12. o.speak();

什么,这里是 Window 了?对!我们仔细想想,这个 fun 函数是对象的方法吗?显然不是,它是个一般函数。它仅仅是在另一个函数中的一个函数,显然符合上文描述的:“凡是没有定义在对象、构造函数或 prototype 中的函数,其中的 this 都是 Window”
如果想在内部函数访问这个对象,也很好解决:

  1. var o = {
  2. name: "Lily",
  3. age: 18,
  4. gender: "F",
  5. speak: function (){
  6. var _this = this;
  7. function fun(_this){
  8. console.log(_this);
  9. }
  10. fun();
  11. }
  12. };
  13. o.speak();

下面做个闭包,为了说明 this 的值,这里不定义太多变量,如果对闭包和作用域有疑惑可以参看博主的另一篇文章:Javascript 函数、作用域链与闭包

  1. var o = {
  2. name: "Lily",
  3. age: 18,
  4. gender: "F",
  5. speak: function (){
  6. return function(){
  7. console.log(this);
  8. }
  9. }
  10. };
  11. o.speak()();

这个难理解吗?返回的函数依然是个定义在别的函数里面的一般函数。如果想让返回的函数可以继续访问该对象,依然使用上面的 varthis=thisvar _this = this 解决。不过这里引出了一个新的问题:

  1. var o = {
  2. name: "Lily",
  3. age: 18,
  4. gender: "F",
  5. speak: function (){
  6. console.log(this);
  7. }
  8. };
  9. var fun = o.speak;
  10. fun();

什么?这里还是 Window!o.speak 明显是一个对象方法啊!那么问题来了?第 10 行调用的是谁?是 fun 函数。那么 fun 函数怎么定义的?对,fun 的定义决定它是一个一般函数。那怎么解决?这个不用解决,没人会试图在对象外获取对象方法,即便是有需要也应该获取对象方法内的闭包。当然,如果你要强行解决它,那就用 bind 方法吧。

原型中的 this

什么? 原型方法中的 this? 看看下面代码就明白了,这个理解起来不会很难

  1. function F(){
  2. return F.prototype.init();
  3. }
  4. F.prototype = {
  5. init: function(){
  6. return this;
  7. },
  8. test: "test"
  9. }
  10. var f = F();
  11. console.log(f);

可见,原型中方法里的 this. 就是一个该构造函数的实例化对象。jQuery 中使用的就是这个构造方法。

bind call 和 apply 方法

这 3 个方法用来改变调用函数内的 this 值

bind 方法

将对象绑定到函数,返回内部 this 值为绑定对象的函数。
如果我们不能修改库中对象的方法,我们就不能用 var_this=this;var _this = this; 的方法改变 this 值,那么我们换个角度考虑上面的问题:

  1. var o = {
  2. name: "Lily",
  3. age: 18,
  4. gender: "F",
  5. speak: function (){
  6. return function(){
  7. console.log(this);
  8. }
  9. }
  10. };
  11. o.speak()();

最后一行中,o.speak() 执行完后得到一个函数,这是个临时函数,定义在全局作用域,如果我们把这个临时函数绑定到 o 对象上,再继续调用这个函数不就可以了么:

  1. var o = {
  2. name: "Lily",
  3. age: 18,
  4. gender: "F",
  5. speak: function (){
  6. return function(){
  7. console.log(this);
  8. }
  9. }
  10. };
  11. o.speak().bind(o)();

bind 不只可以传入一个参数,后面的多个参数可以作为返回函数的绑定参数,如下:

  1. function add(a, b){
  2. console.log(a+b);
  3. return a+b;
  4. }
  5. var add2 = add.bind(null, 2);
  6. add2(4);

可如果是构造函数呢?记住一点,函数作为构造函数调用时,bind 的第一参数无效,注意,仅仅是第一参数无效。

  1. function Person(pname, page){
  2. this.name = pname;
  3. this.age = page;
  4. }
  5. var Person2 = Person.bind({name:"hello",city:"Beijing"}, "world");
  6. var p = new Person2(12);
  7. console.log(p);

call 方法 和 apply 方法

这里举几个和上文不一样的例子

  1. function Animal(){
  2. this.name = "Animal";
  3. }
  4. Animal.prototype.showName = function(){
  5. alert(this.name);
  6. };
  7. function Cat(){
  8. this.name = "cat";
  9. }
  10. var cat = new Cat();

这里 Cat 没有 showName 方法,怎么实现输出名字呢?
有 c++ 和 java 经验的人会认为猫属于动物,所以 Cat 应该继承 Animal,所以我们可以这样修改:

  1. function Animal(){
  2. this.name = "Animal";
  3. }
  4. Animal.prototype.showName = function(){
  5. alert(this.name);
  6. };
  7. function Cat(){
  8. this.name = "cat";
  9. }
  10. Cat.prototype = Animal.prototype;
  11. var cat = new Cat();
  12. cat.showName();

或者:

  1. function Animal(){
  2. this.name = "Animal";
  3. }
  4. Animal.prototype.showName = function(){
  5. alert(this.name);
  6. };
  7. function Cat(){
  8. Animal.call(this, "cat");
  9. }
  10. var cat = new Cat();
  11. cat.showName();

有 c++ 和 java 经验就会知道,在做一个大型项目之前都是要做 UML 设计的,用例图、活动图、类图、状态图等等十几种图,对于没有一定经验的开发者做这个简直就是噩梦,而 js 把各种类或模块独立出来,需要的时候用 call、bind、apply 把多个类联系起来,这样的做法即简化了设计,又简化了维护。
所以 js 里面很少有上面的写法,怎么写看下面:

  1. function Animal(){
  2. this.name = "Animal";
  3. }
  4. Animal.prototype.showName = function(){
  5. alert(this.name);
  6. }
  7. function Cat(){
  8. this.name = "Cat";
  9. }
  10. var cat = new Cat();
  11. Animal.prototype.showName.call(cat);
  12. Animal.prototype.showName.apply(cat);

对,不过感觉那里怪怪的,call 和 apply 一样?他们功能上一样,只是接受的参数不同,简单写就是下面这样:

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

它们的第一个参数都是指定调用该函数的对象,如果为空就是全局对象。后面的时传入该函数的参数,区别在于使用 call 时参数逐一传入,而使用 apply 时参数构成一个数组或类数组对象传入

实例

例子 1:

  1. var numbers = [5, 6, 9, 3, 7];
  2. var maxValue = Math.max(numbers);
  3. alert(maxValue);
  4. maxValue = Math.max.apply(null, numbers);
  5. alert(maxValue);
  6. var max = +Infinity;
  7. for (var i = 0, len = numbers.length; i < len; i++) {
  8. if (numbers[i] > max)
  9. max = numbers[i];
  10. }

例子 2

  1. function typeOf(o){
  2. return Object.prototype.toString.call(o).slice(8,-1);
  3. }
  4. console.log(typeOf (2.1));
  5. console.log(typeOf (undefined));
  6. console.log(typeOf ({}));
  7. console.log(typeOf ("hello"));
  8. console.log(typeOf (false));
  9. console.log(typeOf (typeOf));
  10. console.log(typeOf (null));
  11. console.log(typeOf ([]));
  12. console.log(typeOf (new Date));
  13. console.log(typeOf (/\d/));
  14. console.log(typeOf (document. getElementsByTagName('body')[0]));
  15. console.log(typeof (2.1));
  16. console.log(typeof (undefined));
  17. console.log(typeof ({}));
  18. console.log(typeof ("hello"));
  19. console.log(typeof (false));
  20. console.log(typeof (typeOf));
  21. console.log(typeof (null));
  22. console.log(typeof ([]));
  23. console.log(typeof (new Date));
  24. console.log(typeof (/\d/));
  25. console.log(typeof (document. getElementsByTagName('body')[0]));

例子 3

  1. function(){
  2. return Array.prototype.slice.call(arguments);
  3. }

例子 4

  1. function getRuleSelector(selector){
  2. return Array.prototype.filter.call(getCssList(), function(x){
  3. return pure(x.selectorText) === pure(selector);
  4. });
  5. function pure(selector){
  6. selector.replace(/::/g, ":");
  7. }
  8. function getCssList(){
  9. return Array.prototype.concat.apply([], Array.prototype.map.call(document.styleSheets, function(x){
  10. return Array.prototype.slice.call(x.cssRules);
  11. }));
  12. }
  13. }

例子 5

  1. Array.prototype.forEach.call(document.querySelectAll('input[type=button]'), function(ele){
  2. ele.addEventLister("click", fun, false);
  3. });

例子 6

  1. var forEach = Function.prototype.call.bind(Array.prototype.forEach);
  2. DOMElementList = document.getElementByTagName("li");
  3. forEach(DOMElementList, function (el) {
  4. el.addEventListener('click', handle);
  5. });

箭头函数中的 this

之所以最后说箭头函数,一方面因为这是 ES6 中的内容,更重要的时因为箭头函数中的 this 永远不能被 call, bind 和 apply 改变,也就是说箭头函数中的 this 可不改变,仅仅与其定义的位置有关。

箭头函数的最大特点是:它不改变 this 的作用域 (上下文环境),但是依然构成局部作用域,我们之前遇到过闭包内 this 值被改变的问题,我们用重新定义局部变量的方式解决了这个问题。如果有了箭头函数,解决这个问题就简单多了

这是上面出现过的一段代码:

  1. var o = {
  2. name: "Lily",
  3. age: 18,
  4. gender: "F",
  5. speak: function (){
  6. function fun(){
  7. console.log(this);
  8. }
  9. fun();
  10. }
  11. };
  12. o.speak();

看看用箭头函数函数怎优雅的解决这个问题

  1. var o = {
  2. name: "Lily",
  3. age: 18,
  4. gender: "F",
  5. speak: function (){
  6. (() => {console.log(this);})();
  7. }
  8. };
  9. o.speak();

或者这样也可以:

  1. var o = {
  2. name: "Lily",
  3. age: 18,
  4. gender: "F",
  5. speak: function (){
  6. return () => {console.log(this);};
  7. }
  8. };
  9. o.speak()();

with

with 可以改变上下文环境,实际开发中十分不建议使用 with, 但关于 with 这里简单说明一下,看一个示例:

  1. var a, x, y;
  2. var r = 10;
  3. with (Math) {
  4. a = round(PI * r * r);
  5. x = r * cos(PI);
  6. y = r * sin(PI / 2);
  7. }
  8. console.log(a, x, y);

但是如果在 with 内直接声明变量会发生什么:

  1. var obj = {
  2. name: 'test'
  3. };
  4. with(obj){
  5. name = "hello";
  6. var salary = 10000;
  7. age = 20;
  8. }
  9. console.log(obj.name);
  10. console.log(obj.age);
  11. console.log(age);
  12. console.log(obj.salary);
  13. console.log(salary);

https://segmentfault.com/a/1190000016286558