JavaScript中this的原理

this是 JavaScript 语言的一个关键字。

它是函数运行时,在函数体内部自动生成的一个对象,只能在函数体内部使用。

var obj = {
  foo: function () { console.log(this.bar) },
  bar: 1
};

var foo = obj.foo;
var bar = 2;

obj.foo() // 1
foo() // 2

上述调用foo()函数,输出不同的结果,这种差异的原因,就在于函数体内部使用了this关键字。this指的是函数运行时所在的环境。对于obj.foo()来说,foo运行在obj环境,所以this指向obj;对于foo()来说,foo运行在全局环境,所以this指向全局环境。所以,两者的运行结果不一样。

内存的数据结构

JavaScript 语言之所以有this的设计,跟内存里面的数据结构有关系。

var obj = { foo:  5 };

上面的代码将一个对象赋值给变量obj。JavaScript 引擎会先在内存里面,生成一个对象{ foo: 5 },然后把这个对象的内存地址赋值给变量obj


1.png

也就是说,变量obj是一个地址(reference)。后面如果要读取obj.foo,引擎先从obj拿到内存地址,然后再从该地址读出原始的对象,返回它的foo属性。

原始的对象以字典结构保存,每一个属性名都对应一个属性描述对象。举例来说,上面例子的foo属性,实际上是以下面的形式保存的。

2.png
{
  foo: {
    [[value]]: 5
    [[writable]]: true
    [[enumerable]]: true
    [[configurable]]: true
  }
}

注意,foo属性的值保存在属性描述对象的value属性里面。

函数

这样的结构是很清晰的,问题在于属性的值可能是一个函数。

var obj = { foo: function () {} };

这时,引擎会将函数单独保存在内存中,然后再将函数的地址赋值给foo属性的value属性。

3.png
{
  foo: {
    [[value]]: 函数的地址
    ...
  }
}

由于函数是一个单独的值,所以它可以在不同的环境(上下文)执行。

var f = function () {};
var obj = { f: f };

// 单独执行
f()

// obj 环境执行
obj.f()

环境变量

JavaScript 允许在函数体内部,引用当前环境的其他变量。

var f = function () {
  console.log(x);
};

上面代码中,函数体里面使用了变量x。该变量由运行环境提供。

现在问题就来了,由于函数可以在不同的运行环境执行,所以需要有一种机制,能够在函数体内部获得当前的运行环境(context)。所以,this就出现了,它的设计目的就是在函数体内部,指代函数当前的运行环境。

var f = function () {
  console.log(this.x);
}

上面代码中,函数体里面的this.x就是指当前运行环境的x。

var f = function () {
  console.log(this.x);
}

var x = 1;
var obj = {
  f: f,
  x: 2,
};

// 单独执行
f() // 1

// obj 环境执行
obj.f() // 2

上面代码中,函数f在全局环境执行,this.x指向全局环境的x。


4.png

在obj环境执行,this.x指向obj.x。

5.png
  • obj.foo()是通过obj找到foo,所以就是在obj环境执行。
  • 一旦var foo = obj.foo,变量foo就直接指向函数本身,所以foo()就变成在全局环境执行。

this的绑定方式

默认绑定

默认绑定是在不使用其他绑定规则时的规则,通常是独立函数的调用。

function greeting() {
  console.log(`Hello, ${this.name}`);
}

var name = 'Eric';

greeting();

// Hello, Eric

隐式绑定

隐式绑定指的是在一个对象上调用函数。

通过 obj 调用 greeting 方法,this 就指向了 obj

将 obj.greeting 赋给了一个全局的变量 otherGreeting,所以在执行 otherGreeting 时,this 会指向 window.

function  greeting() {
  console.log(`Hello,${this.name}`);
};

var name = 'Eric';

var obj = {
  name: 'World',
  greeting,
};

var otherGreeting = obj.greeting;

greeting(); // Hello,Eric
obj.greeting(); // Hello,World
otherGreeting().greeting(); // Hello,Eric

异步操作时候隐式绑定丢失问题

如果涉及到回调函数(异步操作),就要小心隐式绑定的丢失问题。

function  greeting() {
  console.log(`Hello,${this.name}`);
};

var name = 'Eric';

var obj1 = {
  name: 'Obj1',
  greeting() {
    setTimeout(function() {
      console.log(`Hello,${this.name}`);
    })
  }
};

var obj2 = {
  name: 'Obj2',
  greeting,
};

obj1.greeting(); //Hello,Eric

obj2.greeting(); //Hello,Obj2

setTimeout(obj2.greeting, 100); //Hello,Eric

