JavaScript内神秘的this

483 阅读5分钟

前言

在JavaScript中,“this”的行为有时确实显得有些“神秘”,因为它不像其他语言中的“this”那样始终指向调用它的对象。在JS中,“this”的实际绑定取决于函数的调用上下文,this的指向取决于函数是如何调用的,这可以导致一些初学者和甚至有经验的开发者时常感到困惑。下面,我们将讨论“this”的不同行为以及如何理解和控制它。

1. 全局作用域中的this

在全局作用域中,this通常指向全局对象:

  • 在浏览器中,全局对象是window
  • 在Node.js中,全局对象是global

this主要写在全局作用域以及函数作用域体内。

2. 函数调用中的this

当函数作为普通函数调用时(不作为任何对象的方法),this的行为如下:

  • 非严格模式(non-strict mode):this指向全局对象(如window)。
  • 严格模式(strict mode):thisundefined
function sayHello() {
    console.log(this);
}
sayHello(); // 在非严格模式下输出 window 对象,在严格模式下输出 undefined

当一个函数没有挂载在别的对象上时,也就是说该函数调用时不作为任何对象的方法,函数内的this会触发默认绑定

举个栗子:

var a = 1 
function foo(){
    var a = 2
    function bar(){
        var a = 3
        function baz(){
            console.log(this.a)
        }
        baz()
    }
    bar()
}
foo()    // 浏览器 打印 1 

每个baz函数是独立调用,其内部this就是指向全局对象。

默认绑定:当一个函数独立调用时,不带任何修饰符的调用,该函数的this指向全局(window)。

3. 方法调用中的this

当函数作为对象的方法被调用时,这个时候触发的就是this的隐式绑定this指向调用该方法的对象:

const obj = {
    myname: '梁总',
    age: 22,
    say: function(){
        console.log(this.myname);
    }
}
obj.say();  // 打印  梁总

隐式绑定: 当一个函数被某个对象拥有,或者函数被某个上下文对象调用时,该函数中的this指向上下文对象

当一个函数被多个对象链式调用,场景如下:

function foo(){
    console.log(this.a)
}
var obj1 = {
    a: 1,
    foo:foo
}
var obj2 = {
    a: 2,
    obj : obj1
}
obj2.obj.foo()  // 浏览器内 打印 1

这个时候就会出现this的隐式丢失:函数被多个对象链式调用时,this指向最近的那个对象。(this的指向遵循就近原则)

4. 构造函数中的this

在构造函数中,this指向新创建的对象实例:

function Person(name) {
    this.name = name;
}
const john = new Person('John Doe');
console.log(john.name); // 输出 "John Doe"

5. 箭头函数中的this

箭头函数没有自己的this绑定;它们从封闭作用域继承this

const person = {
    name: 'John Doe',
    sayHello: () => {
        console.log(`Hello, ${this.name}`);
    }
};
person.sayHello(); // 输出 "Hello, undefined" 或 "Hello, window",取决于全局作用域的`this`

箭头函数中没有this的机制,写在箭头函数中的this那也是外层非箭头函数的this:

var obj = {
    a: 1,
    foo: function(){
        // this    当函数创建时会,其内部会产生一个this
        const fn = ()=>{
            console.log(this.a)
        }
        fn()
    }
}
obj.foo()    //  1

这里fn的内部函数不存在this,但箭头函数fn继承了外部foo函数的this,而函数foo又属于obj对象的方法,所以obj对象调用foo方法时,触发隐式绑定,this会指向obj对象。

6. Event Handler 中的this

在事件处理器中,this通常指向触发事件的DOM元素:

<button id="myButton">Click me</button>
<script>
    document.getElementById('myButton').addEventListener('click', function() {
        console.log(this.id); // 输出 "myButton"
    });
</script>

控制this的指向

为了更精确地控制this的指向,可以使用.call(), .apply(), 和.bind()方法:

  • .call().apply()允许立即调用函数,并指定this的值以及参数。
  • .bind()创建一个新的函数,其this值被固定。

这个通常称为:显式绑定,它是通过call,apply,bind,将函数的this掰弯到一个对象中。

例如: 我们对三个方法逐一分析:

call::

var obj = {
    a: 1
}
function foo(){
    console.log(this.a);
}
foo.call(obj)    // call会把foo中的this指向到obj
//  打印 1

.apply:

var obj = {
    a: 1
}
function foo(x,y){
    console.log(this.a, x + y);
}
foo.apply(obj,[2,2])    // 1 4

如果函数foo存在参数的话,可以在apply函数传入多个参数,第一个参数为this的指向对象,第二个为一个数组对象,像上面那样。

.bind:

const person = {
    name: '小李',
    sayHello: function() {
        console.log(`Hello, ${this.name}`);
    }
};
const sayHelloBound = person.sayHello.bind({ name: '小黄' });
sayHelloBound(); // 输出 "Hello, 小黄"

明白了上面的一些this绑定方式后,你是不是对this有了更加深的印象呢?那么接下来我们针对call方法来手动模拟实现一个mycall的方法吧,学了理论知识,也该实践一下了吧,哈哈哈哈。

首先我们清楚,这三个方法(call、apply、bind)都是传入this将要指向的对象,以及参数的传入,在上面我们了解到隐式绑定的方法可以将函数的this指向一个对象,且主要就是将该函数挂载到指向的对象上,基于这一点,我们可以来手动实现它了:

var obj = {
    a: 1
}
function foo(){
    console.log(this.a);
    return 123  //函数可能存在返回值
}

Function.prototype.mycall = function(){
    
}

如果按照这个模板去实现mycall的话我们可以分析,mycall的实现分为以下步骤:

  1. 拿到foo
  2. 将foo引用到obj
  3. 让obj触发foo
  4. 移除掉obj身上的foo 所以我们可以看看如下的实现代码:
var obj = {
    a: 1
}
function foo(){
    console.log(this.a);
    return 123  //函数可能存在返回值
}

Function.prototype.mycall = function(){
    // arguments 函数内会接受到的所有参数
    const context = arguments[0]   // 拿到第一个参数 ,obj对象就等于 context 对象
    const args = Array.from(arguments).slice(1) // 一个数组[]对象,mycall可能接收不止一个参数,这里是将后面的参数切割下来放在args数组内
    
    context.fn = this;     // this指向 foo函数 ,并把函数挂载到obj对象内。
                        
    const res =  context.fn(...args)   // 执行
    delete context.fn   // 移除函数fn
    return res
}

let res = foo.mycall(obj,4,5)
console.log(res);

由于mycall内部出现了this,这个this本能来是指向mycall函数对象的,但是该函数挂载在Function的原型上(Function.prototype.mycall),因为foo也是一个函数,所以foo的隐式原型就指向Function的显式原型,当foo函数调用mycall函数时,我们有foo.mycall(obj,4,5),这将触发隐式绑定,所以函数mycall的this就是foo函数。

我们将fn 函数挂载到传入的obj对象上,然后直接执行掉fn函数,这里也是触发this的隐式绑定。拿到结果存储,且此时的this是指向context的,也就是obj对象。

然后移除函数,我们就使得函数foo的this会指向obj,实现了和call一样的方法。

image.png

希望小伙伴们可以自行尝试哦,这样记忆会更加深刻呢!!

为了让对象中的函数有能力访问对象中自己的属性,this可以显著的提升代码质量,减少上下文参数的传递 理解并掌握this的这些特性,可以帮助你写出更加健壮和可预测的JavaScript代码。