JS中最让人疑惑的机制:this

如果你系统学习过JS,肯定花过不少精力在this的机制上,但估计大部分人从来没彻底搞明白过,而现实开发过程中,确实发生过一些this导致的bug,而且这种bug有不错的隐蔽性,难以排查。this有时会略显玄学,主要是因为这几点:

  1. this的绑定发生在函数调用时;
  2. this的指向可以被开发者手动更改;
  3. 箭头函数的this规则和普通函数是完全不一样的;

重要:下文中若无说明,所有例子都运行在浏览器环境下;Node.js中的this有一些不同,文章最后会提到。

this到底是什么?

JS的函数本质上是一个对象,比如有个函数function foo() {},在内存中foo保存的其实是函数的地址,而函数本身则单独存在一块内存区域中,所以foo函数定义在哪里并不意味着其执行在哪里(好像是句废话),我们需要以某种方式获取到当前的执行环境(一般称之为执行上下文,context),这就是this存在的目的。

如何准确判断this的指向

一、找到函数的调用位置

理解了前面说的this的存在意义,则 “this取决于函数的调用位置” 这句话就不难理解了,先来看个简单的例子:
#例子1:

var x = 1;
function foo() {
  var x = 2;
  console.log(this.x);
}
foo(); // ?

浏览器环境下答案为:1

这里foo函数在全局环境下被调用,所以函数中的this指向的就是全局环境,即:

function foo() {
  console.log(this === window); // true
}
foo();

调用独立函数时this的绑定一般称为默认绑定

然而,默认绑定有个限制条件:函数运行在非严格模式下!如果函数运行在严格模式下,如:

var x = 1;
function foo() {
  'use strict';
  var x = 2;
  console.log(this.x);
}
foo();

则会报错Uncaught TypeError,因为严格模式下,函数中的this无法绑定全局环境。

接下去,我们看一种稍微复杂点的情况:
#例子2:

var a = {
  x: 1,
  y: function() {
    return this.x;
  }
};
console.log(a.y()); // ?
var b = {
  x: 2,
  y: a.y
};
console.log(b.y()); // ?

答案:a.y()结果为1,b.y()结果为2

来分析一下:

  1. 对象a中定义了一个函数并赋值给了属性y,属性y保存了函数的地址
  2. 在对象a中找到y保存的内存地址所指向的函数,并执行该函数,所以当前函数执行在a环境下,查找到a环境下的x为1;
  3. 将对象a下属性y保存的函数地址赋值给对象b的属性y;
  4. 在对象b下找到y保存的内存地址所指向的函数,并执行该函数,所以该函数目前的执行环境是b,查找到b环境下的x为2;

上述的分析过程中很重要的一点是理解属性y保存的仅仅是函数的地址,而函数的this则取决于是谁通过函数的地址找到了函数本身并调用了!
例子2中的写法其实和下面的写法输出结果是一样的,属性y都只是拿函数的地址而已:

function foo() {
  return this.x;
}
var a = {
  x: 1,
  y: foo,
};
var b = {
  x: 2,
  y: foo,
};
console.log(a.y()); // ?
console.log(b.y()); // ?

类似上面这种函数的调用位置已存在上下文对象的情况(或者说,函数被调用在某个对象里面),一般称之为this的隐式绑定

#例子3:

var x = 1;
var a = {
  x: 2,
  y: function() {
    return this.x;
  }
};
var b = a.y;
console.log(b()); // ?

按照上面的步骤去分析,很容易得到答案1。因为函数的地址最终赋值给了一个全局变量,并且在全局环境下执行了,这里this最后应用了默认绑定。

这种情况有个术语叫做隐式丢失,即函数中this丢失了它想要绑定的对象,而绑定到了全局上(或者undefined上,取决于是否严格模式)。

变态的情况来了:
#例子4:

var x = 1;
function foo() {
  function bar() {
    console.log(this.x);
  }
  bar();
}
var a = {
  x: 2,
  y: foo,
}
a.y();

答案:1。
这里依然还是理解一点:虽然foo函数是在对象a下调用的,但是foo函数本身不属于对象a,所以foo函数内部调用的bar函数与对象a无关!所以bar函数的this使用的默认绑定,即绑定在全局对象上。