setTimeout(function() {
  obj2.greeting();
}, 200); //Hello,Obj2
  • obj1.greeting()调用

因为涉及到异步操作setTimeout。在JavaScript中,一段代码执行时,会先执行宏任务中的同步代码,当遇到setTimeout之类的宏任务,那么就把这个 setTimeout内部的函数推入「宏任务的队列」中,下一轮宏任务执行时调用。当本轮宏任务调用结束后,下一轮宏任务执行时,此时函数位于内存中,this指向全局环境。此时 this.name 就是 Eric

  • obj2.greeting()调用

greeting函数位于内存中,通过obj2来调用,那么this的指向环境变成了obj2。如前面讲述的图所示:


5.png
  • setTimeout(obj2.greeting, 100)调用

可以理解为将 obj2.greeting 赋值给一个新的变量(此时与obj1.greeting类似),所以此时 this 也是指向了 window。

  • setTimeout(function() {obj2.greeting();}, 200)调用

此时setTimeout的function参数里面包含了obj2.greeting()方法,调用则是隐式绑定,此时 this 指向 obj2。我们可以做个实验来验证

setTimeout(function() {
  console.log(this);
  obj2.greeting();
}, 200); 

其中,打印出来的this指向window,所以在200毫秒后,function回调函数里面的this环境指向window,这个与前面的实验obj1.greeting()调用是一致的,此时相当于在全局环境中,我们来调用了obj2.greeting(),这个与前面的实验obj2.greeting()调用是一致的,所以打印的结果是Hello,Obj2

显式绑定

显示绑定就是通过 call, apply, bind 来显式地指定 this 的绑定对象。三者的第一个参数都是传递 this 指向的对象,call 与 apply 的区别是前者从第二个参数起传递一个参数序列,后者传递一个数组,call, apply 和 bind 的区别是前两个都会立即执行对应的函数,而 bind 方法不会。

我们通过 call 显式绑定 this 指向的对象来解决隐式绑定丢失的问题。

function  greeting() {
  console.log(`Hello,${this.name}`);
};

var name = 'Eric';

var obj = {
  name: 'Obj',
  greeting,
};

var otherGreeting = obj.greeting;

// 强制将 this 绑定到 obj
otherGreeting.call(obj); // Hello,Obj
setTimeout(obj.greeting.call(obj), 100); // Hello,Obj

在使用显式绑定时,如果将 null, undefined 作为第一个参数传入 call, apply 或者 bind,实际应用的是默认绑定。

function greeting() {
  console.log(`Hello,${this.name}`);
};

var name = 'Eric';

var obj = {
  name: 'Obj',
  greeting,
};

var otherGreeting = obj.greeting;
// this 仍然指向 window
otherGreeting.call(null); //Hello,Eric

箭头函数

  • 函数体内的 this 对象,继承的是外层代码块的 this
  • 不可以当作构造函数,也就是说,不可以使用 new 命令,否则会抛出一个错误。
  • 不可以使用 arguments 对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
  • 不可以使用 yield 命令,因此箭头函数不能用作 Generator 函数。
  • 箭头函数没有自己的 this,因此不能使用 call()、apply()、bind()等方法改变 this 的指向。
var obj = {
  hi: function() {
    console.log(this);
    return () => {
      console.log(this);
    };
  },

  sayHi: function() {
    return function() {
      console.log(this);
      return () => {
        console.log(this);
      };
    };
  },

  say: () => {
    console.log(this);
  }
};

let hi = obj.hi(); // 输出 obj 对象
hi(); // 输出 obj 对象
let sayHi = obj.sayHi();  // 输出 window
fun1(); // 输出 window
obj.say(); // 输出 window
  1. 第一步是隐式绑定,此时 this 指向 obj,所以打印出 obj 对象
  2. 第二步执行 hi() 方法,虽然看着像闭包,但这是一个箭头函数,它会继承上一层的 this,也就是 obj,所以打印出 obj 对象
  3. 因为 obj.sayHi() 返回一个闭包,所以 this 指向 window,因此打印出 window 对象
  4. 同样箭头函数继承上一层的 this,所以 this 指向 window,因此打印出 window 对象
  5. 最后一次输出,因为 obj 中不存在 this,因此按作用域链找到全局的 this,也就是 window,所以打印出 window 对象
var obj = {
  name: 'Eric',
  greeting() {
    setTimeout(() => {
      console.log(`Hello, ${this.name}`);
    })
  },
  greeting2() {
    console.log(`Hello, ${this.name}`);
  },

  greeting3() {
    setTimeout(function() {
      console.log(`Hello, ${this.name}`);
    });
  }
};

