【JS进阶】this指向

本文详细介绍了JavaScript中的this关键字,包括this的用途、何时确定其指向、调用位置的概念以及this的绑定规则,如默认绑定、隐式绑定、显式绑定和new绑定。此外,还特别提到了箭头函数的this行为,它不遵循常规规则,而是继承自外层作用域的this。理解这些内容有助于开发者更好地掌握JavaScript中的函数上下文和对象引用。

详解JS中的this

1.关于this

1.1 为什么需要使用this

this提供了一种更加优雅的方式来隐式“传递”一个对象引用,因此可以将 API 设计 得更加简洁并且易于复用。

this 使函数可以自动引用合适的上下文对象。

1.2 this是什么?

在学习 JS 的过程中,往往会产生这样的误解:this指向自身或函数的作用域。

但实际上:

this 既不指向函数自身也不指向函数的词法作用域;

this的指向,是在函数被调用的时候确定的,也就是执行上下文被创建时确定的;

this 的指向和函数声明的位置没有任何关系,只取决于函数的调用位置(也就是函数的调用方法);

在函数执行过程中,this一旦被确定,就不可更改了

2.this的解析

先来了解几个概念。

2.1 调用位置

调用位置就是函数在代码中被调用的位置(而不是声明的位置)

寻找调用位置就是寻找“函数被调用的位置”,其中最重要的就是分析调用栈。(即为了到达当前执行位置所调用的所有函数)

而我们所需要的调用位置就在当前执行函数的前一个调用中。

调用栈又称为“执行栈”。调用栈主要用于记录代码执行位置、当前执行环境。

例如:

function baz() { 
    // 当前调用栈是:baz 
    // 因此,当前调用位置是全局作用域 
    console.log( "baz" ); 
    bar(); // <-- bar 的调用位置 
} 

function bar() { 
    // 当前调用栈是 baz -> bar 
    // 因此,当前调用位置在 baz 中 
    console.log( "bar" ); 
    foo(); // <-- foo 的调用位置 
} 

function foo() { 
    // 当前调用栈是 baz -> bar -> foo 
    // 因此,当前调用位置在 bar 中 
    console.log( "foo" ); 
} 

baz(); // <-- baz 的调用位置
2.2 绑定规则
a.默认绑定

针对独立函数调用,同时也是无法应用其它绑定规则时的默认规则

此时this指向全局对象window

例如:

function foo() {
    console.log( this.a ); // this指向全局对象
}
var a = 2;
foo(); // 2

对于默认绑定来说,决定this绑定对象的并不是调用位置是否处于严格模式,而是调用位置的函数体是否处于严格模式。

如果函数体处于严格模式this会被绑定到undefined,否则this会被绑定到全局对象。

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

var a = 2; 
(function(){  
    "use strict"; 
    foo(); // 2,严格模式下与 foo() 的调用位置无关
})();
b.隐式绑定

在一个函数上下文中,this由调用者提供,由调用函数的方式来决定。

如果调用的函数,被某一个对象所拥有,那么该函数在调用时,内部的this指向该对象

例如:

function foo() {
    console.log( this.a );
}
var obj = {
    a: 2,
    foo: foo
};
obj.foo(); // 2
// 调用位置会使用 obj 上下文来引用函数,
// 因此你可以说函数被调用时 obj 对象“拥有”或者“包含”它
// 所以此时的 this 指向调用 foo 函数的 obj 对象

对象属性引用链中,只有最顶层或者说最后一层会影响调用位置,也就是说this指向最终调用函数的对象。举例来说:

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

var obj2 = {
    a: 42,
    foo: foo
};

var obj1 = {
    a: 2,
    obj2: obj2
};

obj1.obj2.foo(); // 42,此时的 this 指向 obj2 对象

此时最终调用函数 foo() 的为 obj2,因此其中的this指向 obj2 对象。

c.隐式丢失
function foo() {
    console.log( this.a );
}

var obj = {
    a: 2,
    foo: foo
};

var bar = obj.foo; // 函数别名!函数的引用而不是函数的调用!!!
var a = "oops, global"; // a 是全局对象的属性
bar(); // "oops, global"

// 虽然 bar 是 obj.foo 的一个引用,但是实际上,它引用的是foo 函数本身,
// 因此此时的 bar() 其实是一个不带任何修饰的函数调用,因此应用了默认绑定

d.显式绑定 —call()apply()方法

call()apply()方法,它们的第一个参数是一个对象,它们会把这个对象绑定到this,接着在调用函数时指定这个this

function foo() {
    console.log( this.a );
}
var obj = {
    a:2
};
foo.call( obj ); // 2
// 在调用 foo 时强制把它的 this 绑定到 obj 上

后面的文章会对这两个方法做详细的解析,这里就不多说了。

e.new绑定