接着看高阶函数this的绑定行为,这可能也是很多人为之困惑的一个地方,来看个例子:
#例子5:

var x = 1;
var a = {
  x: 2,
  y: function() {
    console.log(this.x)
  }
}
setTimeout(a.y, 10)

答案:浏览器中执行,结果为1。
浏览器环境内置的setTimeout函数实现和下面的伪代码类似:

function setTimeout(fn, delay) {
  fn(); // delay毫秒之后执行fn
}

和前面分析的一样,通过a.y保存的是函数的地址,现在把该函数地址传递给了setTimeoutsetTimeout内部调用该函数时已与对象a无关了,所以这里应用了默认绑定,又是一个隐式丢失的问题!

二、手动更改this的指向

上面所说的都是在开发者无意识情况下的this绑定,下面介绍开发者刻意更改this指向的情况。
JS提供了几个可以更改函数执行上下文的方式,首先是著名的callapply,我们正常调用一个函数时样子:

function foo(arg1, arg2, ...) {}; // 声明函数
foo(arg1, arg2, ...); // 调用函数

调用函数的行为foo()其实可以显式成foo.call(ctx, arg1, arg2, ...)foo.apply(ctx, [args])callapply的第一个参数,就是要执行的函数的上下文,默认情况下,ctx指向foo的调用者,比如例子2中,a.y()a.y.call(a)等效,b.y()b.y.call(b)等效。我们也可以更改ctx:

var a = {
  x: 2
}
function foo() {
  console.log(this.x); // 2
  console.log(this === a); // true
}
foo.call(a);

这种绑定方式称之为显示绑定

如果你传入了一个原始值(字符串类型、布尔类型或者数字类型)来当作 this 的绑定对 象,这个原始值会被转换成它的对象形式(也就是new String(..)、new Boolean(..)或者 new Number(..)),这通常被称为“装箱”。

callapply两个方法作用是一样的,只是他们的传参方式不一样。现在我们可以在调用函数时,任意更改函数的this指向了!但是,例子5中的情况我们应该如何做呢?callapply只能在调用时进行手动绑定,可setTimeout这类JS内置函数我们无法更改回调函数的调用行为。
JS提供了bind方式来解决这种问题。如例子5这样写:

var x = 1;
var a = {
  x: 2,
  y: function() {
    console.log(this.x); // 2
  }
}
setTimeout(a.y.bind(a), 10);

bind() 方法会创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。

关于bind的用法,这里不做更多介绍,可查看MDN,里面介绍很详细

三、new绑定

简单来说,当我们用new关键字修饰一个函数时,会自动执行下面一系列操作:

  1. 创建(或者说构造)一个全新的对象。
  2. 这个新对象会被执行[[原型]]连接。(本文不关心原型知识)
  3. 这个新对象会绑定到函数调用的this
  4. 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。

#例子6:

function Foo() {
  this.x = 2;
}
foo = new Foo();
console.log(foo.x); // 2

构造函数很简单,是初级的JS基础,但简单的语法可能遇到复杂场景,比如当new绑定遇上隐式绑定甚至显式绑定时,会发生什么?

先来看下如果遇到隐式绑定的情况:
#例子7:

function foo(num) {
  this.x = num;
}
var a = {
  foo: foo
};
a.foo(1)
console.log(a.x); // ?
var b = new a.foo(2);
console.log(a.x); // ?
console.log(b.x); // ?

答案:第一个a.x结果为1,第二个a.x结果也为1,b.x结果为2
执行结果告诉我们,当函数调用位置在某个对象中时(即隐式绑定),如果调用使用了new关键字进行修饰,则函数内部的this指向的是新创建的实例对象,即:new绑定的优先级高于隐式绑定

接下去探索new绑定遇上显式绑定的情况,注意:callapply不能与new一起使用,即JS不允许这么做:new foo.call(ctx),会报错:Uncaught TypeError: foo.call is not a constructor。但我们通过bind来绑定this
#例子8:

function foo(num) {
  this.x = num;
}
var a = {
  x: 0,
};
var bar = foo.bind(a); // 将foo内的this绑定到a上
bar(1);
console.log(a.x); // ?
var b = new bar(2);
console.log(a.x); // ?
console.log(b.x); // ?

答案:第一个a.x结果为1,第二个a.x结果也为1,b.x结果为2
虽然我们先通过bindfoo函数的this绑定到了对象a上,但是我们new这个绑定后的函数,函数内部的this依然指向了新的实例对象!由此可见:new绑定的优先级高于显式绑定

普通函数的this绑定小总结

  1. 判断this指向最重要的一点是找到函数的调用位置,还要理解一点:无论函数如何被赋值,赋值的都是函数的地址,而不是函数本身。函数的this取决于是谁通过函数的地址找到了函数本身并调用了。
  2. 如果函数被调用在全局环境,严格模式下,this无法指向window,而是undefined
  3. callapplybind可以手动更改函数的this指向,但遇到new实例化函数时,new绑定的优先级最高!

ES6中的箭头函数的this绑定

回顾一下前文关于隐式丢失的例子5:

var x = 1;
var a = {
  x: 2,
  y: function() {
    console.log(this.x);
  }
}
setTimeout(a.y, 10); // 输出:1

如果不用显示绑定,我们该如何做呢?我相信很多人都写过类似这样的代码:
#例子9:

var x = 1;
var a = {
  x: 2,
  y: function() {
    var self = this;
    setTimeout(function() {
      console.log(self.x);
    }, 10);
  }
}
a.y(); // 输出:2

例子9中我们使用了熟悉的词法作用域来解决this绑定丢失的问题:在this指向确定的作用域内将this赋值给一个变量,此时,该作用域内的子作用域内,就可以直接拿这个变量进行使用了,非常直观!
虽然这样比较完美的解决了this绑定丢失的问题,但是如果项目中充斥着这样的代码,会显得不美观优雅,现在ES6的箭头函数很好地解决了这个问题:
#例子10:

var x = 1;
var a = {
  x: 2,
  y: function() {
    setTimeout(() => {
      console.log(this.x);
    }, 10);
  }
}
a.y(); // 输出:2

例子10中的箭头函数的行为和例子9中的写法类似,但是代码更简洁优雅。

箭头函数的this绑定规则和普通函数是完全不一样的,它用箭头函数所定义的词法作用域覆盖了this本来的值。

我们把例子2中的函数改成箭头函数形式,再来看下结果:

var x = 1;
var a = {
  x: 2,
  y: () => {
    return this.x;
  }
};
console.log(a.y()); // ?
var b = {
  x: 3,
  y: a.y
};
console.log(b.y()); // ?

答案:a.y()输出1,b.y()输出也是1。
即使用call/apply/bind,箭头函数的this也不会改变:

var x = 1;
var a = {
  x: 2
}
var foo = () => {
  console.log(this.x);
}
foo.call(a);

答案:1。

记住:箭头函数没有自己this,所以它的this就是它所在的作用域的this!也正因为箭头函数没有this,所以箭头函数不能作为构造函数用new实例化。

image.png

Node.js中的this机制

Node.js下的this和浏览器环境下有所不同,主要表现在以下几处:

1. 全局环境的this指向module.exports
console.log(this === module.exports); // true
2. 独立普通函数的this指向global
function foo() {
  console.log(this === global); 
}
foo(); // true

这里需要注意一点,Node.js的每个js文件都是单独的模块,就算使用var定义在顶层,也只是这个module的全局变量。

var x1 = 1;
global.x2 = 2;
function foo() {
  console.log(this.x1);  // undefined
  console.log(this.x2);  // 2
}
foo();
3. setTimout机制和浏览器不一致

Node.js的setTimeout可查看官方文档:timer,Node.js环境的setTimeout中的this指向的是Timeout类(setInterval也是一样),看个例子:

var x = 1;
var a = {
  x: 2,
  y: function() {
    console.log(this)
  }
}
setTimeout(a.y, 10)

输出结果为:


image.png

大致就是这些不同,若还有别的情况下this绑定不一致,请无情指出,谢谢!

总结

确实蛮复杂。

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

推荐阅读更多精彩内容