浅析JavaScript中的 this 指向

前言

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就指向哪个对象 。


持续更新...

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容

  • 1.概念 在JavaScript中,this 是指当前函数中正在执行的上下文环境,因为这门语言拥有四种不同的函数调...
    BluesCurry阅读 1,120评论 0 2
  • 函数和对象 1、函数 1.1 函数概述 函数对于任何一门语言来说都是核心的概念。通过函数可以封装任意多条语句,而且...
    道无虚阅读 4,521评论 0 5
  • 1. this之谜 在JavaScript中,this是当前执行函数的上下文。因为JavaScript有4种不同的...
    百里少龙阅读 986评论 0 3
  • 导语 不得不说,作为一名初级的前端开发者,this关键字这个问题对于我来说一直是一个痛点,什么是this?什么是函...
    Nicole_tiny阅读 526评论 0 4
  • 01 那年,应该是很久之前的事情了吧,记不清了,只知道那大概是小学的时候了。 “一一,我要转学了,我会给你们我的照...
    古木兮阅读 1,091评论 6 17