JavaScript 中,构造函数只是一些使用new操作符时被调用的函数。它们并不会属于某个类,也不会实例化一个类。实际上,它们甚至都不能说是一种特殊的函数类型,它们只是被new操作符调用的普通函数而已

那么**new操作符做了哪些操作呢?或者说发生构造函数调用**时,执行了哪些操作呢?

  1. 在内存中创建一个全新的对象,并为其链接原型
  2. 构造函数的作用域赋给新对象,因此,this就指向了这个新对象;
  3. 执行构造函数中的代码,为这个新对象添加属性、方法等;
  4. 若函数没有返回其它对象,那么new表达式中的函数调用会自动返回这个新对象;(这也是为什么构造函数里面不需要return)

实际上,我们可以模拟一下这个过程:

function mynew() {
    // 1. 新建⼀个实例对象
    let obj = {};
    // 2. 获得构造函数
    let con = [].shift.call(arguments);
    // 3. 链接原型
    obj.__proto__ = con.prototype;
    // 4. 绑定this,执⾏构造函数
    let res = con.apply(obj,arguments)
    // 5. 返回新对象
    return typeof res === 'object'? res : obj;
}

下面是一个实例:

function foo(a) {
    this.a = a;
}
var bar = new foo(2);
console.log( bar.a ); // 2
// 使用new 来调用foo(..)时,我们会构造一个新对象并把它绑定到foo(..)调用中的this上

了解了 new ⼀个实例对象的过程后,我们发现在普通函数作为构造函数时,其内会改变 this 指向以及修改原型,⽽箭头函数⽆法改变this指向以及没有原型,所以不能作为构造函数使⽤!

2.3 判断this

this指向的判断可以按照下面的优先级顺序来判断函数在某个调用位置应用的是那条规则:

  1. 函数是否在new中调用?— new绑定

    如果是,则此时**this绑定的是最新创建的对象**。

    例如:

    var bar = new foo();
    
  2. 函数是否通过callapply进行显式绑定,或者硬绑定调用?— 显式绑定

    如果是,则此时**this绑定的是指定的对象**。

    例如:

    var bar = foo.call(obj2);
    
  3. 函数是否在某个上下文对象中调用?— 隐式绑定

    如果是的话,this绑定的是那个上下文对象

    例如:

    var bar = obj1.foo();
    
  4. 如果都不是的话,使用默认绑定。

    如果在严格模式下,就绑定到undefined,否则绑定到全局对象

2.4 绑定的例外情况
  1. 被忽略的this

    null 或者undefined作为this的绑定对象传入callapply或者bind,这些值在调用时会被忽略,实际应用的是默认绑定规则

    例如:

    function foo() {  
        console.log( this.a ); 
    } 
    
    var a = 2; 
    foo.call( null ); // 2
    
  2. 间接引用

    间接引用最容易在赋值时发生;间接引用时,调用这个函数会应用默认绑定规则

    例如:

    function foo() {
        console.log( this.a );
    }
    var a = 2;
    var o = { a: 3, foo: foo };
    var p = { a: 4 };
    o.foo(); // 3
    (p.foo = o.foo)(); // 2
    

    赋值表达式p.foo=o.foo的返回值是目标函数的引用,也就是 foo 函数的引用。

    因此调用位置是 foo() 而不是 p.foo() 或者 o.foo()。

2.5 箭头函数的this

箭头函数并不是使用function关键字定义的,而是使用被称为“胖箭头”的操作符 => 定 义的。

箭头函数不使用 this的四种标准规则,而是根据外层(函数或者全局)作用域来决定 this

function foo() { 
    // 返回一个箭头函数  
    return (a) => { 
        //this 继承自 foo() 
        console.log( this.a );  
    }; 
} 
var obj1 = {  
    a:2 
}; 
var obj2 = {  
    a:3 
};

var bar = foo.call( obj1 ); 
bar.call( obj2 ); // 2, 不是 3 !

// foo() 内部创建的箭头函数会捕获调用时 foo() 的 this。
// 由于 foo() 的 this 绑定到 obj1, bar(引用箭头函数)的 this 也会绑定到 obj1,

注意,this一旦被确定,就不可更改,所以箭头函数的绑定无法被修改。(new 也不行!)

总结

如果要判断一个运行中函数的this绑定,就需要找到这个函数的直接调用位置。找到之后就可以顺序应用下面这四条规则来判断 this 的绑定对象。

  1. new 调用?绑定到新创建的对象
  2. call 或者 apply(或者 bind)调用?绑定到指定的对象
  3. 由上下文对象调用?绑定到那个上下文对象
  4. 默认:在严格模式下绑定到 undefined,否则绑定到全局对象

可以用一张图来总结:
在这里插入图片描述
ES6 中的箭头函数并不会使用四条标准的绑定规则,而是根据当前的词法作用域来决定 this,具体来说,箭头函数会继承外层函数调用的 this 绑定(无论 this 绑定到什么)。这其实和 ES6 之前代码中的 self = this 机制一样。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值