Your Browser Don't Support Canvas, Please Download Chrome ^_^``

Javascript中this的值

字数: 123123   阅读数: 0
Posted by Kerwin She on April 25, 2018

原文

ES6之前的函数

ES6之前,任何函数中都可以出现this,它的值是在运行时决定的。说简单一点儿就是this的值取决与你如何调用这个函数,而不是函数定义所在的位置。另外在函数调用的时候可以使用apply/call指定this这样就实现了代码的复用。大多数javascript 的内置函数设计的尽量通用。

注意上面所讲的是ES6之前的函数,因为ES6有个箭头函数,它是没有this,所以它的 this是在定义时就固化好了的,后面会进行详细的介绍

1. 一般函数调用

在一个普通的函数中,this的值在浏览器中是windownodejs中是global(后面不做区分,假设在浏览器中执行)。在最外层的作用域中thiswindow。例如

  console.log(this); //输出 window
  func(); //输出为 window

  function func()
  {
     console.info(this);
  }

可是这样存在一个问题,如果一个函数是构造函数,一般在这个函数中是会给this添加属性的。谁也能不能保证不犯错误,有时候当我们想把函数作为构造函数的时候,可是却写成了一般函数调用的形式,这个时候这一段代码就是给window添加属性,此时程序不会报错,但是明明又不是我们想要的(javascript的坑不止这一个)。所以为了让 javascript 变得更好,就提出了一种严格模式,在某一块代码的一开始写一句”use strict”,对于不支持的浏览器来说这个只是一个字符串。但是对于支持严格模式的浏览器来说,会进入严格模式。在严格模式下,一般的函数调用中 this 的值是 undefined

  func(); //输出为 undefined
  function func()
  {
     "use strict"
     console.info(this);
  }

2.作为对象的方法

如果函数作为对象的方法,也就是说调用方法是对象.方法(),那么这个时候所执行的函数中this就是这个对象的引用。

  var obj={};
  function func(){ console.info(this) }
  obj.f = func;
  obj.f(); //输出 obj
  func(); //输出 window

尝试一下代码的输出:

  function func() {console.info(this)};
  var obj={};
  obj.f=func;

  obj.f();    // obj
  (obj.f)();  // obj
  (a=obj.f)(); // window
  (0,obj.f)(); //  window

前两个是等价的(. 的优先级比函数调用高)。其实,.运算符的返回值不是obj.f中存储的值,而是一个包含obj.f信息的“对象”(这个只是 javascript内部实现时使用的,用 javascript 代码并不可访问)。在ECMAScript 5.1 标准中对这个返回值的描述是:

Retrun a value of type Reference whose base value is baseValue and whose referenced name is propertypname, and whose strict mode flag is strict.

本例中 baseValueobj,propertypname 是字符串类型的属性名"f",后面那个strict是是否处于严格模式的标记。所以说这段话的意思就是obj.f的返回值是一个 baseValueobj, propertypname"f"的“对象”。而this的值其实是取上述返回值的 baseValue ,因此,在函数中thisobj。对于第三个,首先a=obj.f是一个赋值表达式,虽然 obj.f的返回值是一个包含obj.f信息的“对象”,但是赋值表达式的返回值是=右边表达式的返回值“求值”(GetValue),把“对象”中的值取出来,所以a=obj.f的返回值就是一个函数。然后再对这个函数进行调用调用,显然这个返回值的 baseValue 是没有定义的。而函数调用初始话作用域的时候,如果this为空值,那么就会让this等于window(非严格模式)。一般函数调用this是window的原因也是如此– baseValue 为空。最后一个与第三个相同,只不过是赋值表达式换成了逗号表达式。

  function fn() { console.info(this); }
  function fnstrict() {
     'use strict'
     console.info(this);
  }
  fnstrict.call(undefined); // undefined
  fn.call(undefined); // window
  fn.call(null);      // window

3. 构造函数(关键字new)