var name = 'Global';
obj.greeting();   //Hello, Eric
obj.greeting2();  //Hello, Eric
obj.greeting3();  //Hello, Global
  1. obj.greeting(),虽然 setTimeout 会将 this 指向全局,但箭头函数继承上一层的 this,也就是 obj.greeting() 的 this,因为这是一个隐式绑定,所以 this 指向 obj,所以箭头函数的 this 也会指向 obj.
  2. obj.greeting2(),这里是一个隐式绑定,所以 this 指向 obj
  3. greeting3(),setTimeout 会将 this 指向全局

解析一

var number = 5;
var obj = {
  number: 3,
  fn: (function() {
    var number;
    this.number *= 2;
    number = number * 2;
    number = 3;
    return function() {
      var num = this.number;
      this.number *= 2;
      console.log(num);
      number *= 3;
      console.log(number);
    };
  })(),
};
var fn = obj.fn;
fn.call(null);
obj.fn();
console.log(window.number);

因为 obj.fn 是一个立即执行函数(this 会指向 window),所以在 obj 创建时就会执行一次,并返回闭包函数。

var number; // 创建了一个私有变量 number 但未赋初值
this.number *= 2; // this.number 指向的是全局那个 number,所以 window.number = 10
number = number * 2; // 因为私有变量 number 未赋初值,所以乘以 2 会变为 NaN
number = 3; // 此时私有变量 number 变为 3

接着执行下面两句:

var fn = obj.fn;
fn.call(null);

因为将 obj.fn 赋值给一个全局变量 fn,所以此时 this 指向 window。接着,当 call 的第一个参数是 null 或者 undefined 时,调用的是默认绑定,因此 this 仍然指向 window.

var num = this.number; // 因为 window.number = 10,所以 num 也就是 10
this.number *= 2; // window.number 变成了 20
console.log(num); // 打印出 10
number *= 3; // 因为是闭包函数,有权访问父函数的私有变量,所以此时 number 为 9
console.log(number); // 打印出 9

当执行 obj.fn(); 时,此时的 this 指向的是 obj:

var num = this.number; // 因为 obj.number = 3,所以 num 也就为 3
this.number *= 2; // obj.number 变为 6
console.log(num); // 打印出 3
number *= 3; // 上一轮私有变量为变成了 9,所以这里变成 27
console.log(number); // 打印出 27

最后打印出 window.number 就是 20

最终结果:
10
9
3
27
20

解析二

var length = 10;

function fn() {
  console.log(this.length);
}

var obj = {
  length: 5,
  method: function(fn) {
    fn();
    arguments[0]();
  },
};

obj.method(fn, 1);

最终结果:

10
2

传入了 fn 而非 fn(),相当于把 fn 函数赋值给 method 里的 fn 执行,所以这里是默认绑定,此时 this 指向 window,所以执行 fn() 时会打印出 10

arguments0,就相当于执行 fn(),所以是隐式绑定,此时 this 指向 arguments,所以 this.length 就相当于 arguments.length,因为我们传递了两个参数,因此返回 2

window.val = 1;

var obj = {
  val: 2,
  dbl: function() {
    this.val *= 2;
    val *= 2;
    console.log('val:', val);
    console.log('this.val:', this.val);
  },
};

obj.dbl();
var func = obj.dbl;
func();

最终结果:

2, 4
8, 8

第一次调用是隐式调用,因此 this 指向 obj,所以 this.val 也就是 obj.val 变成了 4,但是 dbl 方法中没有定义 val,所以会沿着作用域链找到 window.val,所以会依次打印出 2,4

第二次是默认调用,this 指向 window,window.val 会经历两次乘 2 变成 8,所以会依次打印出 8,8

总结

  • 函数是否在 new 中调用(new 绑定),如果是,那么 this 绑定的是新创建的对象。
  • 函数是否通过 call,apply 调用,或者使用了 bind(即硬绑定),如果是,那么 this 绑定的就是指定的对象。
  • 函数是否在某个上下文对象中调用(隐式绑定),如果是的话,this 绑定的是那个上下文对象。一般是 obj.foo()
  • 如果以上都不是,那么使用默认绑定。如果在严格模式下,则绑定到 undefined,否则绑定到全局对象。
  • 如果把 Null 或者 undefined 作为 this 的绑定对象传入 call、apply 或者 bind,这些值在调用时会被忽略,实际应用的是默认绑定规则。
  • 如果是箭头函数,箭头函数的 this 继承的是外层代码块的 this。

Notes from
http://www.ruanyifeng.com/blog/2018/06/javascript-this.html

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