呕心沥血对 JavaScript 中 this 的理解

this到底指向哪里

在JavaScript 中,函数中this到指向,经常会引起自己的困惑,所以写一篇文章来整理下自己对这个疑点的理解。

函数的执行

所有的 JavaScript 函数都有一个内部属性[[Call]],用来运行该函数。

F.[[Call]](thisArg, argumentsList)

上面代码中,F是一个函数对象,[[Call]]是它的内部方法,F.[call]表示运行该函数,thisArg表示[[Call]]运行时this的值,argumentsList则是调用时传入函数的参数。

thisArg和this是什么关系?ECMA规范里的描述是这样的:

If the function code is strict code, set the ThisBinding to thisArg.
在严格模式下,thisArg和this是一一对应的。
 
Else if thisArg is null or undefined, set the ThisBinding to the global object.
如果thisArg为null或者undefined则this指向全局对象。
 
Else if Type(thisArg) is not Object, set the ThisBinding to ToObject(thisArg).
如果thisArg为非对象类型,则会强制转型成对象类型。
 
Else set the ThisBinding to thisArg.
剩下的情况thisArg和this为一一对应的关系。

概括一下就是3条:
1.thisArg和this是一一对应。
2.非严格模式下thisArg为null或者undefined则this指向全局对象。
3.如果thisArg为非对象类型,则会强制转型成对象类型。

记住这三条,JavaScript 和其他语言不同的是,函数中this的指向不是在函数声明时指定的,而是调用时指定。那this到底是指向哪里呢?

In JavaScript, as in most object-oriented programming languages, this is a special keyword that is used within methods to refer to the object on which a method is being invoked.
一般而言JavaScript中,this 指向函数执行的当前对象

在JavaScript 中函数调用大概有这4中情况:

  • 1.调用对象方法
  • 2.普通函数调用
  • 3.间接调用
  • 4.构造函数调用

我们分别来看一下这四种情况

1.调用对象方法

调用对象的方法类似 obj.fn() 的调用形式。

举个栗子

var a = {
    name: 'bob',
    showName: function(){
        console.log(this.name)
    }
}

var b = {
    name: 'lily',
    showName: a.showName
}

b.showName() //lily

b.showName()函数引用了函数a.showName,this是在函数执行时指定,showName()执行时,this指向了此时showName函数运行时的所在宿主对象b

回顾一下 JavaScript 函数内部属性[[Call]]

F.[[Call]](thisArg, argumentsList)

一般而言JavaScript中,this 指向函数执行的当前对象,函数执行时 thisArg 传入的是此时函数的宿主对象。

再来看看下面的图:
{% img /img/this01.jpg%}
函数b.showName()a.showName() 指向了同一个函数,当使用a.showName()方法调用时,this指向宿主对象a,使用b.showName()调用时,则this指向宿主对象b

2.普通函数调用

直接调用声明的函数 fn()

一个栗子

var name = 'jack'
var a = {
    name: 'bob',
    showName: function(){
        console.log(this.name)
    }
}

var b = a.showName;

b() //jack

b函数是引用的函数a.showName,在b()执行的时候没有明确的宿主对象。这个时候拿出我们刚才看的ECMA规则,

F.[[Call]](thisArg, argumentsList)

没有明确宿主对象的时候 thisArg 传入的相当于 null 或者 undefined,非严格模式下 thisArg 为 null 或者 undefined 则 this 指向全局对象。在浏览器中 this 指向全局对象 window 。

再举个栗子 --立即执行函数

var name = 'jack'
var a = {
    name: 'bob',
    showName: function(){
       (function(){
            console.log(this.name)
        }())
    }
}

var b = a.showName;

b() //jack

b()函数中声明了一个立即执行的匿名函数,继续回顾 JavaScript 函数内部属性[[Call]]

F.[[Call]](thisArg, argumentsList)

b()函数执行时没有明确的宿主对象,thisArg 传入的是 null 或者 undefined,此时 this 在浏览器中指向 window 。

3.间接调用

间接调用是指使用 apply 和 call ,bind 等方法改变函数执行当前对象。方法的第一个参数改变函数中 this 的指向。

举个栗子

var age = 18;
var a = {
    name:'lili',
    age:16
}

var b = function(){
    console.log(this.age)
}
b() // 18
b.call(a) //16

F.call(thisObj,[arg1……]) 方法的 thisObj 和 arglist 会作为 F 内部属性 [[Call]] 的参数传入进行函数的执行操作。
上面这个栗子 b 函数使用 call(a) 方法调用函数,会将第一个参数对象 a 作为 b 函数内部属性[[Call]]的第一个参数 thisArg 来执行函数。此时b 函数内部 this 指向 athis.agea.age 打印出16。

apply 和 call 都会立即调用函数,两者的区别就是 arglist 传参格式不同。call 需要把参数按顺序传递进去,而 apply 则是把参数放在数组里。

call([thisObj[,arg1[, arg2[, [,.argN]]]]])
apply([thisObj[,argArray]])
-------------------------------------------
fn.call(thisObj,args1,args2);
//或者
fn.apply(thisObj,[args1,args2]);

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

fn.bind(thisObj,args1,args2); //bind不会立即调用对应函数,会返回一个绑定函数

有一个地方值得注意

function a(){
    console.log(this)
}
function b(){}
var c = {};

a.call();   //window
a.call(null);   //window
a.call(undefined);   //window
a.call(1);   //Number
a.call('');   //String
a.call(true);   //Boolean
a.call(b);   //function b(){}
a.call(c);   //Object

回忆一下ECMA规范,非严格模式下 thisArg 为 null 或者 undefined 则 this 指向全局对象,如果thisArg为非对象类型,则会强制转型成对象类型。如上个栗子,我们非严格的浏览器环境下,传入空,null,undefined 都会指向 window,传入 String,Boolean,Number 等基础类型,会返回一个基础类型包装过的对象。