在new表达式中的 this 反而简单,统一规定为一个新创建的以这个函数为原型的对象。

function fn(){ console.info(this instanceof fn) };
var obj = new fn(); //输出 true

4. 调用时指定this

也很简单,在调用函数时可以显示的指明用哪个值作为this,所以这仅仅是一个语法问题。javascript给出的方式就是apply/call这两个函数只是在参数的形式上有些区别(这个区别不是本文的主要内容在此不讨论)。call函数的语法是func.call(this的值,可选参数1,...),其中可选参数会传递给func

  function fn(arg){ console.info(this) };
  fn.call("thisvalue");
  // 输出 “thisvalue”, 实际为 toObject("thisvalue"),把这个字符串转换成对象

这样做的好处就是可以实现代码复用,javascript 中的内置函数后设计的尽可能通用,当一个对象满足某些条件的时候就可以用作为这个函数的this。比如一个对象只要有length属性就可以作为Array.prototype中的函数的this,参考 《Array.prototype中函数的定义》

5. 上述说的都不对

还有一种特殊情况,javascript可以指定一个固定的值作为this,无论如何调用这个函数this的值不变。产生这种函数的方式就是调用一个普通函数的bind方法,bind不仅可以指定this还可以指定函数调用的参数,其语法是func.bind(this的值,可选参数1,....),其中可选参数是要绑定的参数,其余参数在调用的时候再给出。

function func() { console.info(this) }
bfunc = func.bind("bind this");
bfunc(); // bind this
// 原函数还是按照前面的方式确定 this
func();  // window

一种常用,但是代码怎么写都有点儿乱的绑定(ES2015可以解决),代码功能不用关心,只要知道如果this没有正确的值this.doSomething调用就会失败就可以了:

var PageHandler = {
   id: "123456",
   init: function() {
       document.addEventListener("click",
           (function(event) {
               this.doSomething(event.type); }
           ).bind(this), false);
   },
   doSomething:function(type) {
       console.log("Handling " + type  + " for " + this.id);
   }
};
PageHandler.init();

6. DOM事件回调函数

作为浏览器中的javascript中还有一种函数调用,那就是事件回调函数。在事件回调函数中this 的值是当前触发该事件的DOM对象(部分浏览器只有使用addEventListener添加的事件才遵循此规定,说的就是IE)。

function evtHandle(e)
{
   console.log(this === e.currentTarget); // 总为true
   console.log(this === e.target);        // 只有在目标阶段为true
}
var elements = doucument.getElementsByTagName('*');
for(var i=0;i<elements.length; i++){
   elements[i].addEventListener('click',evtHandle,false);
}

事件内联代码

DOM元素中添加的事件内联代码中的this就是当前的这个DOM元素。

<button onclick="console.log(this)">
   show this
</button>

上面代码中的this是它所在的DOM元素(button)。但是需要注意的是只有最外层作用域的this是所在的DOM元素,内层作用域代码中this的确定同前面几条。还是那句话,函数中this的值主要看函数是怎么被调用的,函数(内层作用域代码)是如何调用的是我们自己的代码写的,所以我们可以确定this的值,只是初始(最外层)的作用域中的this我们不知道,需要统一规定。

<button onclick="console.log((function (){return this;})());">
   show inner this
</button>

上述代码中的匿名函数就是一次普通的函数调用,所以this的值是window(非严格模式下)。

万恶的IE

在IE下,用 attachEvent 添加的事件处理函数其中的this规定为 window

// IE 下运行
var el = document.getElementById("id");
el.attachEvent('onclick',function(){
   console.info(this);   // window
});

作为总结

在 javascript 中,this的值取决与你如何调用这个函数,而不是函数定义所在的位置。函数的调用分为6种情况,也就是this的来源有6种:

  1. 普通函数调用,this为全局对象或是undefined
  2. 作为对象的方法,this为那个对象
  3. new 表达式,this为以该函数为原型的新创建的对象
  4. 使用 apply/call指定 this
  5. 用bind绑定固定的this
  6. 事件处理函数中的this是当前的触发事件的DOM元素(event.currentTarget) IE attachEvent添加的事件处理函数中this为window

