JavaScript中this的绑定机制解析

126 阅读8分钟

为什么要有this

我们看下面的代码例子

function identify(context) {
    return context.name.toUpperCase()
}


function speak(context) {
    var gretting = 'Hello,I am' + identify(context)
    console.log(gretting);

}
var me = {
    name: 'Tom'
}

speak(me)

在这段代码中,我们是想实现传参并将传入参数的字符全部变为大写,最后输出(Hello,I am(我们传入参数的大写))然后我们写了两个函数 speakidentify,在这里我们并没有调用this关键字,执行speak,将me对象传入speak对象中,然后调用对象的name属性,但是为了实现将属性的内容大写,我们又要使用到identify的函数,并要将对象me传入identify中,实现这个功能并返回,最后进行输出,下面是执行结果

34.png

但这样子的话,但凡我们需要添加一个新功能,就必须在所写的函数中进行传参,这样的话一个参数被传到几十个函数里,会导致十分混乱

让我们来使用this关键字优化一下

function identify() {
    return this.name.toUpperCase()
}


function speak() {
    var gretting = 'Hello,I am' + identify.call(this)
    console.log(gretting);

}
var me = {
    name: 'Tom'
}
speak.call(me)

可以看到,在两个函数中,我们并没有进行传参,而是用this代表了我们想传入的对象,看看执行结果是否和上面一样

35.png

可以看到运行结果是一样的,所以我们得出一个结论;this 让函数可以自动引用合适的上下文对象

this的概念

我们要先明白一些this的概念;this 是一个非常重要的关键字,它永远是一个代词,在js中永远代指某一个域,且this只在域中存在才有意义。 在全局作用域下它代指(在浏览器中是window,在Node.js中是global),下面是代码验证

36.png 可以看到this在全局下指向的是(在浏览器中是window,在Node.js中是global)

this的绑定规则

默认绑定

函数独立调用,this指向window

function foo(){
    let person = {
        name: '彭于晏',
        age: 42
    }
    console.log(this);
    
}
    foo()

让我们来分析一下这段代码,在这段代码中,我们写了一个foo函数并执行,可以看到this关键字写在foo里面,属于foo函数,那这个this代指谁呢,我们来看看输出结果

37.png 可以看到这个this还是代指的(在浏览器中是window,在Node.js中是global),我们再看看下面的代码

function foo(){
    let person = {
        name: '彭于晏',
        age: 42
    }
    console.log(this);
    
}
function bar() {
    let person ={
        name: '金城武',
        age:51
    }
    foo()
}

bar()

这段代码中,我们又创建了一个bar函数并将foo函数放入bar函数中执行,将bar函数放入全局中执行,此时this关键字还是属于foo函数,但foo函数从在全局执行变成了在bar函数中执行,那此时this代指谁呢?我们来看看结果

38.png

可以看到,此时this代指的还是(在浏览器中是window,在Node.js中是global)

可以得出this的绑定和和所在函数的位置没有关系,当this所在函数独立调用的时候,它绑定的就是(在浏览器中是window,在Node.js中是global)。

隐式绑定

当函数的引用有上下文对象(当函数被某一个对象所拥有且调用)this指向该上下文对象

function foo(){
    console.log(this);
}

const obj ={
    a: 1,
    foo: foo
}
obj.foo()

在这段代码中我们创建了foo函数和obj对象,并在obj对象当中将foo函数添加进去了,最后在全局下调用obj对象并执行,那此时foo中的this绑定了谁呢

下面是代码的执行结果

39.png

可以看到此时this代表的就是调用函数foo的对象obj

那当函数的引用有一连串上下文对象,this指向谁呢

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

const obj ={
    a: 1,
    foo: foo
}
const obj2 = {
    a: 2,
    obj: obj
}
obj2.obj.foo() 

在这段代码中foo()属于obj,obj属于obj2,所以foo也属于obj,那此时foo函数中的this代指谁呢,这里只需遵守一个就近原则,那就是离函数近的那一个,下面是代码执行结果

40.png 我们可以得出结论;当函数的引用有一连串上下文对象,this指向最近的那个对象

显示绑定

调用call apply bind方法显示的将函数的this绑定到一个对象上 我们来看看实例

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

在这段代码中,我们独立调用foo函数,因此foo中的this应该是指向全局,但是全局中没有a,因此打印出来的就是undefined,那我们想让this不用隐式绑定去绑定在obj对象上,可以怎么样呢

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

我们可以使用call方法强行将foo中的this指向obj,下面是运行结果

41.png

