程序员

浅析JavaScript中的 this 指向

2018-11-08  本文已影响17人  JuanitaLee

前言

this指向是学习JavaScript必须理解的知识点,在ES6没有出现时,this是动态绑定的,或称为运行期绑定的,这就导致this关键字有能力具备多重含义,带来灵活性的同时,也为初学者带来不少困惑。

JavaScript中的this

JavaScript中的this含义很丰富,他可以是全局对象、当前对象或者任意对象,这完全取决于函数的调用方式。

JavaScript中函数的调用有以下几种方式:
作为对象方法调用
作为函数调用
作为构造函数调用
使用apply或call调用、bind调用

下面分别来看一下上面说的几种调用方式

作为对象方法调用

var point = {
  x:0,
  y:0,
  moveTo: function(x,y){
    this.x = this.x + x;
    this.y = this.y + y;
  }
};

point.moveTo(1,1)  // this 绑定到当前对象,即point对象

上面这段代码我们定义了一个point对象,其有一个moveTo方法,当该方法作为对象方法被调用时,此时this绑定到当前对象,及point对象。

作为函数调用

普通函数内部的this分两种情况,严格模式和非严格模式
非严格模式下,this 默认指向全局对象window

function f1(){
  return this;
}
f1() === window; // true

而严格模式下, this为undefined

function f2(){
  "use strict"; // 这里是严格模式
  return this;
}
f2() === undefined; // true

我们常见的是非严格模式,函数可以直接被调用,此时this绑定到全局对象。在浏览器中,全局对象就是window对象,在下面的例子中,函数被调用时,this被绑定到全局对象,接下来执行赋值语句,相当于声明了一个全局变量,这显然是不应该的。

function setX(x) {
  this.x = x;
}

setX(6);
x; //x 已经是一个全局变量

对于内部函数(即声明在一个函数体内的函数),这种绑定到全局对象的方式会产生另一个问题,我们仍然以前面提到的point为例,这次我们希望在 moveTo 方法内定义两个函数,分别将 x,y 坐标进行平移。结果可能出乎大家意料,不仅 point 对象没有移动,反而多出两个全局变量 x,y。

var point = {
  x:0,
  y:0,
   moveTo: function(x,y){
    var moveX = function(x){
      this.x = x;    //this 绑定在哪里
    }
    var moveY = function(y){
      this.y = y;
    }
    moveX(x);
    moveY(y);
  }

  point.moveTo(5,5);
  console.log(point.x);   //0
  console.log(point.y);   //0
  console.log(x);  //5
  console.log(y);  //5
}

这属于JavaScript的设计缺陷,正确的设计方式是内部函数的this应该绑定到其外层函数对应的对象上,为例避免这一设计缺陷,聪明的JavaScript程序员想出了变量替代的方法,约定俗称,该变量一般被命名为that。上面的代码更改如下:

var point = { 
x : 0, 
y : 0, 
moveTo : function(x, y) { 
    //绑定内部函数的this为外层函数对应的对象
    var that = this; 
    // 内部函数
    var moveX = function(x) { 
      that.x = x; 
    }; 
    // 内部函数
    var moveY = function(y) { 
      that.y = y; 
    } 
    moveX(x); 
    moveY(y); 
    } 
}; 
point.moveTo(5, 5); 
console.log(point.x); //==>5
console.log(point.y); //==>5

//Uncaught ReferenceError: x is not defined
console.log(x);

作为构造函数调用

JavaScript 支持面向对象式编程,与主流的面向对象式编程语言不同,JavaScript 并没有类(class)的概念,而是使用基于原型(prototype)的继承方式。相应的,JavaScript 中的构造函数也很特殊,如果不使用 new调用,则和普通函数一样。作为又一项约定俗成的准则,构造函数以大写字母开头,提醒调用者使用正确的方式调用。如果调用正确,this 绑定到新创建的对象上。

function Point(x, y){ 
   this.x = x; 
   this.y = y; 
}

var point1 = new Point(1,1);  //this 指向point1

使用 apply 或 call 调用

让我们再一次重申,在 JavaScript 中函数也是对象,对象则有方法,apply 和 call 就是函数对象的方法。这两个方法异常强大,他们允许切换函数执行的上下文环境(context),即 this 绑定的对象。很多 JavaScript 中的技巧以及类库都用到了该方法。让我们看一个具体的例子:

function Point(x, y){ 
   this.x = x; 
   this.y = y; 
   this.moveTo = function(x, y){ 
       this.x = x; 
       this.y = y; 
   } 
} 
var p1 = new Point(1,1);
var p2 = {x:2, y:2};
p1.moveTo(3,3);
p1.moveTo.apply(p2, [10, 10]);
// call使用
// p1.moveTo.call(p2, 10,10);

在上面的代码中,我们使用了构造函数生成了对象p1,该对象同时具有moveTo方法。然后我们使用字面量创建了对象p2,我们看到使用 apply 可以将 p1 的方法应用到 p2 上,这时候 this 也被绑定到对象 p2 上。另一个方法 call 也具备同样功能,不同的是最后的参数不是作为一个数组统一传入,而是分开传入的。
apply和call使用