最后提一个问题 下面代码输出什么?

obj = { go: function() { console.info(this) } };
(0 || obj.go)()

参考:

  1. ES5.1 属性运算符
  2. [ES5.1 函数调用][4]
  3. http://javascript.info/tutorial/this
  4. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this

ES6箭头函数

ES6箭头函数与之前所讲的是有很大的差别的,主要注意以下几点:

  1. 函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。

  2. 不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。

  3. 不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。

  4. 不可以使用yield命令,因此箭头函数不能用作 Generator 函数。

上面四点中,第一点尤其值得注意。this对象的指向是可变的,但是在箭头函数中,它是固定的

  function foo() {
    setTimeout(() => {
      console.log('id:', this.id);
    }, 100);
  }

  var id = 21;

  foo.call({ id: 42 });
  // id: 42

上面代码中,setTimeout的参数是一个箭头函数,这个箭头函数的定义生效是在foo函数生成时,而它的真正执行要等到 100 毫秒后。 因为箭头函数this的指向是定义时决定的,所以this会指向它外层对象的this,即指向 { id: 42 }

如果是普通函数,执行时this应该指向全局对象window,这时应该输出21

箭头函数可以让setTimeout里面的this,绑定定义时所在的作用域,而不是指向运行时所在的作用域。下面是另一个例子。

   function Timer() {
    this.s1 = 0;
    this.s2 = 0;
    // 箭头函数
    setInterval(() => this.s1++, 1000);
    // 普通函数
    setInterval(function () {
      this.s2++;
    }, 1000);
  }

  var timer = new Timer();

  setTimeout(() => console.log('s1: ', timer.s1), 3100);
  setTimeout(() => console.log('s2: ', timer.s2), 3100);
  // s1: 3
  // s2: 0

上面代码中,Timer函数内部设置了两个定时器,分别使用了箭头函数和普通函数。前者的this绑定定义时所在的作用域(即Timer函数),后者的this指向运行时所在的作用域(即全局对象)。所以,3100 毫秒之后,timer.s1被更新了 3 次,而timer.s2一次都没更新。

箭头函数可以让this指向固定化,这种特性很有利于封装回调函数。下面是一个例子,DOM 事件的回调函数封装在一个对象里面。

   var handler = {
   id: '123456',

   init: function() {
     document.addEventListener('click',
       event => this.doSomething(event.type), false);
   },

   doSomething: function(type) {
     console.log('Handling ' + type  + ' for ' + this.id);
   }
  };

上面代码的init方法中,使用了箭头函数,这导致这个箭头函数里面的this,总是指向handler对象。否则,回调函数运行时,this.doSomething这一行会报错,因为由第一部分最后总结的第六点可以得知事件处理函数中的this是当前的触发事件的DOM元素,所以this指向document对象。

this指向的固定化,并不是因为箭头函数内部有绑定this的机制,实际原因是箭头函数根本没有自己的this导致内部的this就是外层代码块的this。正是因为它没有this,所以也就不能用作构造函数。 所以,箭头函数转成 ES5 的代码如下。

   // ES6
  function foo() {
   setTimeout(() => {
     console.log('id:', this.id);
   }, 100);
  }

  // ES5
  function foo() {
   var _this = this;

   setTimeout(function () {
     console.log('id:', _this.id);
   }, 100);
  }

除了this,以下三个变量在箭头函数之中也是不存在的,指向外层函数的对应变量:argumentssupernew.target

 function foo() {
  setTimeout(() => {
    console.log('args:', arguments);
  }, 100);l
}

foo(2, 4, 6, 8)
// args: [2, 4, 6, 8]

上面代码中,箭头函数没有自己的this,所以bind方法无效,内部的this指向外部的this