4.构造函数调用

构造函数调用指使用 new 关键词,调用构造器,创建一个新对象。

举个栗子

function Worker(){
    this.name = 'lily';
    this.age = '16';
    console.log(this);
}
Worker(); // window
new Worker(); // Worker {name: "lily", age: "16"}

函数调用 new 操作符时,会创建一个新对象,并用 this 指向它。

其他的一些疑点

一般而言JavaScript中,this 指向函数执行的当前对象,函数的调用一般可以概括为上面4中情况。但是还有一些特殊的情况,影响 this 的指向。

setTimeout , setInterval 函数执行时 this 的对象时全局对象

举个栗子

var a = {
    name: 'lucy',
    showName: function(){
        console.log(this);
    },
    myName: function(){
        //此时的 this 指向对象a
        setTimeout(this.showName,1000) //setTimeout 延迟执行 a.showName 函数中的this 指向window
    }
}
 a.myName()// window

setTimeout 中的延迟执行的代码中的 this 永远都指向 window。

我们可以使用 bind 方法,改变函数内 this 的指向

var a = {
    name: 'lucy',
    showName: function(){
        console.log(this);
    },
    myName: function(){
        //此时的 this 指向对象a
        setTimeout(this.showName.bind(this),1000) //setTimeout 延迟执行 a.showName 函数中的this 指向window
    }
}
 a.myName()// {name: "lucy", showName: ƒ, myName: ƒ}

超时调用的代码都是在全局作用域中执行的,因此函数中this的值在非严格模式下指向window对象,在严格模式下是undefined
----《JavaScript高级程序设计》

再看一个栗子,使用setTimeout(字符串代码, 延迟)

var name = 'lili';

function b (){
    var name =  'jack';
    setTimeout(function(){
        console.log(name); //函数内声明的变量 name
        console.log(this.name); //this 指向window  window.name lili
    },1000)
}

function c (){
    var name =  'lucy';
    setTimeout('console.log(name)',1000)
    setTimeout('console.log(this.name)',1000)
}

b() // jack lili
c() // lili lili

对比一下

var name = 'lili';
var a = {
    name: 'lucy',
    showName: function(){
        console.log(name);
        console.log(this.name);
    },
    myName: function(){
        setTimeout("this.showName()",1000) //在window全局作用域下创建 函数this.showName(),window下无showName方法。
    }
}
a.myName() // Uncaught TypeError: this.showName is not a function

有个共同的现象如果setTimeout(字符串代码, 延迟)使用这种方式执行的函数,默认会在 window 全局作用域下创建一个新的函数。此时this 会指向 window。

小结一下

  • setTimeout中的延迟执行的代码中的this永远都指向window。
  • setTimeout(this.showName,1000),setTimeout 参数中的this,是根据上下文来判断的。
  • setTimeout(“this.showName()”,1000) 执行代码如果是字符串形式的代码,默认在window全局作用域下创建一个新的函数。
eval 方法解析出的this
var name = 'jack'
var a = {
    name: 'bob',
    showName: function(){
        eval('console.log(this.name)'); //和直接执行console.log(this.name) 相同
    }
}
a.showName() // bob

eval 相当于是在当前位置填入代码。

lamda表达式(箭头函数)中 this 的指向

lamda 表达式俗称肩头函数。箭头函数 this 的定义:箭头函数中的 this 是在定义函数的时候绑定,而不是在执行函数的时候绑定。这个和普通函数的 this 绑定刚好相反。

举个栗子

var a = {
    name: 'lucy',
    showName: function(){
        console.log(this.name);
    },
    myName: function(){
        setTimeout(() => {
            this.showName();
        },1000)
    }
}
a.myName()// lucy

setTimeout 执行的匿名函数为箭头函数,定义时就绑定了上下文中的 this ,即使在 setTimeout 延迟在全局作用域中执行,因为已经绑定了 this ,所以仍旧指向 a.name

use strict 模式对this的影响

严格模式下的函数调用,回忆一下文章刚开始介绍的ECMA规则第一条:thisArg和this是一一对应。
在非严格模式下 thisArg 为 null 或者 undefined 则 this 指向全局对象,而在严格模式下 thisArg 和 this 是一一对应,就意味着函数中 thisArg 为 null 或者 undefined 时,this 不会转化指向全局对象,会遵守一一对应原则,传入的什么就是什么。

对比一下下面两个栗子

function a(){
    console.log(this)
}

(function(){
    'use strict';
    console.log(this); //undefined
    a.apply(null)      //window
    a.apply(this)      //window
    a.apply(undefined) //window
})()
'use strict';
function a(){
    console.log(this)
}

(function(){
    console.log(this); //undefined
    a.apply(null)      //null
    a.apply(this)      //undefined
    a.apply(undefined) //undefined
})()

use strict 模式在函数内部定义,则只会影响函数内部作用域内的代码执行,不会影响定义在此函数作用域外的函数的代码执行。

总结

函数执行是依靠内部属性 [[Call]],用来运行该函数。属性 [[Call]] 有一个 thisArg 的参数来决定函数中的this指向。
三条规则:
1.thisArg和this是一一对应。
2.非严格模式下thisArg为null或者undefined则this指向全局对象。
3.如果thisArg为非对象类型,则会强制转型成对象类型。
函数四种调用方式:

  • 1.调用对象方法,指向宿主函数
  • 2.普通函数调用,指向全局变量
  • 3.间接调用,改变函数内部指向
  • 4.构造函数调用,指向构造后的对象

参考文章
Javascript中this关键字详解
深入理解函数内部原理(六
从Ecma规范深入理解this

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值