使用bind调用

bind是在EcmaScript5中扩展的方法(IE6,7,8不支持)
bind() 方法与 apply 和 call 很相似,也是可以改变函数体内 this 的指向。

MDN的解释是:bind()方法会创建一个新函数,称为绑定函数,当调用这个绑定函数时,绑定函数会以创建它时传入 bind()方法的第一个参数作为 this,传入 bind() 方法的第二个以及以后的参数加上绑定函数运行时本身的参数按照顺序作为原函数的参数来调用原函数。

[注意:bind方法的返回值是函数]

var bar=function(){   
  console.log(this.x);   
}
var foo={ x:3 }   
bar();   //此时this指向全局变量,x不存在

bar.bind(foo)();
 /*或*/
var func=bar.bind(foo);   //此时this指向foo对象,运行函数后打印结果为3
func();

输出:
undefined
3

我们再来看一个多参数的例子:
传入多个参数时,this指向第一个参数({x:1}这个对象),第二个参数2,按照顺序作为原函数的参数传入,故y=2,bind方法产生的新的函数m中传入的参数3传给原函数的z,故最终答案为6.

function f(y, z){
    return this.x + y + z;
}
var m = f.bind({x : 1}, 2);
console.log(m(3));
//6

另外有个有趣的问题,如果连续 bind() 两次,亦或者是连续 bind() 三次那么输出的值是什么呢?像这样:

var bar = function(){
    console.log(this.x);
}
var foo = {x:3}
var sed = {x:4}
var func = bar.bind(foo).bind(sed);
func(); //?
 
var fiv = {x:5}
var func = bar.bind(foo).bind(sed).bind(fiv);
func(); //?

答案是,两次都仍将输出 3 ,而非期待中的 4 和 5 。原因是,在Javascript中,多次 bind() 是无效的。更深层次的原因, bind() 的实现,相当于使用函数在内部包了一个 call / apply ,第二次 bind() 相当于再包住第一次 bind() ,故第二次以后的 bind 是无法生效的。

apply、call、bind比较

1、apply 、 call 、bind 三者都是用来改变函数的this对象的指向的;
2、apply 、 call 、bind 三者第一个参数都是this要指向的对象,也就是想指定的上下文;
3、apply 、 call 、bind 三者都可以利用后续参数传参;
4、bind 是返回对应函数,便于稍后调用;apply 、call 则是立即调用 。

DOM 事件处理函数中的 this & 内联事件中的 this

1.当函数被当做监听事件处理函数时, 其 this 指向触发该事件的元素 (针对于addEventListener事件)

// 被调用时,将关联的元素变成蓝色
    function bluify(e){
      //在控制台打印出所点击元素
      console.log(this);
      //阻止时间冒泡
      e.stopPropagation();
      //阻止元素的默认事件
      e.preventDefault();      
      this.style.backgroundColor = '#A5D9F3';
    }

    // 获取文档中的所有元素的列表
    var elements = document.getElementsByTagName('*');

    // 将bluify作为元素的点击监听函数,当元素被点击时,就会变成蓝色
    for(var i=0 ; i<elements.length ; i++){
      elements[i].addEventListener('click', bluify, false);
    }

2.内联事件
内联事件中的this指向分两种情况:
(1)当代码被内联处理函数调用时,它的this指向监听器所在的DOM元素
(2)当代码被包括在函数内部执行时,其this指向等同于 ****函数直接调用****的情况,即在非严格模式指向全局对象window, 在严格模式指向undefined。

内联事件中的this

ES6中 this指向

由于箭头函数不绑定this, 它会捕获其所在(即定义的位置)上下文的this值, 作为自己的this值。
1.所以 call() / apply() / bind() 方法对于箭头函数来说只是传入参数,对它的 this 毫无影响。
2.考虑到 this 是词法层面上的,严格模式中与 this 相关的规则都将被忽略。(可以忽略是否在严格模式下的影响)

我们来看一个例子:

//定义一个对象
var obj = {
  x:100, //属性x
  show(){
    //延迟500毫秒,输出x的值
    setTimeout(
      //匿名函数
      function(){console.log(this.x);}, 500);
   }
};
obj.show();//打印结果:undefined

为什么结果打印是undefined呢,问题出在了this上,当代码执行到了setTimeout( )的时候,此时的this已经变成了window对象(setTimeout( )是window对象的方法),已经不再是obj对象了,所以我们用this.x获取的时候,获取的不是obj.x的值,而是window.x的值,再加上window上没有定义属性x,所以得到的结果就是:undefined。

如果使用箭头函数来编写同样的一段代码,得到的this.x又是另一番景象。

//定义一个对象
var obj = {
  x:100,//属性x
  show(){
    //延迟500毫秒,输出x的值
    setTimeout(
   //不同处:箭头函数
      () => { console.log(this.x)},
      500
    );
  }
};
obj.show();//打印结果:100

为什么会是这样呢?
箭头函数的this绑定看的是this所在的函数定义在哪个对象下,绑定到哪个对象则this就指向哪个对象 。


持续更新...

上一篇 下一篇

猜你喜欢

热点阅读