可以看到我们实现想要的效果了,this的指向被强行指向了obj

那如果函数foo需要接收参数呢

function foo(x,y) {
    console.log(this.a,x+y);
}
var obj = {
    a: 1
}
foo.call(obj)

此时怎么给foo函数进行传参呢,要弄懂这个问题,我们要先知道foo是怎么被执行的,在这段代码中我们只是使用了call方法,并没有去人为触发foo函数,其实就是在call方法中帮我们触发了foo函数,所以本来是foo接受的参数call也会帮我们接收

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

看看结果是否正确

42.png

没有问题,结果正常输出了

除了call方法还有apply方法,它们俩的区别就是接收参数时call是零散的接收,而apply是通过数组去接收 foo.call(obj,2,3),foo.apply(obj,[2,3]),还有一个和call方法一样,只是需要函数体去接收 let bar=foo. bind(obj,2,3),然后再执行bar,我们也可以将参数传入接收的函数体中let bar=foo. bind(obj),bar(2,3)

new绑定

提到new,我们可以先了解一下new的原理下面的文章有进行讲解 juejin.cn/post/743932…

function Person() {
    this.name = '彭于晏',
    this.age = 42
}
let p = new Person()

console.log(p);

在这段代码中this的指向是谁呢,在new的原理中,我们知道我们创建了一个对象,且当函数内部存在return且返回的是一个引用数据类型的时候,new的执行结果就是这个引用类型的数据,而this的绑定就是new创建出来的这个对象,所以我们打印出来new中创建对象的数据,下面是代码执行结果

43.png

箭头函数

1,箭头函数没有this,写在箭头函数中的this那也是它外层的非箭头函数的

function foo() {
  let bar = function () {
    let baz = () => {
      let fn = () => {
        console.log(this);
      }
      fn()
    }
    baz()
  }
  bar()
}
foo()

看这段代码,this写在fn箭头函数里面,那这个this属于谁呢,我们要知道箭头函数里面没有this,所以我们要一层一层往外判断首先fn是箭头函数,所以不是它的,然后是baz,baz也是箭头函数所以也不是baz的,然后是bar,bar不是箭头函数,所以this是属于bar函数的

2,箭头函数不能作为构造函数使用


let Foo = () => {
  this.name = '廖总'
}

let foo = new Foo()

在这段代码中,我们想得到一个对象foo,那我们能通过new关键字从箭头函数中来获得foo对象吗?看看代码运行结果

44.png 这里报错了,所以箭头函数是不能作为构造函数使用的。

call方法的实现原理

call方法可以直接将函数的this绑定在对象上,其实也是用了隐式绑定规则进行了非独立调用,将函数将 foo 引用到 obj 上,让 obj 调用foo,移除obj上的 foo,让我们来手搓一个效果一样的方法

function foo(x, y) {
    console.log(this.a);
    return x + y;
  }
  const obj = {
    a: 1
  }
  
  
  Function.prototype.myCall = function (...args) {
    const context = args[0]
    const arg = args.slice(1)   // [2, 3, 4]
    context.fn = this
    const res = context.fn(...arg)
    delete context.fn
    return res
  }
  
  
  let res = foo.myCall(obj, 2, 3, 4)
  console.log(res);

这段代码上我们在Function函数的原型上创建了一个myCall的方法形参...args表示将传入的实参以数组的方式使用,然后我们线拿到数组下标为0的对象,然后将剩下的内容分割出来然后我们用给传入对象也就是context添加一个fn属性,这个fn属性我们需要变为foo函数使对象能够调用这个函数,而在全局中myCall是由foo进行的隐式调用所以在myCall方法中,this表示的就是foo,我们将它添加到对象当中,然后让context 调用foo,我们知道在这里arg是分割后的数组,我们再用一下...arg可以将数组里的元素拆解出来,然后交给foo进行调用并将调用结果给res然后我们不能真的给obj对象中添加foo函数,所以要将其删除,最后将obj 调用foo的结果进行返回,所以最后foo.myCall(obj, 2, 3, 4)的效果与obj.foo(2, 3, 4)是一样的,我们来看看输出结果

45.png

输出结果我们们分析的预期是一样的

总结

this关键字在JavaScript中用于指代当前执行上下文的对象。通过使用this,可以简化对象内部方法的参数传递,使代码更加简洁和易于维护。this的绑定规则包括默认绑定、隐式绑定、显示绑定(callapplybind)和new绑定。箭头函数没有自己的this,而是继承外层函数的this。理解this的概念和绑定规则对于编写高效的JavaScript代码至关重要。